🔑

Google Identity Platform(Firebase Authentication)でパスワードリセット画面をカスタムする

2023/07/25に公開

はじめに

この記事では以下のケースを想定しています。

  • Google Identity Platform (Firebase Authentication)でメールアドレス&パスワード認証を行う
  • マルチテナンシーを使用する
  • 作成したパスワードリセット画面でパスワードをリセットさせたい

Firebase Web SDKにはパスワードリセットに関する便利な関数が用意されているのですが、公式ドキュメントの通りに実装するとパスワードリセットメールに記載されているリンクの遷移先はFirebase Authenticationのデフォルト画面となっています。
パスワードにサービス独自のバリデーションルールを適用させたり見た目を変えたいので、自作のパスワードリセット画面でパスワードリセットが行えるように実装していきます!

今回つまづいたポイント

パスワードリセットのために必要なリセットコードは正しいはずのにエラーが返却される 
→ 公式ドキュメントのサンプルコードはマルチテナンシーが無効のケースで記述されているのが原因。マルチテナンシーが有効の場合はauthオブジェクトにテナントIDを渡す必要があります(忘れがち)。

パスワードリセットメールに記載されているURLについて

特に何も設定していない状態だと、パスワードリセットメールにはこのようなURLが記載されています。

https://firebaseのプロジェクトID.firebaseapp.com/__/auth/action?mode=resetPassword&oobCode=XXXXX&apiKey=XXXXX&lang=en&tenantId=hogehoge

URLにはいくつかのパラメータが含まれますが、今回使うのはこの4つです。

遷移先https://firebaseのプロジェクトID.firebaseapp.com/__/auth/action
mode: ユーザーのアクション。resetPassword recoverEmail verifyEmail のいずれかの値が入ります。
oobCode: リクエストを識別し、検証するためのワンタイムコード
tenantId: ユーザーが所属するテナントのID(Identity Platformで自動付与されるID)

modeからユーザーが行いたいアクションを判別したりoobCodeの検証を行うには、公式ドキュメントにあるとおり メールアクションハンドラの実装が必要です。

ということで以下の設定と実装を行います。

  1. メールアクションハンドラページの作成
  2. Firebaseコンソール画面からメールテンプレートを編集して、遷移先をメールアクションハンドラに変更
  3. パスワードリセット画面の作成

設定と実装

この記事にあるサンプルコードはVue.js(Vue3)で書いています。

メールアクションハンドラのページを作成する

CustomActionPage.vueのようなメールアクションハンドラページを作成し、moderesetPasswordだったらパスワード画面に遷移するように記述します。

CustomActionPage.vue
<template>
  <div />
</template>

<script setup lang="ts">
import { onMounted } from "vue";
import router from "@/router";
import { parseActionCodeURL } from "firebase/auth";

onMounted(async () => {
  const invalidURLMessage = "このURLは無効です";

  try {
    //URLを解析する
    const route = parseActionCodeURL(window.location.href);
    // modeの取得
    const mode = route?.operation ? route.operation : null;

    switch (mode) {
    case "PASSWORD_RESET":
      // パスワードリセット画面に遷移させる
      await router.push({
        name: "passwordReset",
	// oobCodeとtenantIdをパスワードリセット画面に渡す
        query: { code: route?.code, tenantId: route?.tenantId }
      });
      break;
    case "RECOVER_EMAIL":
      // メールアドレス変更の取り消し行いたいときのハンドラを登録
      break;
    case "VERIFY_EMAIL":
      // メールアドレスの確認を行いたいときのハンドラを登録
      break;
    default:
      alert(invalidURLMessage);
    }
  } catch (error){
    alert(invalidURLMessage);
  }
});
</script>

router/index.ts
  {
    // メールアクションハンドラページ
    path: "/customAction/",
    name: "customAction",
    component: () => import("CustomActionPage.vue"),
  },
  {
    // パスワードリセットページ
    path: "/passwordReset/",
    name: "passwordReset",
    component: () => import("PasswordReset.vue"),
    props: true,
  },

メールテンプレートを編集して、遷移先をメールアクションハンドラページに変更する

Firebaseコンソール画面のAuthenticationにあるTemplatesを編集します。
テンプレートの一番下にあるアクションURLをメールアクションハンドラページに変更します。
注意:アクションURLを変更すると他のメールテンプレートのアクションURLも適用されます

http://localhost:3000/hogehoge とかでもOKです。

パスワードリセット画面を作成する

パスワードリセット画面ではパスワードをリセットさせる前にoobCodeやテナントIDが有効か検証を行います。

