使用 Apple 進行身份驗證

通過使用 Firebase SDK 執行端到端 OAuth 2.0 登錄流程,您可以讓您的用戶使用他們的 Apple ID 通過 Firebase 進行身份驗證。

在你開始之前

要使用 Apple 登錄用戶,首先在 Apple 的開發者網站上配置使用 Apple 登錄,然後啟用 Apple 作為您的 Firebase 項目的登錄提供商。

加入蘋果開發者計劃

Sign In with Apple 只能由Apple Developer Program的成員配置。

配置使用 Apple 登錄

  1. 在 Apple 開發者網站的證書、標識符和配置文件頁面上為您的應用程序啟用 Sign In with Apple。
  2. 將您的網站與您的應用程序相關聯,如為 Web 配置登錄 Apple 的第一部分中所述。出現提示時,將以下 URL 註冊為返回 URL:
    https://YOUR_FIREBASE_PROJECT_ID.firebaseapp.com/__/auth/handler
    您可以在Firebase 控制台設置頁面上獲取您的 Firebase 項目 ID。完成後,記下您的新服務 ID,您將在下一節中用到它。
  3. 創建一個 Sign In with Apple private key 。在下一節中,您將需要新的私鑰和密鑰 ID。
  4. 如果您使用 Firebase 身份驗證的任何向用戶發送電子郵件的功能,包括電子郵件鏈接登錄、電子郵件地址驗證、帳戶更改撤銷等,請配置 Apple 私人電子郵件中繼服務並註冊noreply@ YOUR_FIREBASE_PROJECT_ID .firebaseapp.com (或您自定義的電子郵件模板域),以便 Apple 可以將 Firebase 身份驗證發送的電子郵件轉發到匿名的 Apple 電子郵件地址。

啟用 Apple 作為登錄提供商

  1. 將 Firebase 添加到您的 Apple 項目。當您在 Firebase 控制台中設置您的應用程序時,請務必註冊您的應用程序的 bundle ID。
  2. Firebase 控制台中,打開Auth部分。在登錄方法選項卡上,啟用Apple提供商。指定您在上一節中創建的服務 ID。此外,在OAuth 代碼流配置部分,指定您的 Apple Team ID 以及您在上一節中創建的私鑰和密鑰 ID。

符合 Apple 匿名數據要求

Sign In with Apple 為用戶提供了在登錄時匿名化他們的數據的選項,包括他們的電子郵件地址。選擇此選項的用戶擁有域為privaterelay.appleid.com的電子郵件地址。當您在您的應用程序中使用 Sign In with Apple 時,您必須遵守 Apple 關於這些匿名 Apple ID 的任何適用的開發者政策或條款。

這包括在將任何直接識別個人信息與匿名 Apple ID 相關聯之前獲得任何必要的用戶同意。使用 Firebase 身份驗證時,這可能包括以下操作:

  • 將電子郵件地址鏈接到匿名的 Apple ID,反之亦然。
  • 將電話號碼鏈接到匿名的 Apple ID,反之亦然
  • 將非匿名社交憑證(Facebook、Google 等)鏈接到匿名 Apple ID,反之亦然。

上面的列表並不詳盡。請參閱您開發者帳戶的“會員資格”部分中的 Apple 開發者計劃許可協議,以確保您的應用程序符合 Apple 的要求。

使用 Apple 登錄並使用 Firebase 進行身份驗證

