Android で Apple を使用して認証する

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

始める前に

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

Apple Developer Program に参加する

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

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

Apple Developer サイトでの手順は次のとおりです。

  1. Web 向けに「Apple でサインイン」を構成するの最初のセクションの説明に沿って、ウェブサイトをアプリに関連付けます。プロンプトが表示されたら、次の URL を戻り先 URL として登録します。

    https://YOUR_FIREBASE_PROJECT_ID.firebaseapp.com/__/auth/handler

    Firebase プロジェクト ID は、Firebase コンソールの [プロジェクトの設定] ページで確認できます。

    作業が完了したら、新しいサービス ID をメモしておきます。この ID は次のセクションで必要になります。

  2. 「Apple でサインイン」の秘密鍵を作成します。新しい秘密鍵と鍵 ID は、次のセクションで必要になります。
  3. メールリンク ログイン、メールアドレスの確認、アカウントの変更の取り消しなど、ユーザーにメールを送信する Firebase Authentication の機能を使用する場合は、Apple のプライベート メール リレーサービスを構成し、noreply@YOUR_FIREBASE_PROJECT_ID.firebaseapp.com(またはカスタマイズしたメール テンプレート ドメイン)を登録します。これにより Apple は、Firebase Authentication によって送信されたメールを、匿名化された Apple のメールアドレスに転送できます。

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

  1. Firebase を Android プロジェクトに追加しますFirebase コンソールでアプリを設定する際は、必ずアプリの SHA-1 署名を登録してください。
  2. Firebase コンソールで [Auth] セクションを開きます。 [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 をご覧ください。

Firebase SDK を使用したログインフローの処理

Android では、Apple アカウントを使用して Firebase でユーザーを認証する最も簡単な方法は、Firebase Android SDK でログインフロー全体を処理することです。

Firebase Android SDK でログインフローを処理する手順は次のとおりです。

  1. プロバイダ ID apple.com の Builder を使用して、OAuthProvider のインスタンスを構築します。

    Kotlin

    val provider = OAuthProvider.newBuilder("apple.com")
    

    Java

    OAuthProvider.Builder provider = OAuthProvider.newBuilder("apple.com");
    
  2. 省略可: 認証プロバイダにリクエストする、デフォルトを超える追加の OAuth 2.0 スコープを指定します。

    Kotlin

    provider.setScopes(arrayOf("email", "name"))
    

    Java

    List<String> scopes =
        new ArrayList<String>() {
          {
            add("email");
            add("name");
          }
        };
    provider.setScopes(scopes);
    

    デフォルトでは、[1 つのメールアドレスにつき 1 つのアカウント] が有効である場合、Firebase ではメールアドレスと名前のスコープをリクエストします。この設定を [1 つのメールアドレスにつき複数のアカウント] に変更すると、Firebase は Apple にスコープをリクエストしません(指定した場合を除きます)。

  3. 省略可: Apple のログイン画面を英語以外の言語で表示する場合は、locale パラメータを設定します。サポートされる言語 / 地域については、「Apple でサインイン」に関するドキュメントをご覧ください。

    Kotlin

    // Localize the Apple authentication screen in French.
    provider.addCustomParameter("locale", "fr")
    

    Java

    // Localize the Apple authentication screen in French.
    provider.addCustomParameter("locale", "fr");
    
  4. OAuth プロバイダ オブジェクトを使用して Firebase での認証を行います。こうすると、他の FirebaseAuth オペレーションとは異なり、Chrome カスタムタブを開いて UI を制御することになります。その結果、アタッチした OnSuccessListenerOnFailureListener は UI が起動されるとすぐにデタッチされるため、そこからアクティビティを参照しないでください。

    最初に、レスポンスをすでに受け取っているかどうかを確認してください。この方法でログインすると、アクティビティがバックグラウンドになるため、ログインフロー中にシステムがそのアクティビティを再要求する可能性があります。こうした場合でもユーザーが再試行しないように、結果がすでに存在するかどうかを確認する必要があります。

    保留中の結果があるかどうかを確認するには、getPendingAuthResult() を呼び出します。

    Kotlin

    val pending = auth.pendingAuthResult
    if (pending != null) {
        pending.addOnSuccessListener { authResult ->
            Log.d(TAG, "checkPending:onSuccess:$authResult")
            // Get the user profile with authResult.getUser() and
            // authResult.getAdditionalUserInfo(), and the ID
            // token from Apple with authResult.getCredential().
        }.addOnFailureListener { e ->
            Log.w(TAG, "checkPending:onFailure", e)
        }
    } else {
        Log.d(TAG, "pending: null")
    }
    

    Java

    mAuth = FirebaseAuth.getInstance();
    Task<AuthResult> pending = mAuth.getPendingAuthResult();
    if (pending != null) {
        pending.addOnSuccessListener(new OnSuccessListener<AuthResult>() {
            @Override
            public void onSuccess(AuthResult authResult) {
                Log.d(TAG, "checkPending:onSuccess:" + authResult);
                // Get the user profile with authResult.getUser() and
                // authResult.getAdditionalUserInfo(), and the ID
                // token from Apple with authResult.getCredential().
            }
        }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                Log.w(TAG, "checkPending:onFailure", e);
            }
        });
    } else {
        Log.d(TAG, "pending: null");
    }
    

    保留中の結果がない場合は、startActivityForSignInWithProvider() を呼び出してログインフローを開始します。

    Kotlin

    auth.startActivityForSignInWithProvider(this, provider.build())
            .addOnSuccessListener { authResult ->
                // Sign-in successful!
                Log.d(TAG, "activitySignIn:onSuccess:${authResult.user}")
                val user = authResult.user
                // ...
            }
            .addOnFailureListener { e ->
                Log.w(TAG, "activitySignIn:onFailure", e)
            }
    

    Java

    mAuth.startActivityForSignInWithProvider(this, provider.build())
            .addOnSuccessListener(
                    new OnSuccessListener<AuthResult>() {
                        @Override
                        public void onSuccess(AuthResult authResult) {
                            // Sign-in successful!
                            Log.d(TAG, "activitySignIn:onSuccess:" + authResult.getUser());
                            FirebaseUser user = authResult.getUser();
                            // ...
                        }
                    })
            .addOnFailureListener(
                    new OnFailureListener() {
                        @Override
                        public void onFailure(@NonNull Exception e) {
                            Log.w(TAG, "activitySignIn:onFailure", e);
                        }
                    });
    

    Firebase Auth でサポートされている他のプロバイダとは異なり、Apple では写真の URL が提供されません。

    また、ユーザーがアプリとメールの共有を行わない場合、Apple はそのユーザーに固有のメールアドレス(xyz@privaterelay.appleid.com の形式)をプロビジョニングし、これがアプリと共有されます。プライベート メールリレー サービスを構成した場合、Apple は、匿名化されたアドレスに送信されたメールを、ユーザーの実際のメールアドレスに転送します。

    Apple が表示名などのユーザー情報をアプリと共有するのは、ユーザーの初回ログイン時のみです。通常、ユーザーが初めて Apple でログインしたときに Firebase で表示名が保存されます。この情報は getCurrentUser().getDisplayName() で取得できます。ただし、以前 Apple を使用してアプリへのユーザーのログインを行ったが、この際に Firebase を使用しなかった場合、Apple はユーザーの表示名を Firebase に提供しません。

