iOS で Apple を使用したユーザーのログイン

このドキュメントでは、Identity Platform を使用して iOS アプリに「Apple でログイン」を追加する方法を説明します。

始める前に

  • Identity Platform を使用する iOS アプリを作成します。

  • Apple Developer Program に参加します。

Apple によりアプリを構成する

Apple Developer サイトで次の手順を行います。

  1. アプリの「Apple でログイン」機能を有効にします

  2. 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 プロバイダとして構成するには:

  1. Google Cloud コンソールで [ID プロバイダ] ページに移動します。

    [ID プロバイダ] ページに移動

  2. [プロバイダを追加] をクリックします。

  3. リストから [Apple] を選択します。

  4. [プラットフォーム] で [iOS] を選択します。

  5. アプリのバンドル ID を入力します。

  6. [承認済みドメイン] で [ドメインを追加] をクリックして、アプリのドメインを登録します。開発目的の場合は、localhost はデフォルトですでに有効になっています。

  7. [アプリケーションの構成] で [iOS] をクリックします。このスニペットをアプリのコードにコピーして、Identity Platform Client SDK を初期化します。

  8. [保存] をクリックします。

Client SDK を使用したユーザーのログイン

  1. ユーザーをログインさせ、Apple の AuthenticationServices フレームワークを使用して ID トークンを取得します。

  2. SecRandomCopyBytes(_:_:_:) を呼び出して、ノンスと呼ばれるランダムな文字列を生成します。

    ノンスは、リプレイ攻撃を防ぐために使用されます。認証リクエストにノンスの SHA-256 ハッシュを含めると、Apple はレスポンスで変更せずに返します。その後、Identity Platform は、元のハッシュを Apple から返された値と比較することで、レスポンスを検証します。

  3. 前の手順で作成したノンスの 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;
    }
    
  4. 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){
                                        // ...
                                    }];
                                  }
                                }];
    }
  }
}

次のステップ