iOS 앱에 다단계 인증 추가

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

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

시작하기 전에

  1. 다중 인증(MFA)을 지원하는 제공업체를 하나 이상 사용 설정합니다. 전화 인증, 익명 인증, Apple Game Center를 제외한 모든 제공업체는 MFA를 지원합니다.

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

다단계 인증 사용 설정

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

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

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

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

  5. 저장을 클릭합니다.

앱 확인

Identity Platform은 SMS 요청이 앱에서 오는지 확인해야 합니다. 이 작업은 두 가지 방법으로 수행할 수 있습니다.

  • 자동 APN 알림: 사용자를 처음으로 로그인 처리하면 Identity Platform에서 사용자의 기기에 자동 푸시 알림을 보낼 수 있습니다. 앱이 알림을 수신하면 인증을 진행할 수 있습니다. iOS 8.0부터는 사용자에게 푸시 알림을 허용하도록 요청하지 않아도 이 방법을 사용할 수 있습니다.

  • reCAPTCHA 인증: 사용자가 백그라운드 새로고침을 사용 중지하거나 iOS 시뮬레이터에서 앱을 테스트하는 경우와 같이 자동 알림을 보낼 수 없는 경우 reCAPTCHA를 사용할 수 있습니다. 대부분 reCAPTCHA는 사용자 상호작용 없이 자동으로 해결됩니다.

자동 알림 사용

Identity Platform에서 APN 알림을 사용하도록 설정하려면 다음 단계를 따르세요.

  1. Xcode에서 프로젝트에 푸시 알림을 사용 설정합니다.

  2. Firebase Console을 사용하여 APN 인증 키를 업로드합니다. 변경사항이 자동으로 Google Cloud Identity Platform에 적용됩니다. 아직 APN 인증 키가 없으면 FCM에서 APN 구성을 참조하여 설정 방법을 알아보세요.

    1. Firebase Console을 엽니다.

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

    3. 클라우드 메시징 탭을 선택합니다.

    4. APN 인증 키iOS 앱 구성 섹션에서 업로드를 클릭합니다.

    5. 키를 선택합니다.

    6. 키의 키 ID를 추가합니다. 키 ID는 Apple 개발자 구성원 센터인증서, 식별자, 프로필에서 찾을 수 있습니다.

    7. 업로드를 클릭합니다.

APN 인증서가 이미 있다면 인증서를 대신 업로드할 수 있습니다.

reCAPTCHA 인증 사용

클라이언트 SDK에서 reCAPTCHA를 사용하도록 설정하려면 다음 단계를 따르세요.

  1. Xcode에서 프로젝트 구성을 엽니다.

  2. 왼쪽 트리 보기에서 프로젝트 이름을 더블클릭합니다.

  3. 타겟 섹션에서 앱을 선택합니다.

  4. 정보 탭을 선택합니다.

  5. URL 유형 섹션을 펼칩니다.

  6. 화면의 + 버튼을 클릭합니다.

  7. URL 스키마 입력란에 반전된 클라이언트 ID를 입력합니다. 이 값은 GoogleService-Info.plist 구성 파일에 REVERSED_CLIENT_ID로 표시됩니다.

완료되면 구성이 다음과 비슷하게 표시됩니다.

맞춤 스키마

선택적으로 reCAPTCHA를 표시할 때 앱에서 SFSafariViewController 또는 UIWebView를 표시하는 방식을 맞춤설정할 수 있습니다. 이렇게 하려면 FIRAuthUIDelegate 프로토콜을 준수하는 커스텀 클래스를 만들어 verifyPhoneNumber:UIDelegate:completion:에 전달합니다.

등록 패턴 선택

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

  • 등록 시 사용자의 두 번째 단계를 등록합니다. 앱이 모든 사용자에게 다중 인증(MFA)을 요구한다면 이 방법을 사용하세요. 두 번째 단계를 등록하려면 계정에 인증된 이메일 주소가 있어야 하므로 등록 과정에서 이를 수용해야 합니다.

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

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

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