再認証とアカウントのリンク

同じパターンを startActivityForReauthenticateWithProvider() でも使用できます。これは、ログインしてから短時間のうちに行うべき機密性の高い操作のために、最新の認証情報を取得するのに使用できます。

Kotlin

// The user is already signed-in.
val firebaseUser = auth.getCurrentUser()

firebaseUser
    .startActivityForReauthenticateWithProvider(/* activity= */ this, provider.build())
    .addOnSuccessListener( authResult -> {
        // User is re-authenticated with fresh tokens and
        // should be able to perform sensitive operations
        // like account deletion and email or password
        // update.
    })
    .addOnFailureListener( e -> {
        // Handle failure.
    })

Java

// The user is already signed-in.
FirebaseUser firebaseUser = mAuth.getCurrentUser();

firebaseUser
    .startActivityForReauthenticateWithProvider(/* activity= */ this, provider.build())
    .addOnSuccessListener(
        new OnSuccessListener<AuthResult>() {
          @Override
          public void onSuccess(AuthResult authResult) {
            // User is re-authenticated with fresh tokens and
            // should be able to perform sensitive operations
            // like account deletion and email or password
            // update.
          }
        })
    .addOnFailureListener(
        new OnFailureListener() {
          @Override
          public void onFailure(@NonNull Exception e) {
            // Handle failure.
          }
        });