passwordReset.ts
import { initializeApp } from "firebase/app";
import { getAuth, parseActionCodeURL, verifyPasswordResetCode } from "firebase/auth";

  const config = {
    apiKey: XXXXX,
    authDomain: XXXXXX,
  };
  // SDKの読み込み
  initializeApp(config);
  const auth = getAuth();
  
  // URLの解析
  const route = parseActionCodeURL(window.location.href);
  // oobCodeの取得
  const oobCode = route?.code ? route.code : null;
  // tenantIdの取得
  const tenantId = route?.tenantId ? route.tenantId : null;
  const invalidURLMessage = "このURLは無効です";
  
  // Identity Platformのマルチテナンシー対応のためにauthにtenantIdを設定する
    auth.tenantId = tenantId;
    
  // oobCodeが有効か検証する
  verifyPasswordResetCode(auth, oobCode)
    .catch((error) => {
      alert("リンクの有効期限が切れています。再度パスワードリセットを行ってください。");
    });

ほぼ公式ドキュメントの通りですが、マルチテナンシーが有効の場合は auth.tenantId = tenantId; を忘れるとoobCodeが無効と判断されるので要注意です。

あとはパスワードをリセットするコードを追記します。

passwordReset.ts
import { confirmPasswordReset } from "firebase/auth";

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  const handleResetPassword = (inputPassword: string) => {
    if (oobCode === null || tenantId === null){
      alert(invalidURLMessage)
      return;
    }
    // パスワードのリセット
    confirmPasswordReset(auth, oobCode, inputPassword)
      .then((resp) => {
        alert("パスワードのリセットが完了しました");
      }).catch((error) => {
        alert("パスワードのリセットに失敗しました。");
      });
  };

これで一通りの実装が完了です。
パスワードリセットメールに記載されているリンクから自作のパスワードリセット画面に遷移していればOKです。

おまけ

パスワードリセットに成功したら特定のページに遷移させたい場合

パスワードリセットURLのパラメータにcontinueUrlを追加しておくと、パスワードリセットに成功したあと任意のページにユーザーを誘導してあげることが可能です。
公式ドキュメントの例だとパスワードリセット前に操作していた画面に戻してあげたい場合などが挙げられていました。vue-routerだと対応が難しいケースに対してとても有効です。

sendPasswordResetMail.ts
import { sendPasswordResetEmail } from "firebase/auth";

  const resetEmailPassword = async (emailFormData?: {
    email: string;
    password: string;
  }) => {
    // パスワードリセット成功後の遷移先URL
    const actionCodeSettings = {
      url: "https://hogehoge/success" ,
      handleCodeInApp: true,
    };

    // パスワードリセットメールの送信
    sendPasswordResetEmail(auth, userEmailAddress, actionCodeSettings)
      .then(() => {
        if (emailFormData) {
          alert("入力したメールアドレスにパスワード再設定用リンクを送りました。");
        } else {
          alert("メールアドレスにパスワード再設定用リンクを送りました。");
        }
      })
      .catch((error) => {
        alert("パスワード再設定用リンク送信に失敗しました。");
      });
  };

パスワードリセットに成功した場合、continueUrlに遷移させるように記述

passwordReset.ts
  const handleResetPassword = (inputPassword: string) => {
    if (oobCode === null || tenantId === null){
      alert(invalidURLMessage)
      return;
    }

    // パスワードのリセット
    confirmPasswordReset(auth, oobCode, inputPassword)
      .then((resp) => {
        alert("パスワードのリセットが完了しました");
        if (continueUrl !== null){
	  // continueUrlに遷移
          window.location.href = continueUrl;
        } 
      }).catch((error) => {
        alert("パスワードのリセットに失敗しました。");
      });
  };

注意:continueUrlで指定するURLのドメインはFirebaseコンソール画面のAuthentication > Settings > 認証済みドメインに追加されている必要があります。

パスワードリセットメールに記載されるアプリケーション名の変更

Firebaseコンソール画面のプロジェクトの概要の横にある歯車マークからプロジェクトの設定をクリックし、公開名 を変更すればOKです。

公式ドキュメントのサンプルコードどおりに実装しているのにパスワードがリセットできない場合

マルチテナンシーが有効になっているなら、authオブジェクトにテナントIDを渡し忘れている可能性が高いです。フロントエンド側(Client SDK)もバックエンド側(Identity Platform Admin SDK)もauthオブジェクトにテナントIDを渡してあげる必要があります。
Identity Platformの実装でうまくいかないときは大体これが原因。

レスキューナウテックブログ

Discussion