Firebase Authentication with Identity Platform으로 업그레이드했다면 Android 앱에 SMS 다중 인증(MFA)을 추가할 수 있습니다.
다중 인증(MFA)을 통해 앱의 보안이 강화됩니다. 공격자는 종종 비밀번호와 소셜 미디어 계정을 유출시키지만 문자 메시지를 가로채는 것은 더 어렵습니다.
시작하기 전에
- 다중 인증(MFA)을 지원하는 제공업체를 하나 이상 사용 설정합니다. 전화 인증, 익명 인증, Apple Game Center를 제외한 모든 제공업체는 MFA를 지원합니다. 
- 앱이 사용자 이메일을 확인하고 있는지 확인합니다. MFA를 사용하려면 이메일 인증이 필요합니다. 이를 통해 악의적인 행위자가 자신이 소유하지 않은 이메일에 서비스를 등록한 후 두 번째 단계를 추가하여 실제 소유자의 접근을 막는 일을 방지할 수 있습니다. 
- Firebase Console에 앱의 SHA-1 해시를 등록합니다(변경사항은 자동으로 Google Cloud Firebase에 적용됩니다). - 클라이언트 인증의 단계에 따라 앱의 SHA-1 해시를 가져옵니다. 
- Firebase Console을 엽니다. 
- 프로젝트 설정으로 이동합니다. 
- 내 앱에서 Android 아이콘을 클릭합니다. 
- 안내에 따라 SHA-1 해시를 추가합니다. 
 