두 번째 단계 등록

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

  1. 사용자를 다시 인증합니다.

  2. 사용자에게 전화번호를 입력하도록 요청합니다.

  3. 사용자를 위한 다단계 세션을 가져옵니다.

    Swift

    authResult.user.multiFactor.getSessionWithCompletion() { (session, error) in
      // ...
    }
    

    Objective-C

    [authResult.user.multiFactor
      getSessionWithCompletion:^(FIRMultiFactorSession * _Nullable session,
                                NSError * _Nullable error) {
        // ...
    }];
    
  4. 사용자 전화로 인증 코드를 보내세요. 전화번호 형식은 앞에 +를 입력하고 구두점이나 공백은 사용하지 마세요(예: +15105551234).

    Swift

    // Send SMS verification code.
    PhoneAuthProvider.provider().verifyPhoneNumber(
      phoneNumber,
      uiDelegate: nil,
      multiFactorSession: session) { (verificationId, error) in
        // verificationId will be needed for enrollment completion.
    }
    

    Objective-C

    // Send SMS verification code.
    [FIRPhoneAuthProvider.provider verifyPhoneNumber:phoneNumber
                                          UIDelegate:nil
                                  multiFactorSession:session
                                          completion:^(NSString * _Nullable verificationID,
                                                        NSError * _Nullable error) {
        // verificationId will be needed for enrollment completion.
    }];
    

    필수 사항은 아니지만, 사용자에게 SMS 메시지가 전송되고 표준 요금이 적용된다는 점을 미리 알리는 것이 좋습니다.

    verifyPhoneNumber() 메서드는 자동 푸시 알림을 사용하여 백그라운드에서 앱 인증 프로세스를 시작합니다. 자동 푸시 알림을 사용할 수 없는 경우 reCAPTCHA 테스트가 대신 실행됩니다.

  5. SMS 코드가 전송되면 사용자에게 코드를 확인하도록 요청합니다. 그런 다음 응답을 사용하여 PhoneAuthCredential을 빌드합니다.

    Swift

    // Ask user for the verification code. Then:
    let credential = PhoneAuthProvider.provider().credential(
      withVerificationID: verificationId,
      verificationCode: verificationCode)
    

    Objective-C

    // Ask user for the SMS verification code. Then:
    FIRPhoneAuthCredential *credential = [FIRPhoneAuthProvider.provider
                                           credentialWithVerificationID:verificationID
                                           verificationCode:kPhoneSecondFactorVerificationCode];
    
  6. 어설션 객체를 초기화합니다.

    Swift

    let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
    

    Objective-C

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

    Swift

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    user.multiFactor.enroll(with: assertion, displayName: displayName) { (error) in
      // ...
    }
    

    Objective-C

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    [authResult.user.multiFactor enrollWithAssertion:assertion
                                         displayName:nil
                                          completion:^(NSError * _Nullable error) {
        // ...
    }];
    

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

Swift

let user = Auth.auth().currentUser
user?.multiFactor.getSessionWithCompletion({ (session, error) in
  // Send SMS verification code.
  PhoneAuthProvider.provider().verifyPhoneNumber(
    phoneNumber,
    uiDelegate: nil,
    multiFactorSession: session
  ) { (verificationId, error) in
    // verificationId will be needed for enrollment completion.
    // Ask user for the verification code.
    let credential = PhoneAuthProvider.provider().credential(
      withVerificationID: verificationId!,
      verificationCode: phoneSecondFactorVerificationCode)
    let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    user?.multiFactor.enroll(with: assertion, displayName: displayName) { (error) in
      // ...
    }
  }
})

Objective-C

FIRUser *user = FIRAuth.auth.currentUser;
[user.multiFactor getSessionWithCompletion:^(FIRMultiFactorSession * _Nullable session,
                                              NSError * _Nullable error) {
    // Send SMS verification code.
    [FIRPhoneAuthProvider.provider
      verifyPhoneNumber:phoneNumber
      UIDelegate:nil
      multiFactorSession:session
      completion:^(NSString * _Nullable verificationID, NSError * _Nullable error) {
        // verificationId will be needed for enrollment completion.

        // Ask user for the verification code.
        // ...

        // Then:
        FIRPhoneAuthCredential *credential =
            [FIRPhoneAuthProvider.provider credentialWithVerificationID:verificationID
                                                        verificationCode:kPhoneSecondFactorVerificationCode];
        FIRMultiFactorAssertion *assertion =
            [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];

        // Complete enrollment. This will update the underlying tokens
        // and trigger ID token change listener.
        [user.multiFactor enrollWithAssertion:assertion
                                  displayName:displayName
                                    completion:^(NSError * _Nullable error) {
            // ...
        }];
    }];
}];

