🔐

【Firebase Authentication】メールアドレス・パスワード認証をサービスに導入する際に必要になりそうな機能

2022/11/11に公開

はじめに

Firebase AuthenticationではGoogleやTwitterなどの認証機能を簡単に利用でき
自社サービスでのユーザー認証機能(会員向けサービス)の導入ハードルが格段に低いです。
しかし、SNSや他社サービスとの連携は嫌というユーザーも多くいらっしゃることから
サービス内独自のユーザー認証を行えるようにしたほうがよいというニーズもあります。

この記事ではそんなニーズに答えるためにFirebase Authenticationを利用した
メールアドレス・パスワード認証の導入に必要な機能を最低限洗い出し
各機能がどのようなフローになるか、その際に必要なコードの紹介を行います。
公式ドキュメント
https://firebase.google.com/docs/auth/web/password-auth#web-version-9

扱う例外やメールアドレス認証、パスワード再設定など公式ドキュメントでは
まとまって書かれていない部分も書きましたのでもし導入する際には参考にしてみてください。

注意事項

  • Firebase Authentication自体の説明はそんなに書いていません。
  • サンプルのコードは出てきますが実サービスで利用した際の責任は負いません。
  • 電子メールリンク認証については扱いません(類似箇所はあると思うので参考にはなるかもしれません)
  • 間違えている記述があるかもしれません。その際にはご連絡していただけると助かります。

事前準備

  • Firebase Authenticationのログインプロバイダにて「メール/パスワード」を有効にしておきましょう。
  • (任意)Templatesでテンプレート言語を「日本語」にしておきましょう。
    • メールについては後ほど書きます。

追加機能

追加する機能として以下は最低限必要になると思います。

  • 登録
    • メールアドレス認証
  • ログイン
  • ログアウト
  • パスワード再設定(忘れたとき)
  • 退会

それぞれどのようにして実現していくかをフローとコードで紹介します
※ ログアウトは公式通りなのでそちらをお読みください。

ユーザーの状態定義

追加機能の前にユーザーの状態は最低でも以下の3つが定義できます。

  • 未ログイン
  • ログイン済み・アドレス未認証
  • ログイン済み・アドレス認証済

ログイン済み・アドレス未認証状態があるというのを忘れずに設計しましょう。
アドレスの認証状態はユーザーオブジェクトのemailVerifiedというプロパティから取得することが可能です。

登録機能

Firebase Authentication上のユーザーとしては
メールアドレスとパスワードがあれば作成は可能ですが
アドレスの所持確認をしないと適当なメールアドレス(最悪自分が所持していないもの)で
登録ができてしまいます。
それを回避するために、Firebaseにはメールアドレス所持を確認するための機能が用意されています。

フロー

登録時のフロー

解説

メールアドレスとパスワードを受け取る必要があるため少なくともフォームは用意する必要があります。
メールアドレスとパスワードを受け取ったらそれをベースにFirebase上にユーザーを作成します。
この段階で登録成功した場合、ログイン済み・アドレス未認証となります。
(onAuthStateChangedを登録していた場合、処理が実行されます)
この新規登録したユーザ宛にアドレス認証メールを送信し一旦完了となります。

送信されるメールはコンソールのAuthenticationのTemplates内の
「メールアドレスの確認」になります。以下注意点になります。

  1. 「メールアドレスの確認」で送信されるメールの文面は変更ができません
  • 文面内にユーザ名が必須で埋め込まれていますのでユーザ作成後に固定で「XXサービスユーザー」やフォームで入力していただいたものを紐付ける必要があります。
  1. アクションURL(リンクに記載できるURL)は全テンプレート内で1つだけです
  • デフォルトのものを利用するとFirebaseで用意された英語のUIになるためサービス側で基本的には用意し設定することになると思います(それ前提です)
  • メールアドレスの確認とパスワードの再設定のメール内で記載できるURLは同一のものになります
  • modeというパラメータが付与されるためそちらをページ上で判別し処理を変更する必要があります

これらの注意事項に添えない場合は、CloudFunctions内で各種メール文面を作成しSendgridなどのメールPFを利用して登録ユーザあてにメールを送信するなどの方法が挙げられます。
(リンクの生成はSDKでできます: ドキュメント)
導入する際に引っかかるポイントだと思うので必ず抑えておきましょう。

それを踏まえた上でコードは以下のようになります。

登録を処理するためのコード(最低限)
import {
  getAuth,
  createUserWithEmailAndPassword,
  applyActionCode
} from "firebase/auth"

/**
 * 入力されたメールアドレスとパスワードでユーザーを新規作成しアドレス認証メールを送信する
 * @param {string} email メールアドレス
 * @param {string} password パスワード
 */
async signup(email, password) {
  const auth = getAuth()
  const userCredencial = await createUserWithEmailAndPassword(email, password)
  await userCredencial.user.sendEmailVerification()
  return
}

