透過 Apple 驗證

您可以讓使用者透過 Apple ID 向 Firebase 進行驗證,步驟如下: 並使用 Firebase SDK 執行端對端 OAuth 2.0 登入流程。

事前準備

如要使用 Apple 登入使用者,請先設定「使用 Apple 帳戶登入」 然後啟用 Apple 做為您的登入服務供應商, Firebase 專案。

加入 Apple Developer Program

只有 Apple Developer 成員可以設定「使用 Apple 帳戶登入」 計畫

設定「使用 Apple 登入」功能

  1. 為您的應用程式啟用「使用 Apple 登入」功能: 憑證、ID 與設定檔 頁面。
  2. 按照第一節的說明,為您的網站與應用程式建立關聯 「為網頁版設定使用 Apple 帳戶登入」一文。系統提示時,請註冊 將下列網址做為傳回網址:
    https://YOUR_FIREBASE_PROJECT_ID.firebaseapp.com/__/auth/handler
    如要取得 Firebase 專案 ID,請前往 Firebase 控制台設定頁面。 完成後,請記下新的服務 ID,以備不時之需 請參閱下一節的說明。
  3. 建立 使用 Apple 私密金鑰登入。您需要新的私密金鑰和金鑰 ID。
  4. 如果您使用的 Firebase 驗證功能會傳送電子郵件給使用者 包括電子郵件連結登入、電子郵件地址驗證、帳戶變更 撤銷 其他,請設定 Apple 私人電子郵件轉發服務 並註冊 noreply@YOUR_FIREBASE_PROJECT_ID.firebaseapp.com (或您的自訂電子郵件範本網域),讓 Apple 可以轉發傳送的電子郵件 因為 Firebase 驗證取得匿名的 Apple 電子郵件地址。

啟用 Apple 做為登入服務供應商

  1. 將 Firebase 新增至 Apple 專案。請務必 註冊應用程式的軟體包 ID,方法是在 Firebase 控制台。
  2. Firebase 控制台開啟「驗證」專區。在「Sign in method」分頁中: 啟用 Apple 供應商。 指定您在上一節建立的服務 ID。此外,在 OAuth 程式碼流程設定部分,指定您的 Apple Team ID,然後 私密金鑰和金鑰 ID

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

「使用 Apple 帳戶登入」功能可讓使用者選擇將資料去識別化、 包括電子郵件地址選擇這個選項的使用者 擁有 privaterelay.appleid.com 網域的電子郵件地址。時間 如果您在應用程式中使用「使用 Apple 帳戶登入」功能,您必須遵守所有適用的 有關這些匿名 Apple 的開發人員政策或條款 而非客戶 ID

包括事先取得任何必要的使用者同意聲明 將任何直接識別的個人資訊與去識別化的 Apple 建立關聯 編號。使用 Firebase 驗證時,這可能包括: 動作:

  • 將電子郵件地址連結至去識別化的 Apple ID。
  • 將電話號碼與去識別化的 Apple ID 連結
  • 將非匿名的社會憑證 (Facebook、Google 等) 連結至 或匿名化 Apple ID。

請注意,上方清單僅列出部分示例。參閱 Apple Developer Program 《授權協議》 確認您的應用程式符合 Apple 的規定。

使用 Apple 帳戶登入並使用 Firebase 進行驗證

如要使用 Apple 帳戶進行驗證,請先讓使用者登入 Apple 採用 Apple 的 AuthenticationServices 架構的帳戶, 然後使用 Apple 回應中的 ID 權杖建立 Firebase AuthCredential 物件:

  1. 針對每個登入要求產生隨機字串— 「nonce」,您可以用來確保取得的 ID 權杖 。這個 步驟非常重要,即可預防重送攻擊

    您可以使用下列指令產生經過加密的安全 Nonce SecRandomCopyBytes(_:_:_),如以下範例所示:

    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 和負責處理 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。如果登入成功,請使用 ID 這個符記應用於 Firebase:

    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 Auth 支援的其他供應商,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. 確認已填寫「服務 ID」和「OAuth 程式碼流程設定」 一節,一如 設定「使用 Apple 帳戶登入」部分。

  2. 由於 Firebase 不會儲存使用者權杖, 透過 Apple 帳戶登入,您必須先要求使用者重新登入才能撤銷 權杖並刪除帳戶

    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。 控管使用者可以存取的資料

您可以讓使用者透過多重驗證機制登入您的應用程式 將驗證供應商憑證連結至 現有的使用者帳戶

如要登出使用者,請呼叫 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;
}

我們也建議您新增錯誤處理程式碼,適用於完整的驗證範圍 發生錯誤。請參閱處理錯誤