また、linkWithCredential() を使用して、複数の ID プロバイダを既存のアカウントにリンクできます。

Apple は、Apple アカウントを他のデータにリンクする前にユーザーから明示的な同意を得ることを要件としています。

たとえば、Facebook アカウントを現在の Firebase アカウントにリンクするには、Facebook へのユーザーのログイン時に取得したアクセス トークンを使用します。

Kotlin

// Initialize a Facebook credential with a Facebook access token.
val credential = FacebookAuthProvider.getCredential(token.getToken())

// Assuming the current user is an Apple user linking a Facebook provider.
mAuth.getCurrentUser().linkWithCredential(credential)
    .addOnCompleteListener(this, task -> {
        if (task.isSuccessful()) {
          // Facebook credential is linked to the current Apple user.
          // The user can now sign in to the same account
          // with either Apple or Facebook.
        }
      });

Java

// Initialize a Facebook credential with a Facebook access token.
AuthCredential credential = FacebookAuthProvider.getCredential(token.getToken());

// Assuming the current user is an Apple user linking a Facebook provider.
mAuth.getCurrentUser().linkWithCredential(credential)
    .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
      @Override
      public void onComplete(@NonNull Task<AuthResult> task) {
        if (task.isSuccessful()) {
          // Facebook credential is linked to the current Apple user.
          // The user can now sign in to the same account
          // with either Apple or Facebook.
        }
      }
    });

高度: 手動によるログインフローの処理

Apple Sign-In JS SDK を使用する、OAuth フローを手動で構築する、または AppAuth のような OAuth ライブラリを使用することでログインフローを処理し、Apple アカウントを使用して Firebase での認証を行うこともできます。

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

    SecureRandom を使用すると、Android で暗号的に安全なノンスを生成できます。次に例を示します。

    Kotlin

    private fun generateNonce(length: Int): String {
        val generator = SecureRandom()
    
        val charsetDecoder = StandardCharsets.US_ASCII.newDecoder()
        charsetDecoder.onUnmappableCharacter(CodingErrorAction.IGNORE)
        charsetDecoder.onMalformedInput(CodingErrorAction.IGNORE)
    
        val bytes = ByteArray(length)
        val inBuffer = ByteBuffer.wrap(bytes)
        val outBuffer = CharBuffer.allocate(length)
        while (outBuffer.hasRemaining()) {
            generator.nextBytes(bytes)
            inBuffer.rewind()
            charsetDecoder.reset()
            charsetDecoder.decode(inBuffer, outBuffer, false)
        }
        outBuffer.flip()
        return outBuffer.toString()
    }
    

    Java

    private String generateNonce(int length) {
        SecureRandom generator = new SecureRandom();
    
        CharsetDecoder charsetDecoder = StandardCharsets.US_ASCII.newDecoder();
        charsetDecoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
        charsetDecoder.onMalformedInput(CodingErrorAction.IGNORE);
    
        byte[] bytes = new byte[length];
        ByteBuffer inBuffer = ByteBuffer.wrap(bytes);
        CharBuffer outBuffer = CharBuffer.allocate(length);
        while (outBuffer.hasRemaining()) {
            generator.nextBytes(bytes);
            inBuffer.rewind();
            charsetDecoder.reset();
            charsetDecoder.decode(inBuffer, outBuffer, false);
        }
        outBuffer.flip();
        return outBuffer.toString();
    }
    

    次に、ノンスの SHA246 ハッシュを 16 進数文字列として取得します。

    Kotlin

    private fun sha256(s: String): String {
        val md = MessageDigest.getInstance("SHA-256")
        val digest = md.digest(s.toByteArray())
        val hash = StringBuilder()
        for (c in digest) {
            hash.append(String.format("%02x", c))
        }
        return hash.toString()
    }
    

    Java

    private String sha256(String s) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] digest = md.digest(s.getBytes());
        StringBuilder hash = new StringBuilder();
        for (byte c: digest) {
            hash.append(String.format("%02x", c));
        }
        return hash.toString();
    }
    

    ログイン リクエストでノンスの SHA256 ハッシュを送信します。Apple は、変更を加えることなく、レスポンスでこのノンスを渡します。Firebase では、元のノンスをハッシュ化し、Apple から渡された値と比較することで、レスポンスを検証します。

  2. OAuth ライブラリまたはその他の方法を使用して、Apple のログインフローを開始します。リクエストには、ハッシュ化されたノンスをパラメータとして必ず含めてください。

  3. Apple のレスポンスを受け取ったら、レスポンスから ID トークンを取得し、それとハッシュ化されていないノンスを使用して AuthCredential を作成します。

    Kotlin

    val credential =  OAuthProvider.newCredentialBuilder("apple.com")
        .setIdTokenWithRawNonce(appleIdToken, rawUnhashedNonce)
        .build()
    

    Java

    AuthCredential credential =  OAuthProvider.newCredentialBuilder("apple.com")
        .setIdTokenWithRawNonce(appleIdToken, rawUnhashedNonce)
        .build();
    
  4. Firebase 認証情報を使用して Firebase での認証を行います。

    Kotlin

    auth.signInWithCredential(credential)
          .addOnCompleteListener(this) { task ->
              if (task.isSuccessful) {
                // User successfully signed in with Apple ID token.
                // ...
              }
          }
    

    Java

    mAuth.signInWithCredential(credential)
        .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
          @Override
          public void onComplete(@NonNull Task<AuthResult> task) {
            if (task.isSuccessful()) {
              // User successfully signed in with Apple ID token.
              // ...
            }
          }
        });
    

