ウェブアプリに多要素認証を追加する

Identity Platform を使用する Firebase Authentication にアップグレードした場合は、ウェブアプリに SMS 多要素認証を追加できます。

多要素認証では、アプリのセキュリティが強化されます。攻撃者はパスワードとソーシャル アカウントを不正使用することがよくありますが、テキスト メッセージを傍受するのは、はるかに困難です。

始める前に

  1. 多要素認証をサポートするプロバイダを少なくとも 1 つ有効にします。すべてのプロバイダが、電話認証、匿名認証、Apple Game Center を除く MFA をサポートしています。

  2. アプリでユーザーのメールアドレスが検証されていることを確認します。MFA では、メールの確認を行う必要があります。これにより、悪意のある攻撃者が所有していないメールアドレスでサービスを登録してから第 2 要素を追加することで、実際の所有者をロックアウトすることを防ぎます。

マルチテナンシーの使用

マルチテナント環境で多要素認証を有効にする場合は、必ず以下の手順に沿ってください(このドキュメントの残りの説明をご覧ください)。

  1. Google Cloud コンソールで、作業するテナントを選択します。

  2. コードで、Auth インスタンスの tenantId フィールドをテナントの ID に設定します。次に例を示します。

    ウェブ向けのモジュラー API

    import { getAuth } from "firebase/auth";
    
    const auth = getAuth(app);
    auth.tenantId = "myTenantId1";
    

    ウェブ向けの名前空間付き API

    firebase.auth().tenantId = 'myTenantId1';
    

多要素認証の有効化

  1. Firebase コンソールの [Authentication] > [Sign-in method] ページを開きます。

  2. [詳細] セクションで [SMS 多要素認証] を有効にします。

    また、アプリのテストに使用する電話番号も入力する必要があります。必要に応じて、開発中のスロットリングを回避するためにテスト用の電話番号を登録することを強くおすすめします。

  3. アプリのドメインをまだ承認していない場合は、Firebase コンソールの [Authentication] > [Settings] ページで許可リストに追加します。

登録パターンの選択

アプリで多要素認証が必要かどうかと、ユーザーを登録する方法とタイミングを選択できます。一般的なパターンには、次のようなものがあります。

  • 登録の一部として、ユーザーの第 2 要素を登録する。アプリがすべてのユーザーに対して多要素認証を必要とする場合は、この方法を使用します。

  • 登録中に第 2 要素の登録をスキップできる選択肢を用意する。多要素認証を必須とはしないが、推奨したいと考えるアプリでは、この方法が望ましい場合があります。

  • 登録画面ではなく、ユーザーのアカウントまたはプロフィールの管理ページから第 2 要素を追加する機能を用意する。これにより、登録プロセス中の摩擦が最小限に抑えられる一方、セキュリティに敏感なユーザーは多要素認証を利用できるようになります。

  • セキュリティ要件が強化された機能にユーザーがアクセスする際には、第 2 要素を段階的に追加することを要求する。

reCAPTCHA 検証ツールの設定

SMS コードを送信する前に、reCAPTCHA 検証ツールを構成する必要があります。Firebase では、電話番号の確認リクエストがアプリで許可されたドメインのいずれかから発信されたものであることを確認して、不正行為を防止するために reCAPTCHA が使用されます。

reCAPTCHA クライアントを手動で設定する必要はありません。クライアント SDK の RecaptchaVerifier オブジェクトを使用すると、必要なクライアント キーとシークレットが自動的に作成され、初期化されます。

invisible reCAPTCHA を使用する

RecaptchaVerifier オブジェクトは invisible reCAPTCHA をサポートしており、ユーザーに操作を要求せずにユーザーの確認ができます。invisible reCAPTCHA を使用するには、size パラメータを invisible に設定して RecaptchaVerifier を作成し、多要素の登録を開始する UI 要素の ID を指定します。

ウェブ向けのモジュラー API

import { RecaptchaVerifier } from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier("sign-in-button", {
    "size": "invisible",
    "callback": function(response) {
        // reCAPTCHA solved, you can proceed with
        // phoneAuthProvider.verifyPhoneNumber(...).
        onSolvedRecaptcha();
    }
}, auth);

ウェブ向けの名前空間付き API

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('sign-in-button', {
'size': 'invisible',
'callback': function(response) {
  // reCAPTCHA solved, you can proceed with phoneAuthProvider.verifyPhoneNumber(...).
  onSolvedRecaptcha();
}
});

