iOS에서 Apple로 사용자 로그인

이 문서에서는 Identity Platform을 사용하여 iOS 앱에 Apple로 로그인을 추가하는 방법을 보여줍니다.

시작하기 전에

Apple로 앱 구성

Apple 개발자 사이트에서 다음 단계를 따르세요.

  1. 앱에 Apple로 로그인 기능을 사용 설정합니다.

  2. Identity Platform을 사용하여 사용자에게 이메일을 보내는 경우 다음 이메일을 사용하여 Apple 비공개 이메일 릴레이 서비스로 프로젝트를 구성합니다.

    noreply@project-id.firebaseapp.com
    

    또한 앱에 맞춤 이메일 템플릿이 있는 경우 이 템플릿을 사용할 수 있습니다.

Apple의 익명 처리된 데이터 요구사항 준수

Apple은 사용자에게 이메일 주소 등의 데이터를 익명처리할 수 있는 옵션을 제공합니다. Apple은 이 옵션을 선택한 사용자에게 privaterelay.appleid.com 도메인이 있는 난독화된 이메일 주소를 할당합니다.

앱은 익명처리된 Apple ID와 관련된 모든 관련 개발자 정책 또는 Apple의 약관을 준수해야 합니다. 여기에는 개인 식별 정보(PII)를 익명 처리된 Apple ID와 연결하기 전에 사용자 동의를 얻는 과정이 포함됩니다. PII와 관련된 작업에는 다음이 포함되지만 이에 국한되지는 않습니다.

  • 익명처리된 Apple ID에 이메일 주소를 연결하거나 그 반대로 연결합니다.
  • 익명처리된 Apple ID에 전화번호 연결하거나 그 반대로 연결합니다.
  • 익명처리된 Apple ID에 익명처리되지 않은 소셜 사용자 인증 정보(Facebook, Google 등)를 연결하거나 그 반대로 연결합니다.

자세한 내용은 Apple 개발자 계정의 Apple 개발자 프로그램 라이선스 계약을 참조하세요.

Apple을 공급업체로 구성

Apple을 ID 공급업체로 구성하려면 다음 단계를 따르세요.

  1. Google Cloud Console에서 ID 공급업체 페이지로 이동합니다.

    ID 공급업체 페이지로 이동

  2. 공급업체 추가를 클릭합니다.

  3. 목록에서 Apple을 선택합니다.

  4. 플랫폼에서 iOS를 선택합니다.

  5. 앱의 번들 ID를 입력합니다.

  6. 승인된 도메인 아래의 도메인 추가를 클릭하여 앱의 도메인을 등록합니다. 개발 용도로는 localhost가 이미 기본적으로 사용 설정되어 있습니다.

  7. 애플리케이션 구성에서 iOS를 클릭합니다. 스니펫을 앱 코드에 복사하여 Identity Platform 클라이언트 SDK를 초기화합니다.

  8. 저장을 클릭합니다.

