iOS で Apple を使用したユーザーのログイン
このドキュメントでは、Identity Platform を使用して iOS アプリに「Apple でログイン」を追加する方法を説明します。
始める前に
Identity Platform を使用する iOS アプリを作成します。
Apple Developer Program に参加します。
Apple によりアプリを構成する
Apple Developer サイトで次の手順を行います。
Identity Platform を使用してユーザーにメールを送信する場合は、Apple のプライベート メールリレー サービスを使用してプロジェクトを構成します。
noreply@project-id.firebaseapp.com
また、アプリにカスタム メール テンプレートがある場合は、それを使用することもできます。
Apple の匿名化データの要件を遵守する
Apple には、メールアドレスを含む自分のデータを匿名化できるオプションがあります。Apple は、このオプションを選択したユーザーに、ドメインが privaterelay.appleid.com
の難読化されたメールアドレスを割り当てます。
アプリは、匿名化された Apple ID に関して、Apple が定めるデベロッパー ポリシーと利用規約を遵守する必要があります。これには、個人を特定できる情報(PII)を匿名化された Apple ID に関連付ける前にユーザーの同意を得ることも含まれます。PII が含まれるアクションには次のようなものがあります(ただし、これらに限定されません)。
- 匿名化された Apple ID にメールアドレスをリンク(またはその逆方向にリンク)する。
- 匿名化された Apple ID に電話番号をリンク(またはその逆方向にリンク)する。
- 匿名化された Apple ID に匿名ではないソーシャル認証情報(Facebook、Google など)をリンク(またはその逆方向にリンク)する。
詳しくは、Apple デベロッパー アカウントの Apple Developer Program License Agreement をご覧ください。
Apple をプロバイダとして構成する
Apple を ID プロバイダとして構成するには:
Google Cloud コンソールで [ID プロバイダ] ページに移動します。
[プロバイダを追加] をクリックします。
リストから [Apple] を選択します。
[プラットフォーム] で [iOS] を選択します。
アプリのバンドル ID を入力します。
[承認済みドメイン] で [ドメインを追加] をクリックして、アプリのドメインを登録します。開発目的の場合は、
localhost
はデフォルトですでに有効になっています。[アプリケーションの構成] で [iOS] をクリックします。このスニペットをアプリのコードにコピーして、Identity Platform Client SDK を初期化します。
[保存] をクリックします。
Client SDK を使用したユーザーのログイン
ユーザーをログインさせ、Apple の AuthenticationServices フレームワークを使用して ID トークンを取得します。
SecRandomCopyBytes(_:_:_:)
を呼び出して、ノンスと呼ばれるランダムな文字列を生成します。ノンスは、リプレイ攻撃を防ぐために使用されます。認証リクエストにノンスの SHA-256 ハッシュを含めると、Apple はレスポンスで変更せずに返します。その後、Identity Platform は、元のハッシュを Apple から返された値と比較することで、レスポンスを検証します。
前の手順で作成したノンスの SHA-256 ハッシュや、Apple のレスポンスを処理するデリゲート クラスを含む、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 トークンと、ハッシュ化されていないノンスを組み合わせて使用して、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); }
他の多くの ID プロバイダとは異なり、Apple では写真の URL が提供されません。
ユーザーがアプリと実際のメールの共有を行わない場合、Apple は代わりにそのユーザーに固有のメールアドレスをプロビジョニングして共有します。このメールの形式は xyz@privaterelay.appleid.com
です。プライベート メールリレー サービスを構成した場合、Apple は匿名化されたアドレスに送信されたメールを、ユーザーの実際のメールアドレスに転送します。
Apple が表示名などのユーザー情報をアプリと共有するのは、ユーザーの初回ログイン時のみです。ほとんどの場合、Identity Platform がこのデータを格納しており、その後のセッションでは firebase.auth().currentUser.displayName
を使用してそれを取得できます。ただし、Identity Platform と統合する前に、ユーザーが Apple を使用してアプリにログインできるようにした場合、ユーザー情報は利用できません。
ユーザー アカウントの削除
Apple では、アカウント作成をサポートする iOS アプリには、ユーザーがアプリ内から自分のアカウントの削除を開始できるようにすることを求めています。
ユーザー アカウントを削除する場合は、ユーザーのアカウントを削除する前にユーザーのトークンを取り消す必要があり、Firestore、Cloud Storage、Firebase Realtime Database に保存したすべてのデータも削除する必要があります。詳細については、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 ユーザーの詳細について確認する。
- 他の ID プロバイダでユーザーのログインを行う。