reCAPTCHA ウィジェットを使用する

可視の reCAPTCHA ウィジェットを使用するには、ウィジェットを含む HTML 要素を作成してから、UI コンテナの ID で RecaptchaVerifier オブジェクトを作成します。reCAPTCHA が解決済みまたは期限切れになったときに呼び出されるコールバックを設定することもできます。

ウェブ向けのモジュラー API

import { RecaptchaVerifier } from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier(
    "recaptcha-container",

    // Optional reCAPTCHA parameters.
    {
      "size": "normal",
      "callback": function(response) {
        // reCAPTCHA solved, you can proceed with
        // phoneAuthProvider.verifyPhoneNumber(...).
        onSolvedRecaptcha();
      },
      "expired-callback": function() {
        // Response expired. Ask user to solve reCAPTCHA again.
        // ...
      }
    }, auth
);

ウェブ向けの名前空間付き API

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier(
  'recaptcha-container',
  // Optional reCAPTCHA parameters.
  {
    'size': 'normal',
    'callback': function(response) {
      // reCAPTCHA solved, you can proceed with phoneAuthProvider.verifyPhoneNumber(...).
      // ...
      onSolvedRecaptcha();
    },
    'expired-callback': function() {
      // Response expired. Ask user to solve reCAPTCHA again.
      // ...
    }
  });

reCAPTCHA の事前レンダリング

必要に応じて、2 つの要素の登録を開始する前に reCAPTCHA を事前レンダリングできます。

ウェブ向けのモジュラー API

recaptchaVerifier.render()
    .then(function (widgetId) {
        window.recaptchaWidgetId = widgetId;
    });

ウェブ向けの名前空間付き API

recaptchaVerifier.render()
  .then(function(widgetId) {
    window.recaptchaWidgetId = widgetId;
  });

render() が解決された後、reCAPTCHA のウィジェット ID を取得します。これを使用して reCAPTCHA API を呼び出すことができます。

var recaptchaResponse = grecaptcha.getResponse(window.recaptchaWidgetId);

RecaptchaVerifier が verify メソッドでこのロジックを抽象化するため、grecaptcha 変数を直接処理する必要はありません。

第 2 要素の登録

ユーザーの新しい第 2 要素を登録するには:

  1. ユーザーを再認証します。

  2. ユーザーに電話番号の入力を依頼します。

  3. 前のセクションで説明したように、reCAPTCHA 検証ツールを初期化します。RecaptchaVerifier インスタンスがすでに構成されている場合は、この手順を省略します。

    ウェブ向けのモジュラー API

    import { RecaptchaVerifier } from "firebase/auth";
    
    const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
    

    ウェブ向けの名前空間付き API

    var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
    
  4. ユーザーの多要素セッションを取得します。

    ウェブ向けのモジュラー API

    import { multiFactor } from "firebase/auth";
    
    multiFactor(user).getSession().then(function (multiFactorSession) {
        // ...
    });
    

    ウェブ向けの名前空間付き API

    user.multiFactor.getSession().then(function(multiFactorSession) {
      // ...
    })
    
  5. ユーザーの電話番号と多要素セッションで PhoneInfoOptions オブジェクトを初期化します。

    ウェブ向けのモジュラー API

    // Specify the phone number and pass the MFA session.
    const phoneInfoOptions = {
      phoneNumber: phoneNumber,
      session: multiFactorSession
    };
    

    ウェブ向けの名前空間付き API

    // Specify the phone number and pass the MFA session.
    var phoneInfoOptions = {
      phoneNumber: phoneNumber,
      session: multiFactorSession
    };
    
  6. ユーザーの電話に確認メッセージを送信します。

    ウェブ向けのモジュラー API

    import { PhoneAuthProvider } from "firebase/auth";
    
    const phoneAuthProvider = new PhoneAuthProvider(auth);
    phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
        .then(function (verificationId) {
            // verificationId will be needed to complete enrollment.
        });
    

    ウェブ向けの名前空間付き API

    var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
    // Send SMS verification code.
    return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
      .then(function(verificationId) {
        // verificationId will be needed for enrollment completion.
      })
    

    必須ではありませんが、SMS メッセージを受信することと、その標準料金が適用されることを、あらかじめユーザーに知らせることをおすすめします。

  7. リクエストが失敗した場合は、reCAPTCHA をリセットしてから前の手順を繰り返すと、ユーザーで再試行できます。reCAPTCHA トークンの使用は 1 回限りなので、verifyPhoneNumber() がエラーをスローすると、reCAPTCHA は自動的にリセットされます。

    ウェブ向けのモジュラー API

    recaptchaVerifier.clear();
    

    ウェブ向けの名前空間付き API

    recaptchaVerifier.clear();
    
  8. SMS コードを送信したら、コードの確認をユーザーに依頼します。

    ウェブ向けのモジュラー API

    // Ask user for the verification code. Then:
    const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
    

    ウェブ向けの名前空間付き API

    // Ask user for the verification code. Then:
    var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
    
  9. PhoneAuthCredential を使用して MultiFactorAssertion オブジェクトを初期化します。

    ウェブ向けのモジュラー API

    import { PhoneMultiFactorGenerator } from "firebase/auth";
    
    const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
    

    ウェブ向けの名前空間付き API

    var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
    
  10. 登録を完了します。必要に応じて、第 2 要素の表示名を指定することもできます。これは、認証フローで電話番号がマスクされるため(たとえば、+1******1234 など)、複数の第 2 要素があるユーザーには便利です。

    ウェブ向けのモジュラー API

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    multiFactor(user).enroll(multiFactorAssertion, "My personal phone number");
    

    ウェブ向けの名前空間付き API

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    user.multiFactor.enroll(multiFactorAssertion, 'My personal phone number');
    

