使用 Apple 進行身份驗證

您可以使用 Firebase SDK 執行端對端 OAuth 2.0 登入流程,讓使用者使用其 Apple ID 透過 Firebase 進行身份驗證。

在你開始之前

若要使用 Apple 登入用戶,請先在 Apple 開發者網站上設定 Sign In with Apple,然後啟用 Apple 作為 Firebase 專案的登入提供者。

加入蘋果開發者計劃

「使用 Apple 登入」只能由Apple 開發者計畫的成員進行設定。

配置使用 Apple 登入

  1. 在 Apple 開發者網站的「憑證、識別碼和設定檔」頁面上為您的應用程式啟用「使用 Apple 登入」。
  2. 依照設定使用 Apple 網頁版登入的第一部分所述將您的網站與應用程式相關聯。出現提示時,將下列 URL 註冊為返回 URL:
    https://YOUR_FIREBASE_PROJECT_ID.firebaseapp.com/__/auth/handler
    您可以在Firebase 控制台設定頁面上取得您的 Firebase 專案 ID。完成後,記下新的服務 ID,下一部分將需要它。
  3. 建立使用 Apple 私鑰登入。在下一部分中,您將需要新的私鑰和金鑰 ID。
  4. 如果您使用 Firebase 驗證的任何向使用者發送電子郵件的功能(包括電子郵件連結登入、電子郵件地址驗證、帳戶變更撤銷等),請設定 Apple 私人電子郵件中繼服務並註冊noreply@ YOUR_FIREBASE_PROJECT_ID .firebaseapp.com (或您的自訂電子郵件範本網域),以便 Apple 可以將 Firebase 驗證傳送的電子郵件轉送至匿名 Apple 電子郵件地址。

啟用 Apple 作為登入供應商

  1. 將 Firebase 新增到您的 Apple 專案。在 Firebase 控制台中設定應用程式時,請務必註冊已套用的捆綁包 ID。
  2. Firebase 控制台中,開啟「驗證」部分。在「登入方法」標籤上,啟用Apple提供者。指定您在上一部分中建立的服務 ID。另外,在OAuth 程式碼流配置部分中,指定您的 Apple 團隊 ID 以及您在上一部分中建立的私鑰和金鑰 ID。

遵守 Apple 匿名資料要求

「使用 Apple 登入」為使用者提供了在登入時對其資料(包括電子郵件地址)進行匿名化的選項。選擇此選項的使用者的電子郵件地址的網域為privaterelay.appleid.com 。當您在應用程式中使用「透過 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)
    }
    
        

    Objective-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];
    }
        

    您將隨登入請求一起傳送隨機數的 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
    }
    
        

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

    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];
    }
    
  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)")
      }
    
    }
    

    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, 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.
  // ...
}

Objective-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 登入將不允許您重複使用身份驗證憑證來連結到現有帳戶。如果您想要將「使用 Apple 登入」憑證連結到另一個帳戶,則必須先嘗試使用舊的「使用 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.
  // ...
}

Objective-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 要求支援帳戶建立的應用程式必須允許使用者在應用程式內發起刪除其帳戶的操作,如應用程式商店審查指南中所述

為了滿足此要求,請執行下列步驟:

  1. 確保填寫了「使用 Apple 登入」提供者配置的「服務 ID」「OAuth 代碼流配置」部分,如「配置使用 Apple 登入」部分所述。

  2. 由於使用「透過 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 即時資料庫和雲端儲存安全性規則中,您可以從auth變數取得登入使用者的唯一使用者 ID,並使用它來控制使用者可以存取哪些資料。

您可以透過將身分驗證提供者憑證連結到現有使用者帳戶,允許使用者使用多個驗證提供者登入您的應用程式。

若要登出用戶,請呼叫signOut:

迅速

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

Objective-C

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

您可能還需要為所有身份驗證錯誤新增錯誤處理代碼。請參閱處理錯誤