다중 인증(MFA) 사용 설정
- Firebase Console의 인증 > 로그인 방법 페이지를 엽니다. 
- 고급 섹션에서 SMS 다중 인증(MFA)을 사용 설정합니다. - 앱을 테스트할 전화번호도 입력해야 합니다. 선택사항이지만 개발 중 제한이 발생하지 않도록 테스트 전화번호를 등록하는 것이 좋습니다. 
- 아직 앱 도메인을 승인하지 않았다면 Firebase Console의 인증 > 설정 페이지에서 허용 목록에 추가합니다. 
- 선택사항: 인증 > 설정 페이지에서 SMS 메시지 전송을 허용하거나 거부할 리전에 대한 정책을 설정합니다. SMS 리전 정책을 설정하면 SMS 악용으로부터 앱을 보호할 수 있습니다. 
등록 패턴 선택
앱에 다중 인증(MFA)이 필요한지 여부 및 사용자 등록 방법과 시기를 선택할 수 있습니다. 일반적인 패턴은 다음과 같습니다.
- 등록 시 사용자의 두 번째 단계를 등록합니다. 앱이 모든 사용자에게 다중 인증(MFA)을 요구한다면 이 방법을 사용하세요. 
- 등록 시 건너뛸 수 있는 옵션으로 두 번째 단계를 등록하는 옵션을 제공하세요. 다중 인증(MFA)을 권고하지만 필수이지는 않은 앱은 이 방법을 사용하는 것이 좋습니다. 
- 가입 화면이 아닌 사용자의 계정 또는 프로필 관리 페이지에서 두 번째 단계를 추가할 수 있도록 합니다. 이렇게 하면 등록 프로세스 중에 발생하는 마찰을 최소화하면서도 보안에 민감한 사용자에게 다중 인증(MFA)을 제공할 수 있습니다. 
- 사용자가 보안 요구사항이 향상된 기능에 액세스하려고 할 때 두 번째 단계를 점진적으로 추가하도록 합니다. 
두 번째 단계 등록
사용자의 새로운 두 번째 단계를 등록하려면 다음 단계를 따르세요.
- 사용자를 다시 인증합니다. 
- 사용자에게 전화번호를 입력하도록 요청합니다. 
- 사용자를 위한 다중 세션을 가져옵니다. - Kotlin- user.multiFactor.session.addOnCompleteListener { task -> if (task.isSuccessful) { val multiFactorSession: MultiFactorSession? = task.result } }- Java- user.getMultiFactor().getSession() .addOnCompleteListener( new OnCompleteListener<MultiFactorSession>() { @Override public void onComplete(@NonNull Task<MultiFactorSession> task) { if (task.isSuccessful()) { MultiFactorSession multiFactorSession = task.getResult(); } } });
- 인증 프로세스의 여러 이벤트를 처리하는 - OnVerificationStateChangedCallbacks객체를 빌드합니다.- Kotlin- val callbacks = object : OnVerificationStateChangedCallbacks() { override fun onVerificationCompleted(credential: PhoneAuthCredential) { // 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@MainActivity.credential = credential } override fun onVerificationFailed(e: FirebaseException) { // This callback is invoked in response to invalid requests for // verification, like an incorrect phone number. if (e is FirebaseAuthInvalidCredentialsException) { // Invalid request // ... } else if (e is FirebaseTooManyRequestsException) { // The SMS quota for the project has been exceeded // ... } // Show a message and update the UI // ... } override fun onCodeSent( verificationId: String, forceResendingToken: ForceResendingToken ) { // 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@MainActivity.verificationId = verificationId this@MainActivity.forceResendingToken = forceResendingToken // ... } }- Java- 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; // ... } };
- 사용자의 전화번호, 다중 인증 세션, 콜백으로 - PhoneInfoOptions객체를 초기화합니다.- Kotlin- val phoneAuthOptions = PhoneAuthOptions.newBuilder() .setPhoneNumber(phoneNumber) .setTimeout(30L, TimeUnit.SECONDS) .setMultiFactorSession(MultiFactorSession) .setCallbacks(callbacks) .build()- Java- PhoneAuthOptions phoneAuthOptions = PhoneAuthOptions.newBuilder() .setPhoneNumber(phoneNumber) .setTimeout(30L, TimeUnit.SECONDS) .setMultiFactorSession(multiFactorSession) .setCallbacks(callbacks) .build();- 기본적으로 즉각적인 인증이 사용 설정되어 있습니다. 사용 중지하려면 - requireSmsValidation(true)에 대한 호출을 추가합니다.
- 사용자 전화로 인증 코드 보내기 - Kotlin- PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions)- Java- PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions);- 필수 사항은 아니지만, 사용자에게 SMS 메시지가 전송되고 표준 요금이 적용된다는 점을 미리 알리는 것이 좋습니다. 
- SMS 코드가 전송되면 사용자에게 코드를 확인하도록 요청합니다. - Kotlin- // Ask user for the verification code. val credential = PhoneAuthProvider.getCredential(verificationId, verificationCode)- Java- // Ask user for the verification code. PhoneAuthCredential credential = PhoneAuthProvider.getCredential(verificationId, verificationCode);
- PhoneAuthCredential로- MultiFactorAssertion객체를 초기화합니다.- Kotlin- val multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential)- Java- MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
- 등록을 완료합니다. 선택사항으로 두 번째 단계의 표시 이름을 지정할 수 있습니다. 인증 과정에서 전화번호가 마스킹되므로(예: +1******1234) 두 번째 단계가 여러 개인 사용자에게 유용합니다. - Kotlin- // Complete enrollment. This will update the underlying tokens // and trigger ID token change listener. FirebaseAuth.getInstance() .currentUser ?.multiFactor ?.enroll(multiFactorAssertion, "My personal phone number") ?.addOnCompleteListener { // ... }- Java- // 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) { // ... } });
다음 코드는 두 번째 단계를 등록하는 전체 예시를 보여줍니다.
Kotlin
val multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential)
user.multiFactor.session
    .addOnCompleteListener { task ->
        if (task.isSuccessful) {
            val multiFactorSession = task.result
            val 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.
val credential = PhoneAuthProvider.getCredential(verificationId, verificationCode)
val multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential)
// Complete enrollment.
FirebaseAuth.getInstance()
    .currentUser
    ?.multiFactor
    ?.enroll(multiFactorAssertion, "My personal phone number")
    ?.addOnCompleteListener {
        // ...
    }
Java
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 인증으로 사용자를 로그인 처리하려면 다음 안내를 따르세요.
- 첫 번째 단계로 사용자를 로그인한 다음 - FirebaseAuthMultiFactorException예외를 포착합니다. 이 오류에는 사용자의 등록된 두 번째 단계를 가져오는 데 사용할 수 있는 리졸버가 포함됩니다. 첫 번째 단계로 사용자를 성공적으로 인증했음을 나타내는 기본 세션도 포함됩니다.- 예를 들어 사용자의 첫 번째 단계가 이메일과 비밀번호인 경우는 다음과 같습니다. - Kotlin- FirebaseAuth.getInstance() .signInWithEmailAndPassword(email, password) .addOnCompleteListener( OnCompleteListener { task -> if (task.isSuccessful) { // User is not enrolled with a second factor and is successfully // signed in. // ... return@OnCompleteListener } if (task.exception is FirebaseAuthMultiFactorException) { // The user is a multi-factor user. Second factor challenge is // required. val multiFactorResolver = (task.exception as FirebaseAuthMultiFactorException).resolver // ... } else { // Handle other errors, such as wrong password. } })- Java- 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()호출 후 오류를 포착합니다.
- 사용자가 보조 단계를 여러 개 등록한 경우 어떤 단계를 사용할지 질문합니다. - Kotlin- // Ask user which second factor to use. // You can get the list of enrolled second factors using // multiFactorResolver.hints // Check the selected factor: if (multiFactorResolver.hints[selectedIndex].factorId === PhoneMultiFactorGenerator.FACTOR_ID ) { // User selected a phone second factor. val selectedHint = multiFactorResolver.hints[selectedIndex] as PhoneMultiFactorInfo } else if (multiFactorResolver.hints[selectedIndex].factorId === TotpMultiFactorGenerator.FACTOR_ID) { // User selected a TOTP second factor. } else { // Unsupported second factor. }- Java- // 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() .equals( PhoneMultiFactorGenerator.FACTOR_ID ) ) { // User selected a phone second factor. MultiFactorInfo selectedHint = multiFactorResolver.getHints().get(selectedIndex); } else if ( resolver .getHints() .get(selectedIndex) .getFactorId() .equals(TotpMultiFactorGenerator.FACTOR_ID ) ) { // User selected a TOTP second factor. } else { // Unsupported second factor. }
- 힌트 및 다중 인증 세션을 사용하여 - PhoneAuthOptions객체를 초기화합니다. 이러한 값은- FirebaseAuthMultiFactorException에 연결된 리졸버에 포함됩니다.- Kotlin- val phoneAuthOptions = PhoneAuthOptions.newBuilder() .setMultiFactorHint(selectedHint) .setTimeout(30L, TimeUnit.SECONDS) .setMultiFactorSession(multiFactorResolver.session) .setCallbacks(callbacks) // Optionally disable instant verification. // .requireSmsValidation(true) .build()- Java- PhoneAuthOptions phoneAuthOptions = PhoneAuthOptions.newBuilder() .setMultiFactorHint(selectedHint) .setTimeout(30L, TimeUnit.SECONDS) .setMultiFactorSession(multiFactorResolver.getSession()) .setCallbacks(callbacks) // Optionally disable instant verification. // .requireSmsValidation(true) .build();
- 사용자 전화로 인증 코드 보내기 - Kotlin- // Send SMS verification code PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions)- Java- // Send SMS verification code PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions);
- SMS 코드가 전송되면 사용자에게 코드를 확인하도록 요청합니다. - Kotlin- // Ask user for the verification code. Then, pass it to getCredential: val credential = PhoneAuthProvider.getCredential(verificationId, verificationCode)- Java- // Ask user for the verification code. Then, pass it to getCredential: PhoneAuthCredential credential = PhoneAuthProvider.getCredential(verificationId, verificationCode);
- PhoneAuthCredential로- MultiFactorAssertion객체를 초기화합니다.- Kotlin- val multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential)- Java- MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
- resolver.resolveSignIn()을 호출하여 2차 인증을 완료합니다. 그런 다음 표준 공급업체별 데이터 및 인증 사용자 인증 정보가 포함된 원래 로그인 결과에 액세스할 수 있습니다.- Kotlin- multiFactorResolver .resolveSignIn(multiFactorAssertion) .addOnCompleteListener { task -> if (task.isSuccessful) { val authResult = task.result // 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. } }- Java- 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. } } });
아래 코드는 다중 인증 사용자를 위한 전체 로그인 예시를 보여줍니다.
Kotlin
FirebaseAuth.getInstance()
    .signInWithEmailAndPassword(email, password)
    .addOnCompleteListener { task ->
        if (task.isSuccessful) {
            // User is not enrolled with a second factor and is successfully
            // signed in.
            // ...
            return@addOnCompleteListener
        }
        if (task.exception is FirebaseAuthMultiFactorException) {
            val multiFactorResolver =
                (task.exception as FirebaseAuthMultiFactorException).resolver
            // Ask user which second factor to use. Then, get
            // the selected hint:
            val selectedHint =
                multiFactorResolver.hints[selectedIndex] as PhoneMultiFactorInfo
            // Send the SMS verification code.
            PhoneAuthProvider.verifyPhoneNumber(
                PhoneAuthOptions.newBuilder()
                    .setActivity(this)
                    .setMultiFactorSession(multiFactorResolver.session)
                    .setMultiFactorHint(selectedHint)
                    .setCallbacks(generateCallbacks())
                    .setTimeout(30L, TimeUnit.SECONDS)
                    .build()
            )
            // Ask user for the SMS verification code, then use it to get
            // a PhoneAuthCredential:
            val credential =
                PhoneAuthProvider.getCredential(verificationId, verificationCode)
            // Initialize a MultiFactorAssertion object with the
            // PhoneAuthCredential.
            val multiFactorAssertion: MultiFactorAssertion =
                PhoneMultiFactorGenerator.getAssertion(credential)
            // Complete sign-in.
            multiFactorResolver
                .resolveSignIn(multiFactorAssertion)
                .addOnCompleteListener { task ->
                    if (task.isSuccessful) {
                        // User successfully signed in with the
                        // second factor phone number.
                    }
                    // ...
                }
        } else {
            // Handle other errors such as wrong password.
        }
    }
Java
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(selectedHint)
                .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.
        }
      }
      });
수고하셨습니다. 다중 인증(MFA)을 사용하여 사용자가 성공적으로 로그인했습니다.
다음 단계
- Admin SDK를 사용하여 프로그래매틱 방식으로 다중 인증 사용자를 관리합니다.