次のコードは、第 2 要素を登録するための完全な例を示しています。

ウェブ向けのモジュラー API

import {
    multiFactor, PhoneAuthProvider, PhoneMultiFactorGenerator,
    RecaptchaVerifier
} from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
multiFactor(user).getSession()
    .then(function (multiFactorSession) {
        // Specify the phone number and pass the MFA session.
        const phoneInfoOptions = {
            phoneNumber: phoneNumber,
            session: multiFactorSession
        };

        const phoneAuthProvider = new PhoneAuthProvider(auth);

        // Send SMS verification code.
        return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier);
    }).then(function (verificationId) {
        // Ask user for the verification code. Then:
        const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
        const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);

        // Complete enrollment.
        return multiFactor(user).enroll(multiFactorAssertion, mfaDisplayName);
    });

ウェブ向けの名前空間付き API

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
user.multiFactor.getSession().then(function(multiFactorSession) {
  // Specify the phone number and pass the MFA session.
  var phoneInfoOptions = {
    phoneNumber: phoneNumber,
    session: multiFactorSession
  };
  var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
  // Send SMS verification code.
  return phoneAuthProvider.verifyPhoneNumber(
      phoneInfoOptions, recaptchaVerifier);
})
.then(function(verificationId) {
  // Ask user for the verification code.
  var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
  var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
  // Complete enrollment.
  return user.multiFactor.enroll(multiFactorAssertion, mfaDisplayName);
});

これで完了です。ユーザーの第 2 の認証要素が正常に登録されました。

第 2 要素でのユーザーのログイン

