ふせんUIを使ったタスク管理とチャットを融合したチームのタスク管理コミュニケーションツール「QnQTree」を開発しているあどにゃーです。
今回サービスにFirebaseを用いたメール・パスワード認証を組み込んだので、ハマった点についてまとめていきたいと思います。ハマりポイントがメインなので正常系通れば良いという初心者向けというよりは、security rule含めて最低限サービスで使えるレベルを実装する人向けです。
※Firebase v9記述です。(v8と関数名は変わりません)
パスワード認証の想定されるケース
・新規ユーザの初回登録(Sign up)
・登録済みユーザがSign inできず、パスワードをリセット
・登録済みユーザが通常のSing in
・Sing in済みユーザがパスワードを変更
パスワード認証を実装する場合、大きくわけて上記4つのケースを網羅する必要があります。
それぞれについて説明していきます。
新規ユーザの初回登録(Sign up)
初回はFirebase Authenticationにユーザアカウントがないので、まずはメールとパスワードを入力してアカウントを作ることになります。
アカウントを作ってそのまま使えるようにしてしまうと、他人のメアドでSing upする「なりすまし」を防げません。
そのため、本人のメールアドレス確認を行うメール認証のステップを追加します。
パスワード認証でアカウントが作成された時のauth.currentUserの情報はメール認証前のverifiedEmail=falseの状態です。
verifiedEmail=falseのUserはセキュリティルールでcreateやupdateの機能をブロックして、メール認証完了したタイミングで機能を開放するNo4~8を実装しなければいけません。
このあたりを書いている記事を見かけませんでしたので書いて見ます
password認証で作られるauth.currentUser
email: xxx@xx.jp
verifiedEmail: false
provideData[0].providerId : password
provideId: firebase
google認証で作られるauth.currentUser
email: xxx@gmail.com
verifiedEmail: true
provideData[0].providerId : google.com
provideId: firebase
新規ユーザの初回登録の実装flowは下記になります。
★をつけているのは、他の記事に載っていないハマりポイントです。
実装フロー
1. メールアドレス・パスワード入力
2. すでに登録されたメアドか確認(fetchSignInMethodsForEmail)
3. 新規ユーザーアカウント作成(createUserWithEmailAndPassword)
4. 本人確認メールの送信(sendEmailVerification)
5. 本人確認できるまではすべての機能をロック(firestore.rules) ★
6. 本人確認メールのリンクをクリックして本人確認(applyActionCode)
7. auth.currentUser.reload()で本人のauth.ccurentUserを更新 ★
8. auth.currentUser.getIdToken(true)でrulesのauth.token.email_verifiedを更新 ★
No1~4の実装 認証メールの送信まで
passwordLogin: async function (email, password) {
// すでにSINGUP済かチェック
try {
const signInMethods = await fetchSignInMethodsForEmail(auth, email)
// すでにサインアップ済ならTrue
this.isCreatedUser = (signInMethods.length !== 0)
} catch (error) {
console.error(error)
this.alertMsg = '情報の取得に失敗しました'
return
}
// すでにSIGNUP済み
if (this.isCreatedUser) {
// 通常のSIGN INなので後述
} else {
// SINGUP前
console.log('新規ユーザのSign UP')
try {
// サインアップします
const userCredential = await createUserWithEmailAndPassword(auth, email, password)
let user = userCredential.user
user.displayName = email.substr(0, email.indexOf('@'))
// 独自のsingUp関数でFirestoreにもuser情報を保存
await signUp(user)
// 認証確認用のメールを送信
await sendEmailVerification(auth.currentUser)
// 別ページに移動
Router.push({ path: '/editprofile' })
} catch (error) {
console.log(error)
this.alertMsg = 'アカウント作成に失敗しました'
}
}
}
認証メールをクリックして戻ってきた後のNo5~8の実装は下記となります。
sendEmailVerificationで認証メールを飛ばすことができ、認証メールにはqueryにmode,oobCodeが含まれています。
modeはresetPassword, recoverEmail, verifyEmailのいずれかの値が入っており、ユーザがどの条件でURLから飛んできたかを示します。
oobCodeはリクエストの検証するためのワンタイムコードです。この検証コードを使ってメール認証します。
ハマりポイントは2箇所
1つ目:applyActionCode(auth, actionCode)でメール認証を適応しても、auth.currentUser.emailVerifiedはFalseのままであるという点です。auth.currentUser.reload()を行って、authの情報を更新する必要があります。
2つ目:firestore.rulesのセキュリティルールauth.token.email_verifiedはauth.currentUser.getIdToken(true)でIdTokenを更新してあげないとしないとFalseのままです。
この2つに触れている記事がほとんどなく、実装をする上でかなりハマりました
No5~8の実装 認証メールの適応
async verifyEmail () {
this.modalMsg = ''
const currentUser = auth.currentUser
this.isEmailVerified = currentUser.emailVerified
// すでに認証済
if (this.isEmailVerified) {
this.modalMsg = '認証が完了しました'
Router.push({ path: '/editprofile' })
}
// 認証前の場合は認証処理を行う
const actionCode = this.$route.query.oobCode
if (actionCode && !currentUser.emailVerified) {
// actionCodeを用いてメール認証を行う
try {
await applyActionCode(auth, actionCode)
// メール認証を行ってもauthをreloadしないとUser情報が更新されないのでreloadする
await auth.currentUser.reload()
// firestore.rules側も更新されていないのでgetIdTokenでrefleshする
await auth.currentUser.getIdToken(true)
} catch (error) {
// 確認メールを一定時間クリックしないと無効化されてエラーになる
this.modalMsg = 'メール認証に失敗しました'
console.log(error)
}
try {
const applyUser = auth.currentUser
await getMyUserData(this.loginUserId)
this.isEmailVerified = applyUser.emailVerified
if (this.isEmailVerified) {
this.modalMsg = '認証が完了しました'
Router.push({ path: '/editprofile' })
} else {
this.modalMsg = 'メール認証が完了していません'
}
} catch (error) {
this.modalMsg = 'メール認証に失敗しました'
console.log(error)
}
}
},
セキュリティルールは実装している機能によりますが、firestoreのcreateやupdateをrequest.auth.token.email_verified == trueでないと行えないように設定します。
security rules
allow create: if request.auth.uid != null
&& request.auth.token.firebase.sign_in_provider != 'anonymous'
&& request.auth.token.email_verified == true
登録済みユーザがSign inできず、パスワードをリセット
実装flowは下記になります。こちらは特にハマるポイントはありませんでした。
実装フロー
1. パスワードのリセット sendPasswordResetEmail(auth, this.email)
2. 再発行メールのクリック
3. リセットコードの確認 verifyPasswordResetCode(auth, actionCode)
4. パスワードの再発行 confirmPasswordReset(auth, actionCode, this.password)
5. サインイン signInWithEmailAndPassword(auth, this.email, this.password)
async sendPasswordReset (email) {
this.alertMsg = ''
try {
await sendPasswordResetEmail(auth, email)
this.alertMsg = '再発行メールを送信しました。メールから再認証してください'
} catch (error) {
console.error(error)
this.alertMsg = '発行に失敗しました'
}
},
async confirmPasswordResetUser () {
const actionCode = this.$route.query.oobCode
if (actionCode && this.$route.query.mode === 'resetPassword') {
try {
await verifyPasswordResetCode(auth, actionCode)
// 成功したので別画面を出す
this.isResetPassword = true
} catch (error) {
// 確認メールを一定時間クリックしないと無効化されてエラーになる
console.log(error)
}
}
},
resetPasswordLogin: async function (email, password) {
this.alertMsg = ''
// すでにSINGUP済かチェック
try {
const signInMethods = await fetchSignInMethodsForEmail(auth, email)
// すでにサインアップ済ならTrue
this.isCreatedUser = (signInMethods.length !== 0)
} catch (error) {
console.error(error)
this.alertMsg = '情報の取得に失敗しました'
return
}
// SINGUP前なら新規ユーザなのでパスワード再発行処理ではない
if (!this.isCreatedUser) {
this.alertMsg = '新規登録を行ってください'
this.isResetPassword = false
return
}
// すでにSIGNUP済み && 新規パスワードがある && 再発行認証コードがある
console.log('パスワード再発行処理')
try {
const actionCode = this.$route.query.oobCode
await confirmPasswordReset(auth, actionCode, password)
} catch (error) {
console.error(error)
this.alertMsg = '再発行メールの有効期限が切れています'
this.isResetPassword = false
}
// パスワードのresetが完了したのでSING IN
try {
const userCredential = await signInWithEmailAndPassword(auth, email, password)
const user = userCredential.user
if (!user.emailVerified) {
// メアド確認終わってない
this.alertMsg = 'メール確認が完了していません'
return
} else {
// メアド認証が完了している
Router.push({ path: '/' })
this.isResetPassword = false
}
} catch (error) {
console.log(error)
this.alertMsg = '予期しないエラーが発生しました'
this.isResetPassword = false
}
},
登録済みユーザが通常のSing in
実装flowは下記になります。こちら実は新規登録のFlowに混ざっています。
もう一度コードを記載すると下記になります。
実装フロー
- メールアドレス・パスワード入力
- すでに登録されたメアドか確認(fetchSignInMethodsForEmail)
- 新規ユーザーのSIgnIn (signInWithEmailAndPassword)
passwordLogin: async function (email, password) {
// すでにSINGUP済かチェック
try {
const signInMethods = await fetchSignInMethodsForEmail(auth, email)
// すでにサインアップ済ならTrue
this.isCreatedUser = (signInMethods.length !== 0)
} catch (error) {
console.error(error)
this.alertMsg = '情報の取得に失敗しました'
return
}
// すでにSIGNUP済み
if (this.isCreatedUser) {
console.log('既存ユーザのSign IN')
try {
const userCredential = await signInWithEmailAndPassword(auth, email, password)
const user = userCredential.user
if (!user.emailVerified) {
// メアド確認終わってない
this.alertMsg = 'メール確認が完了していません'
return
} else {
// メアド認証が完了している
Router.push({ path: '/' })
}
} catch (error) {
console.log(error)
this.alertMsg = 'パスワードが間違っています'
}
} else {
// SINGUP前
}
}
Sing in済みユーザがパスワードを変更
実装フローは下記となります。こちらも★がハマりポイントです。
Sign inしてしばらく期間が経過しているようなアカウントでは、クレデンシャル情報が古くなっており、再認証しないとupdatePassword()を使うことができないケースがあるようです。
ですので、必ず再認証処理を入れてください。
実装フロー
- クレデンシャルの取得 EmailAuthProvider.credential(email, oldPassword) ★
- 再認証 reauthenticateWithCredential(user, authCredential) ★
- パスワードの更新 updatePassword(user, newPassword)
async updateNewPassword () {
this.alertPasswordMsg = ''
if (this.newPassword !== this.confirmedPassword) {
this.alertPasswordMsg = '確認passwordが一致しません'
return
}
const currentUser = auth.currentUser
const authCredential = EmailAuthProvider.credential(currentUser.email, this.oldPassword)
// 認証情報の更新(ログインしっぱなしで最近ログインしていないと無効化されている)
try {
await reauthenticateWithCredential(currentUser, authCredential)
} catch (error) {
console.error(error)
this.alertPasswordMsg = '現在のpasswordが間違っています'
return
}
// パスワードの更新
try {
await updatePassword(currentUser, this.newPassword)
this.alertPasswordMsg = '更新に成功しました。新しいパスワードをお使いいただけます'
this.oldPassword = ''
this.newPassword = ''
this.confirmedPassword = ''
} catch (error) {
console.log(error)
this.alertPasswordMsg = '更新に失敗しました'
}
},
まとめ
下記チームのタスク管理サービスで実装してるので良かったらログインしてみてください。
非エンジニアの方だとSing upとSing inの違いがわからない人も多いので、
そのあたりを意識しないでLoginできるようにつくっています。
https://qnqtree.com/about