WebTecNote

[JS] Firebaseの覚書 ① メールアドレス認証

Firebase Authenticationを利用する認証でメールアドレス&パスワード、メールリンク、Google、Twitter…と一通り実装してみたなかではメールアドレス&パスワード認証が一番面倒臭かった。
この記事はその面倒くさいメアド認証にまつわる各種手順を脳内整理がてらメモったものです。

殆どはFirebaseの公式ドキュメントに書いてあることなので、主要な関数名などにはドキュメントへのリンクを貼ってあります。

メールアドレス&パスワードで新規登録

Firebaseだとメールアドレスを使う認証では「メールアドレス&パスワード」と「メールリンク」が選択できる。
メールアドレス&パスワードを利用した場合、確認メール送信までがフェーズ1という感じ。

  1. メールアドレス・パスワード入力
  2. 登録の確認(auth().fetchSignInMethodsForEmail)
  3. ユーザーアカウント作成(auth().createUserWithEmailAndPassword)
  4. 確認メールの送信(currentUser.sendEmailVerification)
// async function
try {
  // 登録の確認
  const providers = await firebase.auth().fetchSignInMethodsForEmail(email);

  if (providers.findIndex(p => p === firebase.auth.EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD) !== -1) {
   return 'すでに登録されているようです';
  }

  // アカウント作成
  await firebase.auth().createUserWithEmailAndPassword(email, password);

  // 確認メールの送信
  await firebase.auth().currentUser.sendEmailVerification({
    url: 'https://example.com/mypage/',
    handleCodeInApp: false,
  });

  // TODO: メール送信完了表示などする

} catch (e) {
  // e.code で処理分ける
  console.error(e);
}

世間一般的によくみるメールアドレスによる登録の流れはこんな感じなんだけど:

  1. メールアドレス入力
  2. 確認メール送信
  3. メールのURLをクリックしてサイトに戻る(=メアド確認完了)
  4. 名前とかパスワードとか残りの情報入力
  5. 登録完了

Firebaseだと 確認メールを送信する sendEmailVerification は firebase.User(auth.currentUser) のメソッドなので、createUserWithEmailAndPassword を実行する前には使えず、①〜②の『メールアドレスだけ入力させて確認メールを送信』 はできない。
必ずパスワードとセットで入力させてユーザーアカウントを作成する必要がある。

createUserWithEmailAndPasswordを実行して成功すればUserCredentialが戻るので、そこからユーザー情報を得ることができる。

const { user } = await firebase.auth().createUserWithEmailAndPassword(email, password);
// user.displayName ユーザー名
// user.uid ユーザーID
// user.email ユーザーメアド
// user.emailVerified メアド確認状況

メールアドレスの確認状況はemailVerifiedで分かる。
確認が終わってなければ(=false)仮登録状態ということにできる。

// async function
try {
  const { user } = await firebase.auth().signInWithEmailAndPassword(email, password);

  if (!user.emailVerified) {
    // メアド確認終わってない
  } else {
    // メアド確認終わってる
  }
} catch (e) {
  // signInWithEmailAndPasswordのエラー
  console.error(e);
}

確認メール送信後から登録完了とするまでをフェーズ2とすると、流れはこんな感じで:

  1. メールのURLをクリックしてサイトに戻る
  2. アクションコードの確認(auth().applyActionCode)
  3. ユーザー名登録(currentUser.updateProfile)
  4. マイページにリダイレクト

ユーザーがメールに記載されたURLをクリックしてサイトに戻ってきた時の処理は概ねここに書いてある通りやればよくて、
クエリでパラメータが4つ(mode, oobCode, apiKey, continueUrl)付与されるのでそれらを利用する。

// async function
try {
  // アクションコードの確認
  await firebase.autn().applyActionCode(oobCode)

  // メアド確認完了

  // TODO: 名前入力画面へ

} catch (e) {
 // applyActionCodeのエラー
  console.error(e)
};

メールアドレス&パスワードで登録したユーザーの場合currentUser.displayNameは空なので、アクションコードの確認が終わったら設定させると良いでしょう。

ID トークンの更新

ServiceWorkerによるセッション管理をしていて、admin.auth().verifyIdTokenDecodedIdTokenからユーザー情報を利用している場合、applyActionCodeでメールアドレスが確認できても、IDトークンではemailVerifedがfalseのままになってるので、getIdToken(true)で更新しておく。

// idToken更新
await firebase.auth().currentUser.getIdToken(true)

