通過使用 Firebase SDK 執行端到端 OAuth 2.0 登錄流程,您可以讓用戶使用其 Apple ID 向 Firebase 進行身份驗證。
在你開始之前
要使用 Apple 登錄用戶,請首先在 Apple 的開發者網站上配置 Sign In with Apple,然後將 Apple 啟用為您的 Firebase 項目的登錄提供商。
加入 Apple 開發者計劃
Sign In with Apple 只能由Apple Developer Program的成員配置。
配置使用 Apple 登錄
- 在 Apple 開發者網站的證書、標識符和配置文件頁面上為您的應用啟用 Sign In with Apple。
- 如果您使用 Firebase 身份驗證的任何功能向用戶發送電子郵件,包括電子郵件鏈接登錄、電子郵件地址驗證、帳戶更改撤銷等,請配置 Apple 私人電子郵件中繼服務並註冊
noreply@ YOUR_FIREBASE_PROJECT_ID .firebaseapp.com
(或您自定義的電子郵件模板域),以便 Apple 可以將 Firebase 身份驗證發送的電子郵件中繼到匿名的 Apple 電子郵件地址。
啟用 Apple 作為登錄提供商
- 將 Firebase 添加到您的 Apple 項目中。在 Firebase 控制台中設置應用時,請務必註冊應用的捆綁 ID。
- 在Firebase 控制台中,打開Auth部分。在登錄方法選項卡上,啟用Apple提供程序。如果您僅在應用程序中使用 Sign In with Apple,您可以將服務 ID、Apple 團隊 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
對象:
對於每個登錄請求,生成一個隨機字符串——一個“nonce”——你將使用它來確保你獲得的 ID 令牌是專門為響應你的應用程序的身份驗證請求而授予的。此步驟對於防止重放攻擊很重要。
您可以使用
SecRandomCopyBytes(_:_:_)
生成加密安全的隨機數,如下例所示:迅速
// Adapted from https://auth0.com/docs/api-auth/tutorials/nonce#generate-a-cryptographically-random-nonce private func randomNonceString(length: Int = 32) -> String { precondition(length > 0) let charset: [Character] = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._") var result = "" var remainingLength = length while remainingLength > 0 { let randoms: [UInt8] = (0 ..< 16).map { _ in var random: UInt8 = 0 let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random) if errorCode != errSecSuccess { fatalError( "Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)" ) } return random } randoms.forEach { random in if remainingLength == 0 { return } if random < charset.count { result.append(charset[Int(random)]) remainingLength -= 1 } } } return result }
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; }
啟動 Apple 的登錄流程,在您的請求中包括 nonce 的 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]; }
在您的
ASAuthorizationControllerDelegate
實現中處理 Apple 的響應。如果登錄成功,請使用 Apple 響應中的 ID 令牌和未散列的 nonce 向 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. 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); }
與 Firebase Auth 支持的其他提供商不同,Apple 不提供照片 URL。
此外,當用戶選擇不與應用程序共享電子郵件時,Apple 會為該用戶提供一個唯一的電子郵件地址(格式xyz@privaterelay.appleid.com
),並與您的應用程序共享。如果您配置了私人電子郵件中繼服務,Apple 會將發送到匿名地址的電子郵件轉發到用戶的真實電子郵件地址。
Apple 僅在用戶首次登錄時與應用共享用戶信息,例如顯示名稱。通常,Firebase 會在用戶首次使用 Apple 登錄時存儲顯示名稱,您可以通過Auth.auth().currentUser.displayName
。但是,如果您之前使用 Apple 將用戶登錄到應用程序而不使用 Firebase,Apple 將不會向 Firebase 提供用戶的顯示名稱。
重新認證和帳戶鏈接
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 登錄將不允許您重複使用身份驗證憑據來鏈接到現有帳戶。如果要將 Sign in with Apple 憑據鏈接到另一個帳戶,則必須首先嘗試使用舊的 Sign in with Apple 憑據鏈接帳戶,然後檢查返回的錯誤以查找新憑據。新憑據將位於錯誤的userInfo
字典中,並且可以通過FIRAuthErrorUserInfoUpdatedCredentialKey
鍵訪問。
例如,要將 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.
// ...
}];
下一步
用戶首次登錄後,會創建一個新用戶帳戶並將其鏈接到憑據(即用戶名和密碼、電話號碼或身份驗證提供商信息),即用戶登錄時使用的憑據。這個新帳戶作為 Firebase 項目的一部分存儲,可用於在項目中的每個應用中識別用戶,無論用戶如何登錄。
在您的 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; }
您可能還想為所有身份驗證錯誤添加錯誤處理代碼。請參閱處理錯誤。