Android 앱에 다단계 인증 추가

이 문서에서는 Android 앱에 SMS 다단계 인증을 추가하는 방법을 설명합니다.

다단계 인증을 통해 앱의 보안이 강화됩니다. 공격자는 종종 비밀번호와 소셜 미디어 계정을 유출시키지만 문자 메시지를 가로채는 것은 더 어렵습니다.

시작하기 전에

  1. 다단계 인증을 지원하는 공급업체를 사용 설정합니다. 예를 들면 다음과 같습니다.

    • 이메일 및 비밀번호
    • 이메일 링크
    • Google
    • Google Play
    • Facebook
    • Twitter
    • GitHub
    • Microsoft
    • Yahoo
    • LinkedIn
  2. 앱이 사용자 이메일을 확인하고 있는지 확인합니다. MFA를 사용하려면 이메일 확인이 필요합니다. 이를 통해 악의적인 행위자가 자신이 소유하지 않은 이메일에 서비스를 등록한 후 두 번째 단계를 추가하여 실제 소유자의 접근을 막는 일을 방지할 수 있습니다.

  3. Firebase Console에 앱의 SHA-1 해시를 등록합니다(변경사항은 자동으로 Google Cloud Identity Platform에 적용됩니다).

    1. 클라이언트 인증의 단계에 따라 앱의 SHA-1 해시를 가져옵니다.

    2. Firebase Console을 엽니다.

    3. 프로젝트 설정으로 이동합니다.

    4. 내 앱에서 Android 아이콘을 클릭합니다.

    5. 안내에 따라 SHA-1 해시를 추가합니다.

다단계 인증 사용 설정

  1. Cloud Console에서 Identity Platform MFA 페이지로 이동합니다.
    MFA 페이지로 이동

  2. SMS 기반 다단계 인증 상자에서 사용 설정을 클릭합니다.

  3. 앱을 테스트할 전화번호를 입력합니다. 선택사항이지만 개발 중 제한이 발생하지 않도록 테스트 전화번호를 등록하는 것이 좋습니다.

  4. 아직 앱 도메인을 승인하지 않은 경우 오른쪽의 도메인 추가를 클릭하여 허용 목록에 도메인을 추가합니다.

  5. 저장을 클릭합니다.

등록 패턴 선택

앱에 다단계 인증이 필요한지 여부 및 사용자 등록 방법과 시기를 선택할 수 있습니다. 일반적인 패턴은 다음과 같습니다.

  • 등록 시 사용자의 두 번째 단계를 등록합니다. 앱이 모든 사용자에게 다단계 인증을 요구한다면 이 방법을 사용하세요.

  • 등록 시 두 번째 단계를 등록하는 건너뛸 수 있는 옵션을 제공하세요. 다단계 인증을 권고하지만 필수이지는 않은 앱은 이 방법을 사용하는 것이 좋습니다.

  • 가입 화면이 아닌 사용자의 계정 또는 프로필 관리 페이지에서 두 번째 단계를 추가할 수 있도록 합니다. 이렇게 하면 등록 프로세스 중에 발생하는 마찰을 최소화하면서도 보안에 민감한 사용자에게 다단계 인증을 제공할 수 있습니다.

  • 사용자가 보안 요구사항이 향상된 기능에 액세스하려고 할 때 두 번째 단계를 점진적으로 추가하도록 합니다.

두 번째 단계 등록

사용자의 새로운 두 번째 단계를 등록하려면 다음 단계를 따르세요.

  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. PhoneAuthCredentialMultiFactorAssertion 객체를 초기화합니다.

    MultiFactorAssertion multiFactorAssertion
       = PhoneMultiFactorGenerator.getAssertion(credential);
    
  9. 등록을 완료합니다. 선택사항으로 두 번째 단계의 표시 이름을 지정할 수 있습니다. 인증 과정에서 전화번호가 마스킹되므로(예: +1******1234) 두 번째 단계가 여러 개인 사용자에게 유용합니다.

    // 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) {
             // ...
           }
          });
    

다음 코드는 두 번째 단계를 등록하는 전체 예시를 보여줍니다.

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단계 SMS 인증으로 사용자를 로그인 처리하려면 다음 안내를 따르세요.

  1. 첫 번째 단계로 사용자를 로그인한 다음 FirebaseAuthMultiFactorException 예외를 포착합니다. 이 오류에는 사용자의 등록된 두 번째 단계를 가져오는 데 사용할 수 있는 리졸버가 포함됩니다. 첫 번째 단계로 사용자를 성공적으로 인증했음을 나타내는 기본 세션도 포함됩니다.

    예를 들어 사용자의 첫 번째 단계가 이메일과 비밀번호인 경우는 다음과 같습니다.

    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.
             }
           }
          });
    

    사용자의 첫 번째 단계가 OAuth와 같은 제휴 공급업체인 경우 startActivityForSignInWithProvider() 호출 후 오류를 포착합니다.

  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. PhoneAuthCredentialMultiFactorAssertion 객체를 초기화합니다.

    MultiFactorAssertion multiFactorAssertion
        = PhoneMultiFactorGenerator.getAssertion(credential);
    
  7. resolver.resolveSignIn()을 호출하여 2차 인증을 완료합니다. 그런 다음 표준 공급업체별 데이터 및 인증 사용자 인증 정보가 포함된 원래 로그인 결과에 액세스할 수 있습니다.

    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.
         }
       }
      });

수고하셨습니다. 다단계 인증을 사용하여 사용자가 성공적으로 로그인했습니다.

다음 단계