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

このドキュメントでは、Android アプリに SMS 多要素認証を追加する方法について説明します。

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

始める前に

  1. 多要素認証をサポートするプロバイダを有効にします。

    • メールとパスワード
    • リンクをメールで送信
    • Google
    • Google Play
    • Facebook
    • Twitter
    • GitHub
    • Microsoft
    • Yahoo!
    • LinkedIn
  2. アプリがユーザーのメールアドレスを検証していることを確認します。MFA では、メールの確認を行う必要があります。これにより、悪意のある攻撃者が所有していないメールアドレスでサービスを登録し、次に第 2 要素を追加することで実際の所有者をロックアウトすることを防ぎます。

  3. Firebase コンソールでアプリの SHA-1 ハッシュを登録します(変更内容は Google Cloud Identity Platform に自動的に引き継がれます)。

    1. クライアントの認証の手順に従って、アプリの SHA-1 ハッシュを取得します。

    2. Firebase コンソールを開きます。

    3. [Project Settings] に移動します。

    4. [Your apps] で Android アイコンをクリックします。

    5. 手順に従って SHA-1 ハッシュを追加します。

多要素認証の有効化

  1. Cloud Console で [Identity Platform MFA] ページに移動します。
    [MFA] ページに移動

  2. [SMS ベースの多要素認証] というボックスで [有効にする] をクリックします。

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

  4. アプリのドメインをまだ承認していない場合は、右側の [ドメインの追加] をクリックして許可リストに追加します。

  5. [保存] をクリックします。

登録パターンの選択

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

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

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

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

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

第 2 要素の登録

ユーザーの新しい第 2 要素を登録するには、次のようにします。

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

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

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

    user.getMultiFactor().getSession()
       .addOnCompleteListener(
          new OnCompleteListener<MultiFactorSession>() {
           @Override
           public void onComplete(@NonNull Task<MultiFactorSession> task) {
             if (task.isSuccessful()) {
              MultiFactorSession multiFactorSession = task.getResult();
             }
           }
          });
    
  4. 検証プロセスのさまざまなイベントを処理する OnVerificationStateChangedCallbacks オブジェクトを構築します。

    OnVerificationStateChangedCallbacks callbacks =
     new OnVerificationStateChangedCallbacks() {
      @Override
      public void onVerificationCompleted(PhoneAuthCredential credential) {
        // This callback will be invoked in two situations:
        // 1) Instant verification. In some cases, the phone number can be
        //    instantly verified without needing to send or enter a verification
        //    code. You can disable this feature by calling
        //    PhoneAuthOptions.builder#requireSmsValidation(true) when building
        //    the options to pass to PhoneAuthProvider#verifyPhoneNumber().
        // 2) Auto-retrieval. On some devices, Google Play services can
        //    automatically detect the incoming verification SMS and perform
        //    verification without user action.
        this.credential = credential;
      }
      @Override
      public void onVerificationFailed(FirebaseException e) {
        // This callback is invoked in response to invalid requests for
        // verification, like an incorrect phone number.
        if (e instanceof FirebaseAuthInvalidCredentialsException) {
         // Invalid request
         // ...
        } else if (e instanceof FirebaseTooManyRequestsException) {
         // The SMS quota for the project has been exceeded
         // ...
        }
        // Show a message and update the UI
        // ...
      }
      @Override
      public void onCodeSent(
         String verificationId, PhoneAuthProvider.ForceResendingToken token) {
        // The SMS verification code has been sent to the provided phone number.
        // We now need to ask the user to enter the code and then construct a
        // credential by combining the code with a verification ID.
        // Save the verification ID and resending token for later use.
        this.verificationId = verificationId;
        this.forceResendingToken = token;
        // ...
      }
    };
    
  5. ユーザーの電話番号、多要素セッション、コールバックを使用して PhoneInfoOptions オブジェクトを初期化します。

    PhoneAuthOptions phoneAuthOptions =
       PhoneAuthOptions.newBuilder()
          .setPhoneNumber(phoneNumber)
          .setTimeout(30L, TimeUnit.SECONDS)
          .setMultiFactorSession(MultiFactorSession)
          .setCallbacks(callbacks)
          .build();
    

    デフォルトでは、即時確認が有効になっています。無効にするには、requireSmsValidation(true) の呼び出しを追加します。

  6. ユーザーの電話に確認メッセージを送信します。

    PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions);
    

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

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

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

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

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    FirebaseAuth.getInstance()
       .getCurrentUser()
       .getMultiFactor()
       .enroll(multiFactorAssertion, "My personal phone number")
       .addOnCompleteListener(
          new OnCompleteListener<Void>() {
           @Override
           public void onComplete(@NonNull Task<Void> task) {
             // ...
           }
          });
    

第 2 要素を登録する完全な例を、次のコードに示します。

MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
user.getMultiFactor().getSession()
   .addOnCompleteListener(
      new OnCompleteListener<MultiFactorSession>() {
       @Override
       public void onComplete(@NonNull Task<MultiFactorSession> task) {
         if (task.isSuccessful()) {
          MultiFactorSession multiFactorSession = task.getResult();
          PhoneAuthOptions phoneAuthOptions =
             PhoneAuthOptions.newBuilder()
                .setPhoneNumber(phoneNumber)
                .setTimeout(30L, TimeUnit.SECONDS)
                .setMultiFactorSession(multiFactorSession)
                .setCallbacks(callbacks)
                .build();
          // Send SMS verification code.
          PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions);
         }
       }
      });

// Ask user for the verification code.
PhoneAuthCredential credential =
   PhoneAuthProvider.getCredential(verificationId, verificationCode);

MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
// Complete enrollment.
FirebaseAuth.getInstance()
   .getCurrentUser()
   .getMultiFactor()
   .enroll(multiFactorAssertion, "My personal phone number")
   .addOnCompleteListener(
      new OnCompleteListener<Void>() {
       @Override
       public void onComplete(@NonNull Task<Void> task) {
         // ...
       }
      });

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

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

2 要素認証プロセスを使用してユーザーのログインを行うには、次のようにします。

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

    たとえば、ユーザーの第 1 要素がメールアドレスとパスワードの場合は、次のように記述します。

    FirebaseAuth.getInstance()
       .signInWithEmailAndPassword(email, password)
       .addOnCompleteListener(
          new OnCompleteListener<AuthResult>() {
           @Override
           public void onComplete(@NonNull Task<AuthResult> task) {
             if (task.isSuccessful()) {
              // User is not enrolled with a second factor and is successfully
              // signed in.
              // ...
              return;
             }
             if (task.getException() instanceof FirebaseAuthMultiFactorException) {
              // The user is a multi-factor user. Second factor challenge is
              // required.
              MultiFactorResolver multiFactorResolver = task.getException().getResolver();
              // ...
             } else {
              // Handle other errors such as wrong password.
             }
           }
          });
    

    ユーザーの第 1 要素が OAuth などのフェデレーション プロバイダの場合は、startActivityForSignInWithProvider() を呼び出してエラーをキャッチします。

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

    // Ask user which second factor to use.
    // You can get the masked phone number using
    // resolver.getHints().get(selectedIndex).getPhoneNumber()
    // You can get the display name using
    // resolver.getHints().get(selectedIndex).getDisplayName()
    if (resolver.getHints().get(selectedIndex).getFactorId()
       == PhoneMultiFactorGenerator.FACTOR_ID) {
     // User selected a phone second factor.
     MultiFactorInfo selectedHint =
       multiFactorResolver.getHints().get(selectedIndex);
    } else {
     // Unsupported second factor.
     // Note that only phone second factors are currently supported.
    }
    
  3. ヒントと多要素セッションを使用して PhoneAuthOptions オブジェクトを初期化します。これらの値は、FirebaseAuthMultiFactorException に接続されているリゾルバに含まれます。

    PhoneAuthOptions phoneAuthOptions =
       PhoneAuthOptions.newBuilder()
          .setMultiFactorHint(selectedHint)
          .setTimeout(30L, TimeUnit.SECONDS)
          .setMultiFactorSession(multiFactorResolver.getSession())
          .setCallbacks(callbacks)
          // Optionally disable instant verification.
          // .requireSmsValidation(true)
          .build();
    
  4. ユーザーの電話に確認メッセージを送信します。

    // Send SMS verification code
    PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions);
    
  5. SMS コードを送信したら、コードの確認をユーザーに依頼します。

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

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

    multiFactorResolver
       .resolveSignIn(multiFactorAssertion)
       .addOnCompleteListener(
          new OnCompleteListener<AuthResult>() {
           @Override
           public void onComplete(@NonNull Task<AuthResult> task) {
             if (task.isSuccessful()) {
              AuthResult authResult = task.getResult();
              // AuthResult will also contain the user, additionalUserInfo,
              // and an 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, authResult.getAdditionalUserInfo() will contain data
              // related to Google provider that the user signed in with.
              // authResult.getCredential() will contain the Google OAuth
              // credential.
              // authResult.getCredential().getAccessToken() will contain the
              // Google OAuth access token.
              // authResult.getCredential().getIdToken() contains the Google
              // OAuth ID token.
             }
           }
          });
    

多要素ユーザーのログインの完全な例を、次のコードに示します。

FirebaseAuth.getInstance()
   .signInWithEmailAndPassword(email, password)
   .addOnCompleteListener(
      new OnCompleteListener<AuthResult>() {
       @Override
       public void onComplete(@NonNull Task<AuthResult> task) {
         if (task.isSuccessful()) {
          // User is not enrolled with a second factor and is successfully
          // signed in.
          // ...
          return;
         }
         if (task.getException() instanceof FirebaseAuthMultiFactorException) {
          FirebaseAuthMultiFactorException e =
            (FirebaseAuthMultiFactorException) task.getException();

          MultiFactorResolver multiFactorResolver = e.getResolver();

          // Ask user which second factor to use.
          MultiFactorInfo selectedHint =
             multiFactorResolver.getHints().get(selectedIndex);

          // Send the SMS verification code.
          PhoneAuthProvider.verifyPhoneNumber(
             PhoneAuthOptions.newBuilder()
                .setActivity(this)
                .setMultiFactorSession(multiFactorResolver.getSession())
                .setMultiFactorHint(selectedtHint)
                .setCallbacks(generateCallbacks())
                .setTimeout(30L, TimeUnit.SECONDS)
                .build());

          // Ask user for the SMS verification code.
          PhoneAuthCredential credential =
             PhoneAuthProvider.getCredential(verificationId, verificationCode);

          // Initialize a MultiFactorAssertion object with the
          // PhoneAuthCredential.
          MultiFactorAssertion multiFactorAssertion =
             PhoneMultiFactorGenerator.getAssertion(credential);

          // Complete sign-in.
          multiFactorResolver
             .resolveSignIn(multiFactorAssertion)
             .addOnCompleteListener(
                new OnCompleteListener<AuthResult>() {
                  @Override
                  public void onComplete(@NonNull Task<AuthResult> task) {
                   if (task.isSuccessful()) {
                     // User successfully signed in with the
                     // second factor phone number.
                   }
                   // ...
                  }
                });
         } else {
          // Handle other errors such as wrong password.
         }
       }
      });

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

次のステップ