/**
 * コードを用いてアドレス認証を完了させる
 * @param {string} oobCode アドレス認証実行用コード
 */
async verifyEmail(oobCode) {
  const auth = getAuth()
  await applyActionCode(auth, oobCode)
  return
}

例外について

予測できるものとしてはいかが考えられます

  1. メールアドレスの形式がおかしい
  2. パスワードが脆弱(そもそも6文字以上であれば登録可能なのでフォームで強固なバリデーションが必須)
  3. すでにメールアドレスが利用されていた
  4. 認証コード(oobCode)の有効期限が切れている
  5. 認証コードが不正な値(or 利用済み)

1と2についてはフォームでバリデーションをするとして3,4,5については
Firebaseに問い合わせ無いとわからないことなのでしっかりと例外を書いておきましょう。
Firebaseのエラーコードについては以下に書かれています。

https://firebase.google.com/docs/auth/admin/errors

登録を処理するためのコード(例外追加)
import {
  getAuth,
  createUserWithEmailAndPassword,
  applyActionCode
} from "firebase/auth"

/**
 * 入力されたメールアドレスとパスワードでユーザーを新規作成しアドレス認証メールを送信する
 * @param {string} email メールアドレス
 * @param {string} password パスワード
 */
async signup(email, password) {
  try {
    const auth = getAuth()
    const userCredencial = await createUserWithEmailAndPassword(email, password)
    await userCredencial.user.sendEmailVerification()
    return
  } catch (error) {
    switch (error.code) {
      case "auth/email-already-in-use":
        // すでにユーザが利用済みである際の処理
	break
      default:
        // その他のエラー処理
    }
  }
}

/**
 * コードを用いてアドレス認証を完了させる
 * @param {string} oobCode アドレス認証実行用コード
 */
async verifyEmail(oobCode) {
  try {
    const auth = getAuth()
    await applyActionCode(auth, oobCode)
    return
  } catch (error) {
    switch (error.code) {
      case "auth/expired-action-code":
        // 有効期限切れの際の処理
	break
      case "auth/invalid-action-code":
        // 不正なコード or 利用済みコードの際の処理
	break
      default:
        // その他のエラー処理
    }
  }
}

ログイン

特別なことはなく、メールアドレス・パスワードを入力していただき
合致したユーザーが登録されていればそのユーザーとして認証、しなければ不正として戻すことになります。
また、登録時のメールアドレス認証が完了していないユーザーをログイン扱いとしないのであれば
その判定と再度認証メールを送るなどの処理が必要になります。

フロー

ログインのフロー

コード

以下のようなコードを用意し呼び出す必要があります。

ログインのコード
import {
  getAuth,
  signInWithEmailAndPassword,
  applyActionCode
} from "firebase/auth"

/**
 * 指定されたメールアドレスとパスワードでユーザ認証する.
 * メールアドレス未認証の場合は認証メールを再度送信する
 * @param {string} email メールアドレス
 * @param {string} password パスワード
 */
async signin(email, password) {
  try {
    const auth = getAuth()
    const userCredencial = await signInWithEmailAndPassword(auth, email, password)
    const isNotVerified = !userCredencial.user.emailVerified
    if (isNotVerified) {
      await reSendVerifyMail(userCredencial.user)
      await signOut()
    }
    return
  } catch (error) {
    switch (error.code) {
      case "auth/user-not-found":
      case "auth/invalid-email":
      case "auth/wrong-password":
        // パスワードが合致しない、ユーザが存在しなかったときの処理
	break
      default:
        // その他のエラー時の処理
    }
    return
  }
}

/**
 * 指定されたユーザに再度アドレス認証メールを送信する
 * @param {Object} user 対象のユーザー
 */
async reSendVerifyMail(user) {
  try {
    if (user) {
      await user.sendEmailVerification()
    }
    return
  } catch (error) {
    switch (error.code) {
      case "auth/too-many-requests":
        // 1分以内は再送できずこのエラーになる.その時の処理.
	break
      default:
        // その他のメール送信失敗時の処理
    }
    return
  }
}

/**
 * ログアウトをする
 */
async signOut() {
  const auth = getAuth()
  await signOut(auth)
}

パスワード再設定

ログインする際のパスワードを忘れてしまったときに行います。
登録メールアドレスを入力していただき、そのアドレス宛に再設定用のメールを送信する
メール内のリンクを踏み、パスワードを再入力したら完了という流れが想定されます。

フロー

パスワード再設定のフロー

解説とコード

フローは長いですがメールアドレス認証と要領は同じです。
ここで注意ですが、メールアドレス認証とパスワード再設定で送信されるメールに
埋め込めるリンク(actionURL)は同じものになります。
区別の仕方としてはmodeパラメータがresetPasswordとなっていればパスワード再設定ということになります。
また、verifyPasswordResetCodeで再設定コードが有効かどうかのチェックができるので
これでチェックをするとエラーを後回しにしなくて良くなります。
これらの処理を実行するコードは以下のようになり、適切なタイミングで呼び出すことになります。