2 つの要素の SMS 確認を使用してユーザーのログインを行うには:

  1. 最初の要素でユーザーのログインを行った後、auth/multi-factor-auth-required エラーを検知します。このエラーには、リゾルバ、登録された第 2 要素に関するヒント、および最初の要素でユーザーが正常に認証された基礎となるセッションが含まれます。

    たとえば、ユーザーの第 1 要素がメールアドレスとパスワードの場合:

    ウェブ向けのモジュラー API

    import { getAuth, getMultiFactorResolver} from "firebase/auth";
    
    const auth = getAuth();
    signInWithEmailAndPassword(auth, email, password)
        .then(function (userCredential) {
            // User successfully signed in and is not enrolled with a second factor.
        })
        .catch(function (error) {
            if (error.code == 'auth/multi-factor-auth-required') {
                // The user is a multi-factor user. Second factor challenge is required.
                resolver = getMultiFactorResolver(auth, error);
                // ...
            } else if (error.code == 'auth/wrong-password') {
                // Handle other errors such as wrong password.
            }
    });
    

    ウェブ向けの名前空間付き API

    firebase.auth().signInWithEmailAndPassword(email, password)
      .then(function(userCredential) {
        // User successfully signed in and is not enrolled with a second factor.
      })
      .catch(function(error) {
        if (error.code == 'auth/multi-factor-auth-required') {
          // The user is a multi-factor user. Second factor challenge is required.
          resolver = error.resolver;
          // ...
        } else if (error.code == 'auth/wrong-password') {
          // Handle other errors such as wrong password.
        } ...
      });
    

    ユーザーの第 1 要素が OAuth、SAML、OIDC などの連携プロバイダである場合は、signInWithPopup() または signInWithRedirect() を呼び出した後にエラーを検知します。

  2. ユーザーに複数の登録された第 2 要素がある場合は、どの要素を使用するかをユーザーに確認します。

    ウェブ向けのモジュラー API

    // Ask user which second factor to use.
    // You can get the masked phone number via resolver.hints[selectedIndex].phoneNumber
    // You can get the display name via resolver.hints[selectedIndex].displayName
    
    if (resolver.hints[selectedIndex].factorId ===
        PhoneMultiFactorGenerator.FACTOR_ID) {
        // User selected a phone second factor.
        // ...
    } else if (resolver.hints[selectedIndex].factorId ===
               TotpMultiFactorGenerator.FACTOR_ID) {
        // User selected a TOTP second factor.
        // ...
    } else {
        // Unsupported second factor.
    }
    

    ウェブ向けの名前空間付き API

    // Ask user which second factor to use.
    // You can get the masked phone number via resolver.hints[selectedIndex].phoneNumber
    // You can get the display name via resolver.hints[selectedIndex].displayName
    if (resolver.hints[selectedIndex].factorId === firebase.auth.PhoneMultiFactorGenerator.FACTOR_ID) {
      // User selected a phone second factor.
      // ...
    } else if (resolver.hints[selectedIndex].factorId === firebase.auth.TotpMultiFactorGenerator.FACTOR_ID) {
      // User selected a TOTP second factor.
      // ...
    } else {
      // Unsupported second factor.
    }
    
  3. 前のセクションで説明したように、reCAPTCHA 検証ツールを初期化します。RecaptchaVerifier インスタンスがすでに構成されている場合は、この手順を省略します。

    ウェブ向けのモジュラー API

    import { RecaptchaVerifier } from "firebase/auth";
    
    recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
    

    ウェブ向けの名前空間付き API

    var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
    
  4. ユーザーの電話番号と多要素セッションで PhoneInfoOptions オブジェクトを初期化します。これらの値は、auth/multi-factor-auth-required エラーに渡される resolver オブジェクトに含まれています。

    ウェブ向けのモジュラー API

    const phoneInfoOptions = {
        multiFactorHint: resolver.hints[selectedIndex],
        session: resolver.session
    };
    

    ウェブ向けの名前空間付き API

    var phoneInfoOptions = {
      multiFactorHint: resolver.hints[selectedIndex],
      session: resolver.session
    };
    
  5. ユーザーの電話に確認メッセージを送信します。

    ウェブ向けのモジュラー API

    // Send SMS verification code.
    const phoneAuthProvider = new PhoneAuthProvider(auth);
    phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
        .then(function (verificationId) {
            // verificationId will be needed for sign-in completion.
        });
    

    ウェブ向けの名前空間付き API

    var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
    // Send SMS verification code.
    return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
      .then(function(verificationId) {
        // verificationId will be needed for sign-in completion.
      })
    
  6. リクエストが失敗した場合は、reCAPTCHA をリセットしてから前の手順を繰り返すと、ユーザーで再試行できます。

    ウェブ向けのモジュラー API

    recaptchaVerifier.clear();
    

    ウェブ向けの名前空間付き API

    recaptchaVerifier.clear();
    
  7. SMS コードを送信したら、コードの確認をユーザーに依頼します。

    ウェブ向けのモジュラー API

    const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
    

    ウェブ向けの名前空間付き API

    // Ask user for the verification code. Then:
    var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
    
  8. PhoneAuthCredential を使用して MultiFactorAssertion オブジェクトを初期化します。

    ウェブ向けのモジュラー API

    const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
    

    ウェブ向けの名前空間付き API

    var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
    
  9. 第 2 要素の認証を完了するには、resolver.resolveSignIn() を呼び出します。その後、元のログイン結果にアクセスできます。これには、標準のプロバイダ固有のデータと認証情報が含まれます。

    ウェブ向けのモジュラー API

    // Complete sign-in. This will also trigger the Auth state listeners.
    resolver.resolveSignIn(multiFactorAssertion)
        .then(function (userCredential) {
            // userCredential will also contain the user, additionalUserInfo, optional
            // credential (null for email/password) associated with the first factor sign-in.
    
            // For example, if the user signed in with Google as a first factor,
            // userCredential.additionalUserInfo will contain data related to Google
            // provider that the user signed in with.
            // - user.credential contains the Google OAuth credential.
            // - user.credential.accessToken contains the Google OAuth access token.
            // - user.credential.idToken contains the Google OAuth ID token.
        });
    

    ウェブ向けの名前空間付き API

    // Complete sign-in. This will also trigger the Auth state listeners.
    resolver.resolveSignIn(multiFactorAssertion)
      .then(function(userCredential) {
        // userCredential will also contain the user, additionalUserInfo, optional
        // credential (null for email/password) associated with the first factor sign-in.
        // For example, if the user signed in with Google as a first factor,
        // userCredential.additionalUserInfo will contain data related to Google provider that
        // the user signed in with.
        // user.credential contains the Google OAuth credential.
        // user.credential.accessToken contains the Google OAuth access token.
        // user.credential.idToken contains the Google OAuth ID token.
      });
    

