透過 Apple 驗證

您可根據 Firebase SDK 執行端對端 OAuth 2.0 登入流程,讓使用者以 Apple ID 向 Firebase 進行驗證。

事前準備

如要使用 Apple 登入使用者,請先在 Apple 的開發人員網站上設定「使用 Apple 帳戶登入」,然後啟用 Apple 做為 Firebase 專案的登入供應商。

加入 Apple Developer Program

只有 Apple Developer Program 成員才能設定「使用 Apple 登入」。

設定「使用 Apple 登入」功能

  1. 在 Apple 開發人員網站的「Certificates, Identifiers & Profiles」(憑證、ID 與設定檔) 頁面中為您的應用程式啟用 Apple 登入功能。
  2. 按照為網頁版設定使用 Apple 帳戶登入第一節的說明,為網站與應用程式建立關聯。系統提示時,請將下列網址註冊為返回網址:
    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(_:_:_) 產生經過加密的安全 Nonce,如以下範例所示:

    Swift

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

    您會在登入要求時傳送 Nonce 的 SHA256 雜湊,Apple 會在回應中傳遞此雜湊。Firebase 會對原始 Nonce 值進行雜湊處理,並與 Apple 傳送的值進行比較,藉此驗證回應。

    Swift

    @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 的登入流程,包括您要求中的 Nonce SHA256 雜湊,以及處理 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()
    }
    

    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 憑證搭配未經雜湊處理的 Nonce 進行驗證:

    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, 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 驗證支援的其他供應商,Apple 不提供相片網址。

此外,如果使用者選擇不與應用程式共用電子郵件地址,Apple 會為該使用者佈建專屬電子郵件地址 (採用 xyz@privaterelay.appleid.com 格式),並將該電子郵件地址與應用程式共用。如果您設定了私人電子郵件轉發服務,Apple 會將傳送至匿名電子郵件地址的電子郵件轉寄至使用者的實際電子郵件地址。

重新驗證和帳戶連結

相同的模式可與 reauthenticateWithCredential() 搭配使用,針對需要近期登入的敏感作業擷取新憑證:

Swift

// 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 時取得的存取權杖:

Swift

// 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 規定,如果應用程式支援建立帳戶,必須允許使用者在應用程式中自行刪除帳戶,如 App Store 審查指南所述。

為符合這項規定,請實作下列步驟:

  1. 請務必按照設定「使用 Apple 帳戶登入」一節所述,填寫「使用 Apple 登入」設定的「服務 ID」和「OAuth 程式碼流程設定」部分。

  2. 當使用者透過 Apple 帳戶登入時,Firebase 不會儲存使用者權杖,因此您必須要求使用者再次登入,才能撤銷權杖並刪除帳戶。

    Swift

    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:) 來撤銷使用者的權杖。

    Swift

    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 即時資料庫和 Cloud Storage 安全性規則中,您可以透過 auth 變數取得登入使用者的專屬 ID,並使用該 ID 控管使用者可存取哪些資料。

您可以將驗證供應商憑證連結至現有的使用者帳戶,讓使用者透過多個驗證服務提供者登入您的應用程式。

如要登出使用者,請呼叫 signOut:

Swift

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

您可能也想要針對各種驗證錯誤加入錯誤處理程式碼。請參閱處理錯誤