IDトークンは認証で更新されるが、updateProfileなど情報更新では変更されないので都度forceRefreshが必要。
これを忘れると、リロードで前の情報に巻き戻る現象が発生する。

メールアドレス変更と再認証

メアド変更、パスワード変更などの操作の前には再認証が必要になる。

メールアドレスの場合は EmailAuthProvider を使って認証情報を発行し、reauthenticateAndRetrieveDataWithCredentialで再認証する。

// async function
try {
  const credential = firebase.auth.EmailAuthProvider.credential(email, password);

  const { user } = await firebase.auth().currentUser.reauthenticateAndRetrieveDataWithCredential(credential);

  // 再認証完了

} catch (e) {
  //reauthenticateAndRetrieveDataWithCredential のエラー
  consoel.error(e);
}

ちなみに、OAuth系プロバイダのようにインスタンスを作成してしまうと、EmailAuthProvider.credential is not a functionというエラーが出る。

const google = new firebase.auth.GoogleAuthProvider();
const email = new firebase.auth.EmailAuthProvider(); // まちがい

メールアドレス変更は、currentUser.updateEmailで。
変更した時点でまたメールアドレス未確認状態になるので、確認メールも送信する。

try {
  // メアド変更
  await firebase.auth().currentUser.updateEmail(email);

  // idToken更新
  await firebase.auth().currentUser.getIdToken(true);

  // 確認メールの送信
  await firebase.auth().currentUser.sendEmailVerification({
    url: 'https://example.com/setting/email',
    handleCodeInApp: false,
  });

  // TODO: メール送信完了の表示などする

} catch (e) {
  // e.codeでエラー判別
  switch(e.code) {
    case 'auth/email-already-in-use':
    break;
    case 'auth/invalid-email':
    break;
    case 'auth/requires-recent-login':
    break;
  }
}

メールアドレス変更の取り消し

updateEmailでメールアドレス変更を行うと、元のアドレスの方にリセット用のURLを記載したメールが送信される。
やることはドキュメントに書いてある通りで、

  1. アクションコードの確認(checkActionCode)
  2. メールアドレスを復元(applyActionCode)
  3. パスワードリセットメールを送信(sendPasswordResetEmail)

の3つ。

// async function
try {
  // アクションコードの確認
  const info = await firebase.auth().checkActionCode(oobCode);
  
  // Get the restored email address.
  restoredEmail = info['data']['email'];

  // メアド復元
  await firebase.auth().applyActionCode(oobCode);

  // 元のメアドにパスワードリセットメールを送信
  await firebase.auth().sendPasswordResetEmail(restoredEmail);
 
  // 復元完了
 // TODO: メール送信完了表示などする

} catch (e) {
  console.error(e)
}

applyActionCodeを実行すると新メアドに送られたverifyEmailのアクションコードは利用できなくなる。

パスワード変更