클라이언트 SDK로 사용자 로그인

  1. 사용자를 로그인하고 Apple의 인증 서비스 프레임워크를 사용하여 ID 토큰을 받습니다.

  2. SecRandomCopyBytes(_:_:_:)를 호출하여 nonce라는 임의의 문자열을 생성합니다.

    nonce는 재생 공격을 방지하는 데 사용됩니다. 인증 요청에 nonce의 SHA-256 해시를 포함하고 Apple은 응답에서 그것을 수정되지 않은 상태로 반환합니다. 그런 다음 Identity Platform은 원래 해시를 Apple에서 반환한 값과 비교하여 응답을 검증합니다.

  3. 이전 단계에서 만든 nonce의 SHA-256 해시와 Apple의 응답을 처리하는 대리자 클래스를 포함하여 Apple의 로그인 과정을 시작합니다.

    Swift

    import CryptoKit
    
    // Unhashed nonce.
    fileprivate var currentNonce: String?
    
    @available(iOS 13, *)
    func startSignInWithAppleFlow() {
      let nonce = randomNonceString()
      currentNonce = nonce
      let appleIDProvider = ASAuthorizationAppleIDProvider()
      let request = appleIDProvider.createRequest()
      request.requestedScopes = [.fullName, .email]
      request.nonce = sha256(nonce)
    
      let authorizationController = ASAuthorizationController(authorizationRequests: [request])
      authorizationController.delegate = self
      authorizationController.presentationContextProvider = self
      authorizationController.performRequests()
    }
    
    @available(iOS 13, *)
    private func sha256(_ input: String) -> String {
      let inputData = Data(input.utf8)
      let hashedData = SHA256.hash(data: inputData)
      let hashString = hashedData.compactMap {
        return String(format: "%02x", $0)
      }.joined()
    
      return hashString
    }
    

    Objective-C

    @import CommonCrypto;
    
    - (void)startSignInWithAppleFlow {
      NSString *nonce = [self randomNonce:32];
      self.currentNonce = nonce;
      ASAuthorizationAppleIDProvider *appleIDProvider = [[ASAuthorizationAppleIDProvider alloc] init];
      ASAuthorizationAppleIDRequest *request = [appleIDProvider createRequest];
      request.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
      request.nonce = [self stringBySha256HashingString:nonce];
    
      ASAuthorizationController *authorizationController =
          [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]];
      authorizationController.delegate = self;
      authorizationController.presentationContextProvider = self;
      [authorizationController performRequests];
    }
    
    - (NSString *)stringBySha256HashingString:(NSString *)input {
      const char *string = [input UTF8String];
      unsigned char result[CC_SHA256_DIGEST_LENGTH];
      CC_SHA256(string, (CC_LONG)strlen(string), result);
    
      NSMutableString *hashed = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
      for (NSInteger i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
        [hashed appendFormat:@"%02x", result[i]];
      }
      return hashed;
    }
    
  4. ASAuthorizationControllerDelegate를 구현하여 Apple의 응답을 처리합니다. 로그인에 성공하면 해시되지 않은 nonce가 포함된 Apple의 응답에서 ID 토큰을 사용하여 Identity Platform에 인증합니다.

    Swift

    @available(iOS 13.0, *)
    extension MainViewController: ASAuthorizationControllerDelegate {
    
      func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
          guard let nonce = currentNonce else {
            fatalError("Invalid state: A login callback was received, but no login request was sent.")
          }
          guard let appleIDToken = appleIDCredential.identityToken else {
            print("Unable to fetch identity token")
            return
          }
          guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
            print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
            return
          }
          // Initialize a Firebase credential.
          let credential = OAuthProvider.credential(withProviderID: "apple.com",
                                                    IDToken: idTokenString,
                                                    rawNonce: nonce)
          // Sign in with Firebase.
          Auth.auth().signIn(with: credential) { (authResult, error) in
            if error {
              // Error. If error.code == .MissingOrInvalidNonce, make sure
              // you're sending the SHA256-hashed nonce as a hex string with
              // your request to Apple.
              print(error.localizedDescription)
              return
            }
            // User is signed in to Firebase with Apple.
            // ...
          }
        }
      }
    
      func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        // Handle error.
        print("Sign in with Apple errored: \(error)")
      }
    }
    

    Objective-C

    - (void)authorizationController:(ASAuthorizationController *)controller
      didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) {
      if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
        ASAuthorizationAppleIDCredential *appleIDCredential = authorization.credential;
        NSString *rawNonce = self.currentNonce;
        NSAssert(rawNonce != nil, @"Invalid state: A login callback was received, but no login request was sent.");
    
        if (appleIDCredential.identityToken == nil) {
          NSLog(@"Unable to fetch identity token.");
          return;
        }
        NSString *idToken = [[NSString alloc] initWithData:appleIDCredential.identityToken
                                                  encoding:NSUTF8StringEncoding];
        if (idToken == nil) {
          NSLog(@"Unable to serialize id token from data: %@", appleIDCredential.identityToken);
        }
        // Initialize a Firebase credential.
        FIROAuthCredential *credential = [FIROAuthProvider credentialWithProviderID:@"apple.com"
                                                                            IDToken:idToken
                                                                           rawNonce:rawNonce];
        // Sign in with Firebase.
        [[FIRAuth auth] signInWithCredential:credential
                                  completion:^(FIRAuthDataResult * _Nullable authResult,
                                               NSError * _Nullable error) {
          if (error != nil) {
            // Error. If error.code == FIRAuthErrorCodeMissingOrInvalidNonce,
            // make sure you're sending the SHA256-hashed nonce as a hex string
            // with your request to Apple.
            return;
          }
          // Sign-in succeeded!
        }];
      }
    }
    
    - (void)authorizationController:(ASAuthorizationController *)controller
               didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)) {
      NSLog(@"Sign in with Apple errored: %@", error);
    }
    

다른 많은 ID 공급업체와는 달리 Apple에서는 사진 URL을 제공하지 않습니다.

사용자가 앱에 실제 이메일을 공유하지 않는 경우 Apple에서는 사용자가 대신 공유할 고유 이메일 주소를 프로비저닝합니다. 이 이메일은 xyz@privaterelay.appleid.com 형식을 사용합니다. 비공개 이메일 릴레이 서비스를 구성한 경우 Apple은 익명처리된 이메일 주소로 전송된 이메일을 사용자의 실제 이메일 주소로 전달합니다.

Apple은 사용자가 처음 로그인할 때만 표시 이름과 같은 사용자 정보를 앱과 공유합니다. 대부분의 경우 Identity Platform은 이 데이터를 저장하므로 향후 세션 중에 firebase.auth().currentUser.displayName을 사용해 데이터를 가져올 수 있습니다. 하지만 Identity Platform과 통합하기 전에 사용자가 Apple을 사용하여 앱에 로그인할 수 있도록 허용한 경우에는 사용자 정보를 사용할 수 없습니다.