パスワード再設定に関する処理
import {
  getAuth,
  sendPasswordResetEmail,
  verifyPasswordResetCode,
  confirmPasswordReset,
  signOut
} from "firebase/auth"

/*
 * 指定したメールアドレスにパスワード再設定用のメールを送信する
 * メールアドレスはユーザー登録に利用されていなければ送信されない
 * @param {string} email メールアドレス
 */
async sendPasswordResetEmail (email) {
  try {
    const auth = getAuth()
    await firebase.auth().sendPasswordResetEmail(auth, email)
  } catch (error) {
    switch (error.code) {
      case "auth/invalid-email":
      case "auth/user-not-found":
        // メールアドレスが間違っている(登録されていない)場合の処理
	break
      default:
        // その他エラー発生時の処理
    }
    return
  }
}

/**
 * パスワード再設定をするための認証コードを検証する
 * @param {string} oobCode 認証コード
 * @return {boolean} コードの利用可否
 */
async verifyPasswordResetCode (oobCode) {
  try {
    const auth = getAuth()
    await verifyPasswordResetCode(auth, oobCode)
    return true
  } catch (error) {
    switch (error.code) {
      case "auth/expired-action-code":
        // 有効期限切れの場合の処理
	break
      case "auth/invalid-action-code":
        // コードが不正 or 利用済みの場合処理
	break
      default:
        // その他エラー発生時の処理
    }
    return false
  }
}

/**
 * 指定されたパスワードで再設定する
 * @param {string} newPassword 新しく設定するパスワード
 * @param {string} oobCode oobCode 認証コード
 */
async resetPassword (newPassword, oobCode) {
  try {
    const auth = getAuth()
    await confirmPasswordReset(auth, oobCode, newPassword)
    if (auth.currentUser) {
      await signOut(auth)
    }
  } catch (error) {
    switch (error.code) {
      case "auth/expired-action-code":
        // 有効期限切れの場合の処理
	break
      case "auth/invalid-action-code":
        // コードが不正 or 利用済みの場合処理
	break
      default:
        // その他エラー発生時の処理
    }
    return false
  }
}

退会

Firebase Authenticationから認証情報を削除することを退会と呼んでいます。
注意点としましては公式ドキュメントにも書かれていますが削除をする際には
最近サインインしている必要があるそうです。
(最近はどれくらいかについては書かれおらず...書かれている箇所などあったら教えてほしいです...)
なので、この問題を解決するために今回は退会の前にかならず再認証を行うようにします。

フロー

退会時のフロー

解説とコード

再認証について、公式ドキュメントに書かれています。
credencialの作り方がTODOとなっていますが、これはProviderごとに変わるからと思われます(多分)。
今回メールアドレス・パスワード認証ではEmailAuthProviderを利用していることになっているので
これを利用してCredencialを作成、再認証します。
再認証に成功したらユーザー削除を行う流れになります。
この関連に必要なコードとしては以下のようになり、これを適切なタイミングで呼び出す必要があります。

退会処理のコード
import {
  getAuth,
  EmailAuthProvider,
  reauthenticateWithCredential
} from "firebase/auth"

/**
 * 認証済みユーザーアカウントを削除する
 * 
 * @param {string} email メールアドレス
 * @param {string} password パスワード
 */
async deleteAccount(email, password) {
  try {
    const user = getAuth().currentUser
    const userCredential = await EmailAuthProvider.credential(email, password)
    const reAuthCredencial = await reauthenticateWithCredential(user, userCredential)
    await reAuthCredencial.user.delete()
    return
  } catch (error) {
    switch (error.code) {
      case "auth/user-mismatch":
      case "auth/user-not-found":
      case "auth/invalid-credential":
      case "auth/wrong-password":
        // 再認証失敗時の処理
	break
      default:
        // その他の例外時の処理
    }
    return
  }
}

まとめ

Firebase Authenticationを用いてメールアドレス・パスワード認証を
サービスに導入する際に必要となる機能とそのフロー、実行する際に必要となるコードについて
紹介しました。

メールアドレス認証を用いる場合は認証状態にメールアドレス未認証というのが加わるので意識しましょう。

また、Firebaseのメールテンプレートを利用する場合は以下に注意しましょう。
以下では要件を満たせない、メールの送信タイミングなどを自分たちで管理したいなどがあれば
CloudFunctions + SendgridなどのメールPFを利用して送信するようにしましょう。

  • メールアドレス認証メールの文面の変更ができない
    • メールの文面にユーザー名が入っているため、ユーザー作成後
  • アクションURLは全テンプレートで1つしか設定できない
    • デフォルトのものはデザインが固定されているため利用することはあまりなさそう...
    • 設定したURL(エンドポイント)でどのメールの処理かを判別する必要があります

Discussion