在 iOS 上透過 Apple 登入使用者
本文說明如何使用 Identity Platform,在 iOS 應用程式中加入「使用 Apple 登入」功能。
事前準備
建立使用 Identity Platform 的 iOS 應用程式。
透過 Apple 設定應用程式
在 Apple Developer 網站上:
為應用程式啟用「使用 Apple 帳戶登入」功能。
如果您使用 Identity Platform 傳送電子郵件給使用者,請使用以下電子郵件透過 Apple 的私人電子郵件轉送服務設定專案:
noreply@project-id.firebaseapp.com
如果您的應用程式有自訂電子郵件範本,您也可以使用該範本。
遵守 Apple 的去識別化資料規定
Apple 提供使用者選擇是否要匿名處理資料的選項,包括電子郵件地址。Apple 會為選擇這個選項的使用者指派使用網域 privaterelay.appleid.com
的模糊處理電子郵件地址。
您的應用程式必須遵守 Apple 針對匿名 Apple ID 所發布的任何適用開發人員政策或條款。這包括在將任何個人識別資訊 (PII) 與匿名 Apple ID 建立關聯前,先徵得使用者同意。涉及個人識別資訊的動作包括但不限於:
- 將電子郵件地址連結至匿名 Apple ID,或反之。
- 將電話號碼連結至匿名 Apple ID,或將匿名 Apple ID 連結至電話號碼
- 將非匿名社群媒體憑證 (例如 Facebook 或 Google) 連結至匿名 Apple ID,或反之。
如需更多資訊,請參閱 Apple 開發人員帳戶的《Apple Developer Program License Agreement》。
將 Apple 設為供應商
如要將 Apple 設為識別資訊提供者,請按照下列步驟操作:
前往 Google Cloud 控制台的「Identity Providers」頁面。
按一下「Add A Provider」。
從清單中選取「Apple」。
在「平台」下方,選取「iOS」。
輸入應用程式的套件 ID。
按一下「Authorized Domains」下方的「Add domain」,即可註冊應用程式的網域。為方便開發,
localhost
已預設為啟用。按一下「設定應用程式」下方的「iOS」。將程式碼片段複製到應用程式的程式碼中,即可初始化 Identity Platform 用戶端 SDK。
按一下 [儲存]。
使用用戶端 SDK 登入使用者
使用 Apple 的Authentication Services 架構登入使用者,並取得 ID 權杖。
呼叫
SecRandomCopyBytes(_:_:_:)
產生隨機字串 (稱為「Nonce」)。這個值可用來防範重送攻擊。您可以在驗證要求中加入 Nonce 的 SHA-256 雜湊,Apple 會在回應中原封不動地傳回該雜湊。接著,Identity Platform 會將原始雜湊與 Apple 傳回的值進行比較,驗證回應。
開始 Apple 的登入流程,包括您在前一個步驟中建立的 Nonce 的 SHA-256 雜湊,以及用於處理 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() } @available(iOS 13, *) private func sha256(_ input: String) -> String { let inputData = Data(input.utf8) let hashedData = SHA256.hash(data: inputData) let hashString = hashedData.compactMap { return String(format: "%02x", $0) }.joined() return hashString }
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]; } - (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; }
在實作
ASAuthorizationControllerDelegate
時處理 Apple 的回應。如果登入成功,請使用 Apple 回應中的 ID 權杖和未經雜湊的 Nonce,透過 Identity Platform 進行驗證: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. 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); }
與許多其他身分提供者不同,Apple 不會提供相片網址。
如果使用者選擇不與您的應用程式分享真實電子郵件地址,Apple 會為該使用者提供專屬電子郵件地址,供其使用。這封電子郵件的格式為 xyz@privaterelay.appleid.com
。如果您已設定私人電子郵件轉送服務,Apple 會將寄到匿名地址的電子郵件轉寄至使用者的實際電子郵件地址。
Apple 只會在使用者首次登入時,將使用者資訊 (例如顯示名稱) 提供給應用程式。在大多數情況下,Identity Platform 會儲存這類資料,讓您在日後的工作階段中使用 firebase.auth().currentUser.displayName
擷取資料。不過,如果您在整合 Identity Platform 之前,就允許使用者使用 Apple 登入應用程式,就無法取得使用者資訊。
刪除使用者帳戶
Apple 規定,支援帳戶建立功能的 iOS 應用程式也必須讓使用者在應用程式中啟動帳戶刪除程序。
刪除使用者帳戶時,您必須先撤銷使用者的權杖,再刪除使用者帳戶,以及您在 Firestore、Cloud Storage 和 Firebase 即時資料庫中為使用者儲存的所有資料。詳情請參閱 Apple 開發人員支援說明文件中的「在應用程式中提供帳戶刪除功能」。
使用者透過 Apple 登入建立帳戶時,Identity Platform 不會儲存使用者權杖,因此您必須先要求使用者登入,才能撤銷權杖並刪除帳戶。或者,如果使用者已透過 Apple 登入,您可以儲存授權碼,以便在撤銷權杖時重複使用,避免要求使用者再次登入。
如要撤銷使用者的權杖並刪除其帳戶,請執行以下操作:
Swift
let user = Auth.auth().currentUser
// Check if the user has a token.
if let providerData = user?.providerData {
for provider in providerData {
guard let provider = provider as? FIRUserInfo else {
continue
}
if provider.providerID() == "apple.com" {
isAppleProviderLinked = true
}
}
}
// Re-authenticate the user and revoke their token
if isAppleProviderLinked {
let request = appleIDRequest(withState: "revokeAppleTokenAndDeleteUser")
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
} else {
// Usual user deletion
}
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
) {
if authorization.credential is ASAuthorizationAppleIDCredential {
let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
if authorization.credential is ASAuthorizationAppleIDCredential {
if appleIDCredential.state == "signIn" {
// Sign in with Firebase.
// ...
} else if appleIDCredential.state == "revokeAppleTokenAndDeleteUser" {
// Revoke token with Firebase.
Auth.auth().revokeTokenWithAuthorizationCode(code) { error in
if error != nil {
// Token revocation failed.
} else {
// Token revocation succeeded then delete user again.
let user = Auth.auth().currentUser
user?.delete { error in
// ...
}
}
}
}
}
}
}
Objective-C
FIRUser *user = [FIRAuth auth].currentUser;
// Check if the user has a token.
BOOL isAppleProviderLinked = false;
for (id<FIRUserInfo> provider in user.providerData) {
if ([[provider providerID] isEqual:@"apple.com"]) {
isAppleProviderLinked = true;
}
}
// Re-authenticate the user and revoke their token
if (isAppleProviderLinked) {
if (@available(iOS 13, *)) {
ASAuthorizationAppleIDRequest *request =
[self appleIDRequestWithState:@"revokeAppleTokenAndDeleteUser"];
ASAuthorizationController *controller = [[ASAuthorizationController alloc]
initWithAuthorizationRequests:@[ request ]];
controller.delegate = self;
controller.presentationContextProvider = self;
[controller performRequests];
}
} else {
// Usual user deletion
}
- (void)authorizationController:(ASAuthorizationController *)controller
didCompleteWithAuthorization:(ASAuthorization *)authorization
API_AVAILABLE(ios(13.0)) {
if ([authorization.credential
isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
ASAuthorizationAppleIDCredential *appleIDCredential =
authorization.credential;
if ([appleIDCredential.state isEqualToString:@"signIn"]) {
// Sign in with Firebase.
// ...
} else if ([appleIDCredential.state
isEqualToString:@"revokeAppleTokenAndDeleteUser"]) {
// Revoke token with Firebase.
NSString *code =
[[NSString alloc] initWithData:appleIDCredential.authorizationCode
encoding:NSUTF8StringEncoding];
[[FIRAuth auth]
revokeTokenWithAuthorizationCode:code
completion:^(NSError *_Nullable error) {
if (error != nil) {
// Token revocation failed.
} else {
// Token revocation succeeded then delete
// user again.
FIRUser *user = [FIRAuth auth].currentUser;
[user deleteWithCompletion:^(
NSError *_Nullable error){
// ...
}];
}
}];
}
}
}
後續步驟
- 進一步瞭解 Identity Platform 使用者。
- 使用其他身分識別提供者登入使用者。