以下のコードは、多要素ユーザーのログインの完全な例を示しています。

ウェブ向けのモジュラー API

import {
    getAuth,
    getMultiFactorResolver,
    PhoneAuthProvider,
    PhoneMultiFactorGenerator,
    RecaptchaVerifier,
    signInWithEmailAndPassword
} from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);

const auth = getAuth();
signInWithEmailAndPassword(auth, email, password)
    .then(function (userCredential) {
        // User is not enrolled with a second factor and is successfully
        // signed in.
        // ...
    })
    .catch(function (error) {
        if (error.code == 'auth/multi-factor-auth-required') {
            const resolver = getMultiFactorResolver(auth, error);
            // Ask user which second factor to use.
            if (resolver.hints[selectedIndex].factorId ===
                PhoneMultiFactorGenerator.FACTOR_ID) {
                const phoneInfoOptions = {
                    multiFactorHint: resolver.hints[selectedIndex],
                    session: resolver.session
                };
                const phoneAuthProvider = new PhoneAuthProvider(auth);
                // Send SMS verification code
                return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
                    .then(function (verificationId) {
                        // Ask user for the SMS verification code. Then:
                        const cred = PhoneAuthProvider.credential(
                            verificationId, verificationCode);
                        const multiFactorAssertion =
                            PhoneMultiFactorGenerator.assertion(cred);
                        // Complete sign-in.
                        return resolver.resolveSignIn(multiFactorAssertion)
                    })
                    .then(function (userCredential) {
                        // User successfully signed in with the second factor phone number.
                    });
            } else if (resolver.hints[selectedIndex].factorId ===
                       TotpMultiFactorGenerator.FACTOR_ID) {
                // Handle TOTP MFA.
                // ...
            } else {
                // Unsupported second factor.
            }
        } else if (error.code == 'auth/wrong-password') {
            // Handle other errors such as wrong password.
        }
    });

ウェブ向けの名前空間付き API

var resolver;
firebase.auth().signInWithEmailAndPassword(email, password)
  .then(function(userCredential) {
    // User is not enrolled with a second factor and is successfully signed in.
    // ...
  })
  .catch(function(error) {
    if (error.code == 'auth/multi-factor-auth-required') {
      resolver = error.resolver;
      // Ask user which second factor to use.
      if (resolver.hints[selectedIndex].factorId ===
          firebase.auth.PhoneMultiFactorGenerator.FACTOR_ID) {
        var phoneInfoOptions = {
          multiFactorHint: resolver.hints[selectedIndex],
          session: resolver.session
        };
        var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
        // Send SMS verification code
        return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
          .then(function(verificationId) {
            // Ask user for the SMS verification code.
            var cred = firebase.auth.PhoneAuthProvider.credential(
                verificationId, verificationCode);
            var multiFactorAssertion =
                firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
            // Complete sign-in.
            return resolver.resolveSignIn(multiFactorAssertion)
          })
          .then(function(userCredential) {
            // User successfully signed in with the second factor phone number.
          });
      } else if (resolver.hints[selectedIndex].factorId ===
        firebase.auth.TotpMultiFactorGenerator.FACTOR_ID) {
        // Handle TOTP MFA.
        // ...
      } else {
        // Unsupported second factor.
      }
    } else if (error.code == 'auth/wrong-password') {
      // Handle other errors such as wrong password.
    } ...
  });

これで完了です。多要素認証を使用したユーザーのログインが正常に終了しました。

次のステップ