signInWithCredential の呼び出しが成功した場合は、getCurrentUser メソッドを使用してユーザーのアカウント データを取得できます。

トークン失効

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

また、Apple アカウントでログインをサポートしているアプリで、Apple アカウントでログイン REST API を使用して、ユーザー トークンを取り消す必要があります。

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

  1. startActivityForSignInWithProvider() メソッドを使用して Apple アカウントでログインし、AuthResult を取得します。

  2. Apple プロバイダのアクセス トークンを取得します。

    Kotlin

    val oauthCredential: OAuthCredential =  authResult.credential
    val accessToken = oauthCredential.accessToken
    

    Java

    OAuthCredential oauthCredential = (OAuthCredential) authResult.getCredential();
    String accessToken = oauthCredential.getAccessToken();
    
  3. revokeAccessToken API を使用してトークンを取り消します。

    Kotlin

    mAuth.revokeAccessToken(accessToken)
      .addOnCompleteListener(this) { task ->
        if (task.isSuccessful) {
          // Access token successfully revoked
          // for the user ...
        }
    }
    

    Java

    mAuth.revokeAccessToken(accessToken)
        .addOnCompleteListener(this, new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
              if (task.isSuccessful()) {
                // Access token successfully revoked
                // for the user ...
              }
            }
      });
    
  1. 最後に、ユーザー アカウント(および関連するすべてのデータ)を削除します。

    次のステップ

    ユーザーが初めてログインすると、新しいユーザー アカウントが作成され、ユーザーがログイン時に使用した認証情報(ユーザー名とパスワード、電話番号、または認証プロバイダ情報)にアカウントがリンクされます。この新しいアカウントは Firebase プロジェクトの一部として保存され、ユーザーのログイン方法にかかわらず、プロジェクトのすべてのアプリでユーザーを識別するために使用できます。

    • アプリでは、FirebaseUser オブジェクトからユーザーの基本的なプロフィール情報を取得できます。ユーザーを管理するをご覧ください。

    • Firebase Realtime DatabaseCloud Storageセキュリティ ルールでは、ログイン済みユーザーの一意のユーザー ID を auth 変数から取得し、それを使用して、ユーザーがアクセスできるデータを制御できます。

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

    ユーザーのログアウトを行うには、signOut を呼び出します。

    Kotlin

    Firebase.auth.signOut()

    Java

    FirebaseAuth.getInstance().signOut();