パスワード変更の流れ:

  1. 再認証
  2. パスワードリセットメールを送信(auth().sendPasswordResetEmail
  3. ユーザーがメールのURLをクリックして戻ってくる
  4. アクションコード確認(auth().verifyPasswordResetCode)
  5. ユーザーが新パスワード入力
  6. 新パスワード設定(auth().confirmPasswordReset
  7. 新パスワードで再認証(auth().signInWithCredential
/**
 * パスワードリセット
 */
async handleResetPassword() {
  this.isLoading = true;

  try {
    // コードの確認
    const email = await firebase.auth().verifyPasswordResetCode(this.oobCode);

    // TODO: 新パスワード入力のUIを表示する

  } catch (e) {
    console.error(e);
  }
}

confirmPasswordResetは新パスワード入力フォームのsumitハンドラで実行する

/**
 * 新しいパスワードの設定
 */
async saveNewPassword() {

  try {
    // 新パスワード設定
    await firebase.auth().confirmPasswordReset(this.oobCode, this.newPassword);

    // 再認証
    const credential = firebase.auth.EmailAuthProvider.credential(this.email, this.newPassword);

    const { user } = await firebase.auth().signInWithCredential(credential);

    // 認証完了
    // TODO: マイページに移動したりする

  } catch (e) {
    console.error(e);
  }
}

他の認証プロバイダのリンク

リンクする方法はドキュメントにある通り
で、ログイン画面から同じメールアドレスを持つ別のアカウントでログインしてしまった時にも自動でリンクされる。

メールアドレス&パスワード認証でアカウントを作ったユーザーが、同じメールアドレスのGoogleアカウントによるログインを行なった場合、挙動が2つに別れる。

  • メールアドレス確認済み → メールアドレス&パスワードにGoogleの認証情報がリンクされる
  • メールアドレス未確認 → メールアドレス&パスワードがGoogleの認証情報で上書きされる

メールアドレス確認済みであれば、ログインした時に追加の認証プロバイダが自動でリンクされていくので、以降はどの認証方法でもログインできるようになる。

メールアドレス未確認だと別の認証方法でログインした時点でその認証方法で上書きになるので、以降はパスワードを使ってログインすることができなくなる。

設定済みの認証方法については User.providerData に配列で持っている。

ハマりどころメモ

メールアドレス確認後のverifyEmail更新

メールアドレスが確認された時何かしらイベントが発生すると思ったらしないので、onAuthStateChangedは無力であった…。
最新の情報への反映は auth().currentUser.reload() でできるので、確認メール再送信など用意している場合は、メール送信前にreloadを実行してverifyEmailをチェックする。

認証の時間切れ

メールアドレス変更やアカウント削除には直近の認証が必須で、最終ログインから時間が過ぎていながらこれらの操作をするとauth/requires-recent-loginエラーが発生する。時間切れまでの時間は5分くらいです。

メールリンクで新規登録

メールリンク認証…簡単ログイン、マジックリンクログインなどとも言うかな。
パスワード不要でログインできて便利な反面、パスワード登録方式に慣れきったユーザーだと仮登録と誤認しやすいので、見間違えない誘導が必要だと思う。

メールリンクを利用した場合の登録フロー:

  1. メールアドレス入力
  2. 登録の確認(auth().fetchSignInMethodsForEmail)
  3. メールリンク送信(auth().sendSignInLinkToEmail
  4. メールのURLをクリックしてサイトに戻る
  5. メールリンク確認(auth().isSignInWithEmailLink
  6. 認証(auth().signInWithEmailLink
  7. ユーザー名登録(currentUser.updateProfile)
  8. マイページにリダイレクト
//async function
try {
  // 登録の確認
  const providers = await firebase.auth().fetchSignInMethodsForEmail(email);

  if (providers.findIndex(p => p === 'emailLink') === -1) {
   return '登録がお済みでないようです。';
  }

  // メールリンクの送信
  await firebase.auth().currentUser.sendSignInLinkToEmail(email, {
    url: 'https://example.com/mypage/',
    handleCodeInApp: true,
  });

  // 確認用に保存しとく
  window.localStorage.setItem('emailForSignIn', email);

} catch (e) {
  // e.code で処理分ける
  console.error(e);
}

メールリンクの場合、sendSignInLinkToEmailsendEmailVerificationも兼ねるので、URLをクリックして戻ってきた時点でEmailVeryfiedはtrueになる。

URLクリック後の処理はドキュメントに書いてある通りで、やることはauth().signInWithEmailLinkにメアドとURLを投げるだけ。

// async function

if (!firebase.auth().isSignInWithEmailLink(window.location.href)) {
  // 不正なURL
  return await false;
}

try {
  // 送信完了後に保存しといたやつ
  const email = window.localStorage.getItem('emailForSignIn');
  
  if (!email) {
    // 保存されたメアドが見つからなかったので入力を求める
    email = window.prompt('Please provide your email for confirmation');
  }

 const { additionalUserInfo } = await firebase.auth().signInWithEmailLink(email, window.location.href);
 
// ログインしたので保存していたメアド消す
 window.localStorage.removeItem('emailForSignIn');

 if (additionalUserInfo.isNewUser) {
   // 新規ユーザー
 } else {
   // 既存ユーザー
 }
 
} catch (e) {
 console.error(e);
}

signInWithEmailLinkが返すUserCredentialのadditionalUserInfo.isNewUserで新規登録かどうかわかる。

メールリンクで再認証

メールリンクの送信はsendSignInLinkToEmailで。
再認証にはパスワードと同じくEmailAuthProviderを使うが、関数がcredentialWithLinkに変わる。
アクションコード確認画面でemailと共にリンクURLを渡すと再認証できる。

// async function
try {
  const credential = firebase.auth.EmailAuthProvider.credentialWithLink(email, window.location.href);

  const { user } = await firebase.auth().currentUser.reauthenticateAndRetrieveDataWithCredential(credential);

  // 再認証完了

} catch (e) {
  consoel.error(e);
}

メールアドレス&パスワードとメールリンクの共存

メアドから認証方法を調べられるので、ログインや登録の時点でどっちか選ぶUIを出すなどすればいいが、FirebaseUI使わずにやろうとしたらだいぶ面倒くさい。
メールリンクだけにする方が楽。

モバイルバージョンを終了