Flutter アプリに多要素認証を追加する

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

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

始める前に

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

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

  3. Android: アプリの SHA-256 ハッシュを Firebase コンソールにまだ設定していない場合は、それを行います。アプリの SHA-256 ハッシュを見つける方法についてはクライアントの認証をご覧ください。

  4. iOS: Xcode でプロジェクトのプッシュ通知を有効にし、APNs 認証キーが Firebase Cloud Messaging(FCM)で構成されていることを確認します。この手順の詳細については、Firebase iOS 電話認証のドキュメントをご覧ください。

  5. ウェブ: Firebase コンソールの [OAuth リダイレクト ドメイン] で、アプリケーション ドメインが追加されていることを確認します。

多要素認証の有効化

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

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

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

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

登録パターンの選択

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

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

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

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

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

第 2 要素の登録

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

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

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

  3. ユーザーの多要素セッションを取得します。

    final multiFactorSession = await user.multiFactor.getSession();
    
  4. 多要素セッションとコールバックで電話番号を確認します。

    await FirebaseAuth.instance.verifyPhoneNumber(
      multiFactorSession: multiFactorSession,
      phoneNumber: phoneNumber,
      verificationCompleted: (_) {},
      verificationFailed: (_) {},
      codeSent: (String verificationId, int? resendToken) async {
        // The SMS verification code has been sent to the provided phone number.
        // ...
      },
      codeAutoRetrievalTimeout: (_) {},
    ); 
    
  5. SMS コードを送信したら、コードの確認をユーザーに依頼します。

    final credential = PhoneAuthProvider.credential(
      verificationId: verificationId,
      smsCode: smsCode,
    );
    
  6. 登録を完了します。

    await user.multiFactor.enroll(
      PhoneMultiFactorGenerator.getAssertion(
        credential,
      ),
    );
    

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

  final session = await user.multiFactor.getSession();
  final auth = FirebaseAuth.instance;
  await auth.verifyPhoneNumber(
    multiFactorSession: session,
    phoneNumber: phoneController.text,
    verificationCompleted: (_) {},
    verificationFailed: (_) {},
    codeSent: (String verificationId, int? resendToken) async {
      // See `firebase_auth` example app for a method of retrieving user's sms code: 
      // https://github.com/firebase/flutterfire/blob/master/packages/firebase_auth/firebase_auth/example/lib/auth.dart#L591
      final smsCode = await getSmsCodeFromUser(context);

      if (smsCode != null) {
        // Create a PhoneAuthCredential with the code
        final credential = PhoneAuthProvider.credential(
          verificationId: verificationId,
          smsCode: smsCode,
        );

        try {
          await user.multiFactor.enroll(
            PhoneMultiFactorGenerator.getAssertion(
              credential,
            ),
          );
        } on FirebaseAuthException catch (e) {
          print(e.message);
        }
      }
    },
    codeAutoRetrievalTimeout: (_) {},
  );

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

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

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

  1. 最初の要素でユーザーのログインを行った後、FirebaseAuthMultiFactorException 例外を検知します。このエラーには、ユーザーの登録済みの第 2 要素を取得するために使用するリゾルバが含まれています。また、第 1 要素でユーザーが正常に認証されたことを示す基礎となるセッションも含まれています。

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

    try {
      await _auth.signInWithEmailAndPassword(
          email: emailController.text,
          password: passwordController.text,
      );
      // User is not enrolled with a second factor and is successfully
      // signed in.
      // ...
    } on FirebaseAuthMultiFactorException catch (e) {
      // The user is a multi-factor user. Second factor challenge is required
      final resolver = e.resolver
      // ...
    }
    
  2. ユーザーに複数の登録された第 2 要素がある場合は、どの要素を使用するかをユーザーに確認します。

    final session = e.resolver.session;
    
    final hint = e.resolver.hints[selectedHint];
    
  3. ヒントと多要素セッションを使用して、ユーザーのスマートフォンに確認メッセージを送信します。

    await FirebaseAuth.instance.verifyPhoneNumber(
      multiFactorSession: session,
      multiFactorInfo: hint,
      verificationCompleted: (_) {},
      verificationFailed: (_) {},
      codeSent: (String verificationId, int? resendToken) async {
        // ...
      },
      codeAutoRetrievalTimeout: (_) {},
    );
    
  4. 第 2 要素の認証を完了するには、resolver.resolveSignIn() を呼び出します。

    final smsCode = await getSmsCodeFromUser(context);
    if (smsCode != null) {
      // Create a PhoneAuthCredential with the code
      final credential = PhoneAuthProvider.credential(
        verificationId: verificationId,
        smsCode: smsCode,
      );
    
      try {
        await e.resolver.resolveSignIn(
          PhoneMultiFactorGenerator.getAssertion(credential)
        );
      } on FirebaseAuthException catch (e) {
        print(e.message);
      }
    }
    

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

try {
  await _auth.signInWithEmailAndPassword(
    email: emailController.text,
    password: passwordController.text,
  );
} on FirebaseAuthMultiFactorException catch (e) {
  setState(() {
    error = '${e.message}';
  });
  final firstHint = e.resolver.hints.first;
  if (firstHint is! PhoneMultiFactorInfo) {
    return;
  }
  await FirebaseAuth.instance.verifyPhoneNumber(
    multiFactorSession: e.resolver.session,
    multiFactorInfo: firstHint,
    verificationCompleted: (_) {},
    verificationFailed: (_) {},
    codeSent: (String verificationId, int? resendToken) async {
      // See `firebase_auth` example app for a method of retrieving user's sms code: 
      // https://github.com/firebase/flutterfire/blob/master/packages/firebase_auth/firebase_auth/example/lib/auth.dart#L591
      final smsCode = await getSmsCodeFromUser(context);

      if (smsCode != null) {
        // Create a PhoneAuthCredential with the code
        final credential = PhoneAuthProvider.credential(
          verificationId: verificationId,
          smsCode: smsCode,
        );

        try {
          await e.resolver.resolveSignIn(
            PhoneMultiFactorGenerator.getAssertion(
              credential,
            ),
          );
        } on FirebaseAuthException catch (e) {
          print(e.message);
        }
      }
    },
    codeAutoRetrievalTimeout: (_) {},
  );
} catch (e) {
  ...
} 

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

次のステップ