要使用 Apple 帳戶進行身份驗證,首先使用 Apple 的AuthenticationServices框架讓用戶登錄到他們的 Apple 帳戶,然後使用來自 Apple 響應的 ID 令牌創建一個 Firebase AuthCredential對象:

  1. 對於每個登錄請求,生成一個隨機字符串(一個“nonce”),您將使用它來確保您獲得的 ID 令牌是專門為響應您的應用程序的身份驗證請求而授予的。此步驟對於防止重放攻擊很重要。

    您可以使用SecRandomCopyBytes(_:_:_)生成加密安全隨機數,如以下示例所示:

    迅速

    private func randomNonceString(length: Int = 32) -> String {
      precondition(length > 0)
      var randomBytes = [UInt8](repeating: 0, count: length)
      let errorCode = SecRandomCopyBytes(kSecRandomDefault, randomBytes.count, &randomBytes)
      if errorCode != errSecSuccess {
        fatalError(
          "Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)"
        )
      }
    
      let charset: [Character] =
        Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
    
      let nonce = randomBytes.map { byte in
        // Pick a random character from the set, wrapping around if needed.
        charset[Int(byte) % charset.count]
      }
    
      return String(nonce)
    }
    
        

    目標-C

    // Adapted from https://auth0.com/docs/api-auth/tutorials/nonce#generate-a-cryptographically-random-nonce
    - (NSString *)randomNonce:(NSInteger)length {
      NSAssert(length > 0, @"Expected nonce to have positive length");
      NSString *characterSet = @"0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._";
      NSMutableString *result = [NSMutableString string];
      NSInteger remainingLength = length;
    
      while (remainingLength > 0) {
        NSMutableArray *randoms = [NSMutableArray arrayWithCapacity:16];
        for (NSInteger i = 0; i < 16; i++) {
          uint8_t random = 0;
          int errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random);
          NSAssert(errorCode == errSecSuccess, @"Unable to generate nonce: OSStatus %i", errorCode);
    
          [randoms addObject:@(random)];
        }
    
        for (NSNumber *random in randoms) {
          if (remainingLength == 0) {
            break;
          }
    
          if (random.unsignedIntValue < characterSet.length) {
            unichar character = [characterSet characterAtIndex:random.unsignedIntValue];
            [result appendFormat:@"%C", character];
            remainingLength--;
          }
        }
      }
    
      return [result copy];
    }
        

    您將隨登錄請求發送 nonce 的 SHA256 哈希值,Apple 將在響應中原封不動地傳遞該哈希值。 Firebase 通過散列原始隨機數並將其與 Apple 傳遞的值進行比較來驗證響應。

    迅速

    @available(iOS 13, *)
    private func sha256(_ input: String) -> String {
      let inputData = Data(input.utf8)
      let hashedData = SHA256.hash(data: inputData)
      let hashString = hashedData.compactMap {
        String(format: "%02x", $0)
      }.joined()
    
      return hashString
    }
    
        

    目標-C

    - (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;
    }
        
  2. 啟動 Apple 的登錄流程,在您的請求中包括隨機數的 SHA256 哈希和將處理 Apple 響應的委託類(請參閱下一步):

    迅速

    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()
    }
    

    目標-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];
    }
    
  3. ASAuthorizationControllerDelegate的實現中處理 Apple 的響應。如果登錄成功,請使用 Apple 響應中的 ID 令牌和未經哈希處理的隨機數來通過 Firebase 進行身份驗證:

    迅速

    @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, including the user's full name.
          let credential = OAuthProvider.appleCredential(withIDToken: idTokenString,
                                                            rawNonce: nonce,
                                                            fullName: appleIDCredential.fullName)
          // 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)")
      }
    
    }
    

    目標-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, including the user's full name.
        FIROAuthCredential *credential = [FIROAuthProvider appleCredentialWithIDToken:IDToken
                                                                             rawNonce:self.appleRawNonce
                                                                             fullName:appleIDCredential.fullName];
    
        // 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);
    }
    

與 Firebase Auth 支持的其他提供商不同,Apple 不提供照片 URL。

此外,當用戶選擇不與應用程序共享他們的電子郵件時,Apple 會為該用戶提供一個唯一的電子郵件地址(格式為xyz@privaterelay.appleid.com ),並將其與您的應用程序共享。如果您配置了私人電子郵件中繼服務,Apple 會將發送到匿名地址的電子郵件轉發到用戶的真實電子郵件地址。

重新驗證和帳戶鏈接

相同的模式可以與reauthenticateWithCredential()一起使用,您可以使用它來為需要最近登錄的敏感操作檢索新的憑證:

迅速

// Initialize a fresh Apple credential with Firebase.
let credential = OAuthProvider.credential(
  withProviderID: "apple.com",
  IDToken: appleIdToken,
  rawNonce: rawNonce
)
// Reauthenticate current Apple user with fresh Apple credential.
Auth.auth().currentUser.reauthenticate(with: credential) { (authResult, error) in
  guard error != nil else { return }
  // Apple user successfully re-authenticated.
  // ...
}

目標-C

FIRAuthCredential *credential = [FIROAuthProvider credentialWithProviderID:@"apple.com",
                                                                   IDToken:appleIdToken,
                                                                  rawNonce:rawNonce];
[[FIRAuth auth].currentUser
    reauthenticateWithCredential:credential
                      completion:^(FIRAuthDataResult * _Nullable authResult,
                                   NSError * _Nullable error) {
  if (error) {
    // Handle error.
  }
  // Apple user successfully re-authenticated.
  // ...
}];

而且,您可以使用linkWithCredential()將不同的身份提供者鏈接到現有帳戶。

請注意,Apple 要求您在將用戶的 Apple 帳戶鏈接到其他數據之前獲得用戶的明確同意。

使用 Apple 登錄將不允許您重複使用授權憑據來鏈接到現有帳戶。如果您想將 Sign in with Apple 憑據鏈接到另一個帳戶,您必須首先嘗試使用舊的 Sign in with Apple 憑據鏈接帳戶,然後檢查返回的錯誤以找到新的憑據。新憑證將位於錯誤的userInfo字典中,可以通過AuthErrorUserInfoUpdatedCredentialKey鍵訪問。