수고하셨습니다. 사용자의 두 번째 인증 단계가 성공적으로 등록되었습니다.

두 번째 단계 인증으로 사용자 로그인 처리

2단계 SMS 인증으로 사용자를 로그인 처리하려면 다음 안내를 따르세요.

  1. 첫 번째 단계로 사용자를 로그인 처리한 후 다중 인증(MFA)이 필요함을 나타내는 오류를 확인합니다. 이 오류에는 리졸버, 등록된 두 번째 단계에 대한 힌트, 사용자가 첫 번째 단계에서 성공적으로 인증되었음을 증명하는 기본 세션이 포함됩니다.

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

    Swift

    Auth.auth().signIn(
      withEmail: email,
      password: password
    ) { (result, error) in
      let authError = error as NSError
      if authError?.code == AuthErrorCode.secondFactorRequired.rawValue {
        // The user is a multi-factor user. Second factor challenge is required.
        let resolver =
          authError!.userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver
        // ...
      } else {
        // Handle other errors such as wrong password.
      }
    }
    

    Objective-C

    [FIRAuth.auth signInWithEmail:email
                         password:password
                       completion:^(FIRAuthDataResult * _Nullable authResult,
                                    NSError * _Nullable error) {
        if (error == nil || error.code != FIRAuthErrorCodeSecondFactorRequired) {
            // User is not enrolled with a second factor and is successfully signed in.
            // ...
        } else {
            // The user is a multi-factor user. Second factor challenge is required.
        }
    }];
    

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

  2. 사용자가 보조 단계를 여러 개 등록한 경우 어떤 단계를 사용할지 질문합니다. resolver.hints[selectedIndex].phoneNumber를 사용하여 마스킹된 전화번호를 가져오고 resolver.hints[selectedIndex].displayName을 사용하여 표시 이름을 가져올 수 있습니다.

    Swift

    // Ask user which second factor to use. Then:
    if resolver.hints[selectedIndex].factorID == PhoneMultiFactorID {
      // User selected a phone second factor.
      // ...
    } else if resolver.hints[selectedIndex].factorID == TotpMultiFactorID {
      // User selected a TOTP second factor.
      // ...
    } else {
      // Unsupported second factor.
    }
    

    Objective-C

    FIRMultiFactorResolver *resolver =
        (FIRMultiFactorResolver *) error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey];
    
    // Ask user which second factor to use. Then:
    FIRPhoneMultiFactorInfo *hint = (FIRPhoneMultiFactorInfo *) resolver.hints[selectedIndex];
    if (hint.factorID == FIRPhoneMultiFactorID) {
      // User selected a phone second factor.
      // ...
    } else if (hint.factorID == FIRTOTPMultiFactorID) {
      // User selected a TOTP second factor.
      // ...
    } else {
      // Unsupported second factor.
    }
    
  3. 사용자 전화로 인증 코드 보내기

    Swift

    // Send SMS verification code.
    let hint = resolver.hints[selectedIndex] as! PhoneMultiFactorInfo
    PhoneAuthProvider.provider().verifyPhoneNumber(
      with: hint,
      uiDelegate: nil,
      multiFactorSession: resolver.session
    ) { (verificationId, error) in
      // verificationId will be needed for sign-in completion.
    }
    

    Objective-C

    // Send SMS verification code
    [FIRPhoneAuthProvider.provider
      verifyPhoneNumberWithMultiFactorInfo:hint
      UIDelegate:nil
      multiFactorSession:resolver.session
      completion:^(NSString * _Nullable verificationID, NSError * _Nullable error) {
        if (error != nil) {
            // Failed to verify phone number.
        }
    }];
    
  4. SMS 코드가 전송되면 사용자에게 코드를 확인하고 이를 사용하여 PhoneAuthCredential을 빌드하도록 요청합니다.

    Swift

    // Ask user for the verification code. Then:
    let credential = PhoneAuthProvider.provider().credential(
      withVerificationID: verificationId!,
      verificationCode: verificationCodeFromUser)
    

    Objective-C

    // Ask user for the SMS verification code. Then:
    FIRPhoneAuthCredential *credential =
        [FIRPhoneAuthProvider.provider
          credentialWithVerificationID:verificationID
                      verificationCode:verificationCodeFromUser];
    
  5. 사용자 인증 정보로 어설션 객체를 초기화합니다.

    Swift

    let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
    

    Objective-C

    FIRMultiFactorAssertion *assertion =
        [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];
    
  6. 로그인 문제를 해결합니다. 그런 다음 표준 공급업체별 데이터 및 인증 사용자 인증 정보가 포함된 원래 로그인 결과에 액세스할 수 있습니다.

    Swift

    // Complete sign-in. This will also trigger the Auth state listeners.
    resolver.resolveSignIn(with: assertion) { (authResult, error) in
      // authResult 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,
      // authResult.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.
    }
    

    Objective-C

    // Complete sign-in.
    [resolver resolveSignInWithAssertion:assertion
                              completion:^(FIRAuthDataResult * _Nullable authResult,
                                            NSError * _Nullable error) {
        if (error != nil) {
            // User successfully signed in with the second factor phone number.
        }
    }];
    