사용자 계정 삭제

Apple에서는 계정 생성을 지원하는 iOS 앱이 사용자가 앱 내에서 계정 삭제를 시작할 수 있도록 해야 허용한다고 요청합니다.

사용자 계정을 삭제할 때 삭제하기 전에 사용자의 토큰을 취소해야 하며 Firestore, Cloud Storage, Firebase 실시간 데이터베이스에 저장한 모든 데이터도 삭제해야 합니다. 자세한 내용은 Apple의 개발자 지원 문서의 앱에서 계정 삭제 제공을 참조하세요.

Apple 로그인으로 사용자를 생성할 때 Identity Platform은 사용자 토큰을 저장하지 않으므로 토큰을 취소하고 계정을 삭제하기 전에 사용자에게 로그인하도록 요청해야 합니다. 또는 사용자가 Apple 로그인으로 로그인했을 때 사용자에게 다시 로그인하도록 요청하지 않으려면 토큰 취소 중에 재사용할 승인 코드를 저장할 수 있습니다.

사용자 토큰을 취소하고 계정을 삭제하려면 다음을 실행합니다.

Swift

let user = Auth.auth().currentUser

// Check if the user has a token.
if let providerData = user?.providerData {
  for provider in providerData {
    guard let provider = provider as? FIRUserInfo else {
      continue
    }
    if provider.providerID() == "apple.com" {
      isAppleProviderLinked = true
    }
  }
}

// Re-authenticate the user and revoke their token
if isAppleProviderLinked {
  let request = appleIDRequest(withState: "revokeAppleTokenAndDeleteUser")
  let controller = ASAuthorizationController(authorizationRequests: [request])
  controller.delegate = self
  controller.presentationContextProvider = self
  controller.performRequests()
} else {
  // Usual user deletion
}

func authorizationController(
  controller: ASAuthorizationController,
  didCompleteWithAuthorization authorization: ASAuthorization
) {
  if authorization.credential is ASAuthorizationAppleIDCredential {
    let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
    if authorization.credential is ASAuthorizationAppleIDCredential {
      if appleIDCredential.state == "signIn" {
        // Sign in with Firebase.
        // ...
      } else if appleIDCredential.state == "revokeAppleTokenAndDeleteUser" {
        // Revoke token with Firebase.
        Auth.auth().revokeTokenWithAuthorizationCode(code) { error in
          if error != nil {
            // Token revocation failed.
          } else {
            // Token revocation succeeded then delete user again.
            let user = Auth.auth().currentUser
            user?.delete { error in
              // ...
            }
          }

        }
      }
    }
  }
}

Objective-C

FIRUser *user = [FIRAuth auth].currentUser;

// Check if the user has a token.
BOOL isAppleProviderLinked = false;
for (id<FIRUserInfo> provider in user.providerData) {
  if ([[provider providerID] isEqual:@"apple.com"]) {
    isAppleProviderLinked = true;
  }
}

// Re-authenticate the user and revoke their token
if (isAppleProviderLinked) {
  if (@available(iOS 13, *)) {
    ASAuthorizationAppleIDRequest *request =
        [self appleIDRequestWithState:@"revokeAppleTokenAndDeleteUser"];
    ASAuthorizationController *controller = [[ASAuthorizationController alloc]
        initWithAuthorizationRequests:@[ request ]];
    controller.delegate = self;
    controller.presentationContextProvider = self;
    [controller performRequests];
  }
} else {
  // Usual user deletion
}

- (void)authorizationController:(ASAuthorizationController *)controller
    didCompleteWithAuthorization:(ASAuthorization *)authorization
    API_AVAILABLE(ios(13.0)) {
  if ([authorization.credential
          isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
    ASAuthorizationAppleIDCredential *appleIDCredential =
        authorization.credential;

    if ([appleIDCredential.state isEqualToString:@"signIn"]) {
      // Sign in with Firebase.
      // ...
    } else if ([appleIDCredential.state
                  isEqualToString:@"revokeAppleTokenAndDeleteUser"]) {
      // Revoke token with Firebase.
      NSString *code =
          [[NSString alloc] initWithData:appleIDCredential.authorizationCode
                                encoding:NSUTF8StringEncoding];
      [[FIRAuth auth]
          revokeTokenWithAuthorizationCode:code
                                completion:^(NSError *_Nullable error) {
                                  if (error != nil) {
                                    // Token revocation failed.
                                  } else {
                                    // Token revocation succeeded then delete
                                    // user again.
                                    FIRUser *user = [FIRAuth auth].currentUser;
                                    [user deleteWithCompletion:^(
                                              NSError *_Nullable error){
                                        // ...
                                    }];
                                  }
                                }];
    }
  }
}

다음 단계