在 iOS 裝置上透過 Apple 登入使用者
本文說明如何使用 Identity Platform,在 iOS 應用程式中加入「使用 Apple 登入」功能。
事前準備
- 啟用 Identity Platform 並建立基本 iOS 應用程式。如要瞭解如何啟用 Identity Platform 及登入,請參閱快速入門導覽課程。 
- 加入 Apple 開發人員計畫。 
向 Apple 設定應用程式
在 Apple Developer 網站上:
- 如果您使用 Identity Platform 傳送電子郵件給使用者,請使用下列電子郵件透過 Apple 的私密電子郵件轉送服務設定專案: - noreply@project-id.firebaseapp.com - 如果應用程式有自訂電子郵件範本,也可以使用該範本。 
遵守 Apple 的去識別化資料規定
Apple 讓使用者選擇匿名處理資料,包括電子郵件地址。選取這個選項後,Apple 會為使用者指派網域為 privaterelay.appleid.com 的模糊化電子郵件地址。
您的應用程式必須遵守 Apple 針對匿名 Apple ID 制定的所有適用開發人員政策或條款。包括在將任何個人識別資訊 (PII) 與匿名 Apple ID 建立關聯前,先徵求使用者同意。涉及 PII 的動作包括但不限於:
- 將電子郵件地址連結至匿名 Apple ID,或反向操作。
- 將電話號碼連結至匿名 Apple ID,或反向操作
- 將非匿名社群憑證 (例如 Facebook 或 Google) 連結至匿名 Apple ID,反之亦然。
詳情請參閱 Apple 開發人員帳戶的《Apple 開發人員計畫授權協議》。
將 Apple 設為提供者
如要將 Apple 設定為識別資訊提供者,請按照下列步驟操作:
- 前往 Google Cloud 控制台的「Identity Providers」(識別資訊提供者) 頁面。 
- 按一下「Add A Provider」。 
- 從清單中選取「Apple」。 
- 在「平台」下方選取「iOS」。 
- 輸入應用程式的套件組合 ID。 
- 按一下「已授權網域」下方的「新增網域」,註冊應用程式的網域。為方便開發, - localhost預設為啟用狀態。
- 在「設定應用程式」下方,按一下「iOS」。將程式碼片段複製到應用程式的程式碼中,初始化 Identity Platform 用戶端 SDK。 
- 按一下 [儲存]。 
透過用戶端 SDK 登入使用者
- 使用 Apple 的驗證服務架構登入使用者並取得 ID 權杖。 
- 呼叫 - SecRandomCopyBytes(_:_:_:)產生隨機字串,也就是 nonce。- 隨機值可用於防範重送攻擊。您會在驗證要求中加入 Nonce 的 SHA-256 雜湊,而 Apple 會在回應中傳回該雜湊,且不會修改。接著,Identity Platform 會比較原始雜湊與 Apple 傳回的值,藉此驗證回應。 
- 啟動 Apple 的登入流程,包括您在上一個步驟中建立的隨機數 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; }
- 在 - ASAuthorizationControllerDelegate的實作中處理 Apple 的回應。如果登入成功,請使用 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); }
與許多其他身分提供者不同,Apple 不會提供相片網址。
如果使用者選擇不與應用程式分享真實電子郵件地址,Apple 會提供專屬電子郵件地址供使用者分享。這封電子郵件的格式為 xyz@privaterelay.appleid.com。如果你設定了私人電子郵件轉送服務,Apple 會將寄至匿名地址的電子郵件轉寄至使用者的真實電子郵件地址。
Apple 只會在使用者首次登入時,與應用程式分享使用者資訊 (例如顯示名稱)。在大多數情況下,Identity Platform 會儲存這項資料,讓您在日後的工作階段中使用 firebase.auth().currentUser.displayName 擷取資料。不過,如果您在整合 Identity Platform 前,允許使用者透過 Apple 登入應用程式,系統就不會提供使用者資訊。
刪除使用者帳戶
根據 Apple 的規定,支援帳戶建立功能的 iOS 應用程式,也必須允許使用者從應用程式內啟動帳戶刪除程序。
刪除使用者帳戶時,您必須先撤銷使用者的權杖,再刪除使用者帳戶,以及您為該使用者儲存在 Firestore、Cloud Storage 和 Firebase 即時資料庫中的所有資料。詳情請參閱 Apple 開發人員支援說明文件中的「在應用程式中提供帳戶刪除功能」。
由於 Identity Platform 不會在透過 Apple 登入建立使用者時儲存使用者權杖,因此您必須先要求使用者登入,才能撤銷權杖並刪除帳戶。或者,如要避免在使用者透過 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){
                                        // ...
                                    }];
                                  }
                                }];
    }
  }
}
後續步驟
- 進一步瞭解 Identity Platform 使用者。
- 透過其他身分識別提供者登入使用者。