Apple を使用して認証する

Firebase SDK を使用してエンドツーエンドの OAuth 2.0 ログインフローを実行すると、ユーザーが Firebase での認証に Apple ID を使用できるようになります。

始める前に

ユーザーが Apple を使用してログインできるようにするには、まず Apple のデベロッパー サイトで「Apple でサインイン」を構成してから、Firebase プロジェクトのログイン プロバイダとして Apple を有効にします。

Apple Developer Program に参加する

「Apple でサインイン」は Apple Developer Program のメンバーのみが構成できます。

「Apple でサインイン」を構成する

  1. Apple のデベロッパー サイトの [Certificates, Identifiers & Profiles] ページでアプリの「Apple でサインイン」を有効にします。
  2. Web 向けに「Apple でサインイン」を構成するの最初のセクションの説明に沿って、ウェブサイトをアプリに関連付けます。プロンプトが表示されたら、次の URL を戻り URL として登録します。
    https://YOUR_FIREBASE_PROJECT_ID.firebaseapp.com/__/auth/handler
    Firebase プロジェクト ID は Firebase コンソールの設定ページで確認できます。 作業が完了したら、新しいサービス ID をメモしておきます。この ID は次のセクションで必要になります。
  3. 「Apple でサインイン」の秘密鍵を作成します。新しい秘密鍵と鍵 ID は、次のセクションで必要になります。
  4. メールリンク ログイン、メールアドレスの確認、アカウントの変更の取り消しなど、ユーザーにメールを送信する Firebase Authentication の機能を使用する場合は、Apple のプライベート メール リレーサービスを構成し、noreply@YOUR_FIREBASE_PROJECT_ID.firebaseapp.com(またはカスタマイズしたメール テンプレート ドメイン)を登録します。これにより Apple は、Firebase Authentication によって送信されたメールを、匿名化された Apple のメールアドレスに転送できます。

Apple をログイン プロバイダとして有効にする

  1. Apple プロジェクトに Firebase を追加します。Firebase コンソールでアプリを設定する際は、必ずアプリのバンドル ID を登録してください。
  2. Firebase コンソールで [Authentication] セクションを開きます。[Sign-in method] タブで、[Apple] プロバイダを有効にします。前のセクションで作成したサービス ID を指定します。また、[OAuth コードフローの構成] セクションで、Apple チーム ID、前のセクションで作成した秘密鍵と鍵 ID を指定します。

Apple の匿名化データの要件を遵守する

「Apple でサインイン」には、ユーザーがログイン時に、メールアドレスを含む自分のデータを匿名化できるオプションがあります。このオプションを選択したユーザーには、ドメイン privaterelay.appleid.com のメールアドレスが作成されます。アプリで「Apple でサインイン」を使用する場合は、これらの匿名化された Apple ID に関して、Apple が定めるデベロッパー ポリシーと利用規約を遵守する必要があります。

これには、本人を直接特定できる個人情報を、匿名化された Apple ID に関連付ける前に、必要なユーザーの同意を得ることも含まれます。Firebase Authentication を使用する場合、この関連付けには、次のアクションが該当することがあります。

  • 匿名化された Apple ID にメールアドレスをリンク(またはその逆方向にリンク)する。
  • 匿名化された Apple ID に電話番号をリンク(またはその逆方向にリンク)する。
  • 匿名化された Apple ID に匿名ではないソーシャル認証情報(Facebook、Google など)をリンク(またはその逆方向にリンク)する。

上記のリストはすべてを網羅しているわけではありません。アプリが Apple の要件を満たしていることを確認するには、デベロッパー アカウントの [Membership] セクションにある Apple Developer Program License Agreement をご覧ください。

「Apple でサインイン」して Firebase で認証する

Apple アカウントで認証するには、まず Apple の AuthenticationServices フレームワークを使用して Apple アカウントでユーザーをログインさせます。続いて、Apple のレスポンスの ID トークンを使用して Firebase の AuthCredential オブジェクトを作成します。

  1. ログイン リクエストごとにランダムな文字列「ナンス」を生成します。ナンスは、取得した ID トークンが、当該アプリの認証リクエストへのレスポンスとして付与されたことを確認するために使用します。この手順は、リプレイ攻撃の防止に重要です。

    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];
    }
        

    ログイン リクエストでナンスの SHA256 ハッシュを送信します。Apple は、変更を加えることなく、レスポンスでこのナンスを渡します。Firebase では、元のノンスをハッシュ化し、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 のログインフローを開始します。リクエストには、ナンスの SHA256 ハッシュと、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 の実装で Apple のレスポンスを処理します。ログインが正常に完了したら、Apple のレスポンスの 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 では写真の URL が提供されません。

また、ユーザーがアプリとメールの共有を行わない場合、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() を使用して、複数の ID プロバイダを既存のアカウントにリンクすることができます。

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.
  // ...
}];

トークン失効

App Store のレビュー ガイドラインで説明されているように、Apple は、アカウントの作成をサポートするアプリにおいて、ユーザーがアプリ内でアカウントの削除を開始できるようにすることを求めています。

この要件を満たすには、次の手順を行います。

  1. 「Apple でサインイン」を構成するセクションで説明されているとおり、「Apple でサインイン」プロバイダ構成のサービス ID と OAuth コードフロー構成セクションを入力します。

  2. 「Apple でサインイン」を使用してユーザーを作成した場合、Firebase はユーザー トークンを保存しないため、トークンを取り消してアカウントを削除する前に、ユーザーに再度ログインするように求める必要があります。

    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 Realtime Database と Cloud Storage のセキュリティ ルールでは、ログイン済みユーザーの一意のユーザー ID を auth 変数から取得し、それを使用して、ユーザーがアクセスできるデータを管理できます。

既存のユーザー アカウントに認証プロバイダの認証情報をリンクすることで、ユーザーは複数の認証プロバイダを使用してアプリにログインできるようになります。

ユーザーのログアウトを行うには、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;
}

さまざまな認証エラーに対応できるようにエラー処理コードを追加することもできます。エラーの処理をご覧ください。