在 iOS 上透過 Apple 登入使用者

本文說明如何使用 Identity Platform,在 iOS 應用程式中加入「使用 Apple 登入」功能。

事前準備

透過 Apple 設定應用程式

在 Apple Developer 網站上:

  1. 為應用程式啟用「使用 Apple 帳戶登入」功能

  2. 如果您使用 Identity Platform 傳送電子郵件給使用者,請使用以下電子郵件透過 Apple 的私人電子郵件轉送服務設定專案

    noreply@project-id.firebaseapp.com
    

    如果您的應用程式有自訂電子郵件範本,您也可以使用該範本。

遵守 Apple 的去識別化資料規定

Apple 提供使用者選擇是否要匿名處理資料的選項,包括電子郵件地址。Apple 會為選擇這個選項的使用者指派使用網域 privaterelay.appleid.com 的模糊處理電子郵件地址。

您的應用程式必須遵守 Apple 針對匿名 Apple ID 所發布的任何適用開發人員政策或條款。這包括在將任何個人識別資訊 (PII) 與匿名 Apple ID 建立關聯前,先徵得使用者同意。涉及個人識別資訊的動作包括但不限於:

  • 將電子郵件地址連結至匿名 Apple ID,或反之。
  • 將電話號碼連結至匿名 Apple ID,或將匿名 Apple ID 連結至電話號碼
  • 將非匿名社群媒體憑證 (例如 Facebook 或 Google) 連結至匿名 Apple ID,或反之。

如需更多資訊,請參閱 Apple 開發人員帳戶的《Apple Developer Program License Agreement》。

將 Apple 設為供應商

如要將 Apple 設為識別資訊提供者,請按照下列步驟操作:

  1. 前往 Google Cloud 控制台的「Identity Providers」頁面。

    前往「Identity Providers」(識別資訊提供者) 頁面

  2. 按一下「Add A Provider」

  3. 從清單中選取「Apple」

  4. 在「平台」下方,選取「iOS」

  5. 輸入應用程式的套件 ID

  6. 按一下「Authorized Domains」下方的「Add domain」,即可註冊應用程式的網域。為方便開發,localhost 已預設為啟用。

  7. 按一下「設定應用程式」下方的「iOS」。將程式碼片段複製到應用程式的程式碼中,即可初始化 Identity Platform 用戶端 SDK。

  8. 按一下 [儲存]

使用用戶端 SDK 登入使用者

  1. 使用 Apple 的Authentication Services 架構登入使用者,並取得 ID 權杖。

  2. 呼叫 SecRandomCopyBytes(_:_:_:) 產生隨機字串 (稱為「Nonce」)。

    這個值可用來防範重送攻擊。您可以在驗證要求中加入 Nonce 的 SHA-256 雜湊,Apple 會在回應中原封不動地傳回該雜湊。接著,Identity Platform 會將原始雜湊與 Apple 傳回的值進行比較,驗證回應。

  3. 開始 Apple 的登入流程,包括您在前一個步驟中建立的 Nonce 的 SHA-256 雜湊,以及用於處理 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 的回應。如果登入成功,請使用 Apple 回應中的 ID 權杖和未經雜湊的 Nonce,透過 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);
    }
    

與許多其他身分提供者不同,Apple 不會提供相片網址。

如果使用者選擇不與您的應用程式分享真實電子郵件地址,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){
                                        // ...
                                    }];
                                  }
                                }];
    }
  }
}

後續步驟