例如,要將 Facebook 帳戶鏈接到當前的 Firebase 帳戶,請使用您通過將用戶登錄到 Facebook 獲得的訪問令牌:

迅速

// Initialize a Facebook credential with Firebase.
let credential = FacebookAuthProvider.credential(
  withAccessToken: AccessToken.current!.tokenString
)
// Assuming the current user is an Apple user linking a Facebook provider.
Auth.auth().currentUser.link(with: credential) { (authResult, error) in
  // Facebook credential is linked to the current Apple user.
  // The user can now sign in with Facebook or Apple to the same Firebase
  // account.
  // ...
}

目標-C

// Initialize a Facebook credential with Firebase.
FacebookAuthCredential *credential = [FIRFacebookAuthProvider credentialWithAccessToken:accessToken];
// Assuming the current user is an Apple user linking a Facebook provider.
[FIRAuth.auth linkWithCredential:credential completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) {
  // Facebook credential is linked to the current Apple user.
  // The user can now sign in with Facebook or Apple to the same Firebase
  // account.
  // ...
}];

令牌撤銷

Apple 要求支持帳戶創建的應用程序必須允許用戶在應用程序內啟動刪除他們的帳戶,如App Store 審核指南中所述

要滿足此要求,請執行以下步驟:

  1. 確保您填寫了使用 Apple 登錄提供商配置的服務 IDOAuth 代碼流配置部分,如配置使用 Apple 登錄部分所述。

  2. 由於在使用 Sign in with Apple 創建用戶時 Firebase 不存儲用戶令牌,因此您必須要求用戶重新登錄,然後才能撤銷他們的令牌並刪除帳戶。

    迅速

    private func deleteCurrentUser() {
      do {
        let nonce = try CryptoUtils.randomNonceString()
        currentNonce = nonce
        let appleIDProvider = ASAuthorizationAppleIDProvider()
        let request = appleIDProvider.createRequest()
        request.requestedScopes = [.fullName, .email]
        request.nonce = CryptoUtils.sha256(nonce)
    
        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.presentationContextProvider = self
        authorizationController.performRequests()
      } catch {
        // In the unlikely case that nonce generation fails, show error view.
        displayError(error)
      }
    }
    
    
  3. ASAuthorizationAppleIDCredential獲取授權碼,並使用它調用Auth.auth().revokeToken(withAuthorizationCode:)來撤銷用戶的令牌。

    迅速

    func authorizationController(controller: ASAuthorizationController,
                                 didCompleteWithAuthorization authorization: ASAuthorization) {
      guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
      else {
        print("Unable to retrieve AppleIDCredential")
        return
      }
    
      guard let _ = currentNonce else {
        fatalError("Invalid state: A login callback was received, but no login request was sent.")
      }
    
      guard let appleAuthCode = appleIDCredential.authorizationCode else {
        print("Unable to fetch authorization code")
        return
      }
    
      guard let authCodeString = String(data: appleAuthCode, encoding: .utf8) else {
        print("Unable to serialize auth code string from data: \(appleAuthCode.debugDescription)")
        return
      }
    
      Task {
        do {
          try await Auth.auth().revokeToken(withAuthorizationCode: authCodeString)
          try await user?.delete()
          self.updateUI()
        } catch {
          self.displayError(error)
        }
      }
    }
    
    
  4. 最後,刪除用戶帳戶(以及所有相關數據)

下一步

用戶首次登錄後,將創建一個新的用戶帳戶並將其鏈接到用戶登錄所用的憑據,即用戶名和密碼、電話號碼或身份驗證提供商信息。這個新帳戶存儲為您的 Firebase 項目的一部分,可用於在項目中的每個應用程序中識別用戶,無論用戶如何登錄。

  • 在您的應用程序中,您可以從User對象獲取用戶的基本個人資料信息。請參閱管理用戶

  • 在您的 Firebase Realtime Database 和 Cloud Storage Security Rules中,您可以從auth變量中獲取登錄用戶的唯一用戶 ID,並使用它來控制用戶可以訪問的數據。

您可以允許用戶使用多個身份驗證提供程序登錄您的應用程序,方法是將身份驗證提供程序憑據鏈接到現有用戶帳戶。

要註銷用戶,請調用signOut:

迅速

let firebaseAuth = Auth.auth()
do {
  try firebaseAuth.signOut()
} catch let signOutError as NSError {
  print("Error signing out: %@", signOutError)
}

目標-C

NSError *signOutError;
BOOL status = [[FIRAuth auth] signOut:&signOutError];
if (!status) {
  NSLog(@"Error signing out: %@", signOutError);
  return;
}

您可能還想為所有身份驗證錯誤添加錯誤處理代碼。請參閱處理錯誤