아래 코드는 다중 인증 사용자를 위한 전체 로그인 예시를 보여줍니다.

Swift

Auth.auth().signIn(
  withEmail: email,
  password: password
) { (result, error) in
  let authError = error as NSError?
  if authError?.code == AuthErrorCode.secondFactorRequired.rawValue {
    let resolver =
      authError!.userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver

    // Ask user which second factor to use.
    // ...

    // Then:
    let hint = resolver.hints[selectedIndex] as! PhoneMultiFactorInfo

    // Send SMS verification code
    PhoneAuthProvider.provider().verifyPhoneNumber(
      with: hint,
      uiDelegate: nil,
      multiFactorSession: resolver.session
    ) { (verificationId, error) in
      if error != nil {
        // Failed to verify phone number.
      }
      // Ask user for the SMS verification code.
      // ...

      // Then:
      let credential = PhoneAuthProvider.provider().credential(
        withVerificationID: verificationId!,
        verificationCode: verificationCodeFromUser)
      let assertion = PhoneMultiFactorGenerator.assertion(with: credential)

      // Complete sign-in.
      resolver.resolveSignIn(with: assertion) { (authResult, error) in
        if error != nil {
          // User successfully signed in with the second factor phone number.
        }
      }
    }
  }
}

Objective-C

[FIRAuth.auth signInWithEmail:email
                     password:password
                   completion:^(FIRAuthDataResult * _Nullable authResult,
                               NSError * _Nullable error) {
    if (error == nil || error.code != FIRAuthErrorCodeSecondFactorRequired) {
        // User is not enrolled with a second factor and is successfully signed in.
        // ...
    } else {
        FIRMultiFactorResolver *resolver =
            (FIRMultiFactorResolver *) error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey];

        // Ask user which second factor to use.
        // ...

        // Then:
        FIRPhoneMultiFactorInfo *hint = (FIRPhoneMultiFactorInfo *) resolver.hints[selectedIndex];

        // Send SMS verification code
        [FIRPhoneAuthProvider.provider
          verifyPhoneNumberWithMultiFactorInfo:hint
                                    UIDelegate:nil
                            multiFactorSession:resolver.session
                                    completion:^(NSString * _Nullable verificationID,
                                                NSError * _Nullable error) {
            if (error != nil) {
                // Failed to verify phone number.
            }

            // Ask user for the SMS verification code.
            // ...

            // Then:
            FIRPhoneAuthCredential *credential =
                [FIRPhoneAuthProvider.provider
                  credentialWithVerificationID:verificationID
                              verificationCode:kPhoneSecondFactorVerificationCode];
            FIRMultiFactorAssertion *assertion =
                [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];

            // Complete sign-in.
            [resolver resolveSignInWithAssertion:assertion
                                      completion:^(FIRAuthDataResult * _Nullable authResult,
                                                    NSError * _Nullable error) {
                if (error != nil) {
                    // User successfully signed in with the second factor phone number.
                }
            }];
        }];
    }
}];

수고하셨습니다. 다중 인증(MFA)을 사용하여 사용자가 성공적으로 로그인했습니다.

다음 단계