Google のユーザーデータへのアクセスを承認する

認証は、ユーザーが誰であるかを特定するもので、通常はユーザー登録またはログインと呼ばれます。認可は、データまたはリソースへのアクセス権を付与または拒否するプロセスです。たとえば、アプリがユーザーの Google ドライブにアクセスするためのユーザーの同意をリクエストします。

認証と認可の呼び出しは、アプリのニーズに基づいて、2 つの別個のフローで行う必要があります。

アプリに Google API データを利用できる機能があるものの、アプリのコア機能の一部として必須ではない場合は、API データにアクセスできない場合にアプリが適切に処理できるように設計します。たとえば、ユーザーがドライブへのアクセス権を付与していない場合は、最近保存したファイルのリストを非表示にできます。

Google API にアクセスする必要があるスコープへのアクセス権は、ユーザーが特定の API へのアクセスを必要とする操作を行った場合にのみリクエストする必要があります。たとえば、ユーザーが [ドライブに保存] ボタンをタップしたときに、ユーザーのドライブへのアクセス権をリクエストする必要があります。

認可を認証から分離することで、新規ユーザーに負担をかけたり、特定の権限を求められる理由についてユーザーを混乱させたりすることを防ぐことができます。

認証には、Credential Manager API を使用することをおすすめします。Google に保存されているユーザーデータへのアクセスを必要とするアクションを 認可するには、 AuthorizationClientを使用することをおすすめします。

Google Cloud コンソール プロジェクトを設定する

  1. Cloud Console でプロジェクトを開くか、 まだプロジェクトがない場合は作成します。
  2. [ブランディング] ページで、 すべての情報が完全かつ正確であることを確認します。
    1. アプリに正しいアプリ名、アプリのロゴ、アプリのホームページが割り当てられていることを確認します。これらの値は、登録時の「Google でログイン」の同意画面と [サードパーティ製のアプリとサービス] 画面に表示されます。
    2. アプリのプライバシー ポリシーと利用規約の URL を指定していることを確認します。
  3. [クライアント] ページで、 アプリの Android クライアント ID がまだない場合は作成します。アプリのパッケージ名と SHA-1 署名を指定する必要があります。
    1. [クライアント] ページに移動します
    2. [クライアントの作成] をクリックします。
    3. アプリケーションの種類として [Android] を選択します。
    4. OAuth クライアントの名前を入力します。この名前は、プロジェクトの [クライアント] ページに表示され、クライアントを識別します。
    5. Android アプリのパッケージ名を入力します。この値は、 package 属性の <manifest> 要素 で、AndroidManifest.xml ファイルに定義されています。
    6. アプリ配信の SHA-1 署名証明書フィンガープリントを入力します。
    7. アプリで Google Play アプリ署名を使用している場合は、 Google Play Console のアプリ署名ページから SHA-1 フィンガープリントをコピーします。
    8. 独自のキーストアと署名鍵を管理している場合は、Java に付属の keytool ユーティリティを使用して、証明書情報を 人間が読める形式で出力します。keytool 出力の Certificate fingerprints セクションにある SHA-1 値をコピーします。 詳しくは、Android 用 Google APIs のドキュメントのクライアントの認証をご覧ください。
    9. (省略可)Android アプリの所有権を確認します。
  4. [クライアント] ページで、 まだ作成していない場合は、新しい [ウェブ アプリケーション] クライアント ID を作成します。現時点では、[承認済みの JavaScript 生成元] フィールドと [承認済みのリダイレクト URI] フィールドは無視してかまいません。このクライアント ID は、バックエンド サーバーが Google の認証サービスと通信するときに、バックエンド サーバーを識別するために使用されます。
    1. [クライアント] ページに移動します
    2. [クライアントの作成] をクリックします。
    3. タイプとして [ウェブ アプリケーション] を選択します。

アプリの所有権を確認する

アプリの所有権を確認することで、アプリのなりすましのリスクを軽減できます。

確認プロセスを完了するには、 Google Play デベロッパー アカウントをお持ちで、アプリが Google Play Consoleに登録されている場合は、そのアカウントを使用できます。確認を成功させるには、次の要件を満たす必要があります。

  • 確認を完了する Android OAuth クライアントと同じパッケージ名と SHA-1 署名証明書フィンガープリントを使用して、Google Play Console に登録されたアプリが必要です。
  • Google Play Console でアプリの管理者 権限が必要です。 詳しくは 、Google Play Console でのアクセス管理をご覧ください。

Android クライアントの [アプリの所有権を確認] セクションで、[所有権の確認] ボタンをクリックして、確認プロセスを完了します。

確認が成功すると、確認プロセスの成功を確認する通知が表示されます。それ以外の場合は、エラー プロンプトが表示されます。

確認に失敗した場合は、次の方法をお試しください。

  • 確認するアプリが Google Play Console に登録されているアプリであることを確認します。
  • Google Play Console でアプリの管理者 権限があることを確認します。

依存関係の宣言

モジュールの build.gradle ファイルで、Google Identity Services ライブラリの最新バージョンを使用して依存関係を宣言します。

dependencies {
  // ... other dependencies

  implementation "com.google.android.gms:play-services-auth:21.5.1"
}

ユーザー アクションに必要な権限をリクエストする

ユーザーが追加のスコープを必要とする操作を行うたびに、AuthorizationClient.authorize() を呼び出します。たとえば、ユーザーがドライブ アプリのストレージへのアクセスを必要とする操作を行う場合は、次の操作を行います。

Kotlin

val requestedScopes: List<Scope> = listOf(DriveScopes.DRIVE_FILE)
val authorizationRequest = AuthorizationRequest.builder()
    .setRequestedScopes(requestedScopes)
    .build()

Identity.getAuthorizationClient(activity)
    .authorize(authorizationRequestBuilder.build())
    .addOnSuccessListener { authorizationResult ->
        if (authorizationResult.hasResolution()) {
            val pendingIntent = authorizationResult.pendingIntent
            // Access needs to be granted by the user
            startAuthorizationIntent.launch(IntentSenderRequest.Builder(pendingIntent!!.intentSender).build())
        } else {
            // Access was previously granted, continue with user action
            saveToDriveAppFolder(authorizationResult);
        }
    }
    .addOnFailureListener { e -> Log.e(TAG, "Failed to authorize", e) }

Java

List<Scopes> requestedScopes = Arrays.asList(DriveScopes.DRIVE_FILE);
AuthorizationRequest authorizationRequest = AuthorizationRequest.builder()
    .setRequestedScopes(requestedScopes)
    .build();

Identity.getAuthorizationClient(activity)
    .authorize(authorizationRequest)
    .addOnSuccessListener(authorizationResult -> {
        if (authorizationResult.hasResolution()) {
            // Access needs to be granted by the user
            startAuthorizationIntent.launch(
                new IntentSenderRequest.Builder(
                    authorizationResult.getPendingIntent().getIntentSender()
                ).build()
            );
        } else {
            // Access was previously granted, continue with user action
            saveToDriveAppFolder(authorizationResult);
        }
    })
    .addOnFailureListener(e -> Log.e(TAG, "Failed to authorize", e));

ActivityResultLauncher を定義するときは、次のスニペットに示すようにレスポンスを処理します。ここでは、フラグメントで処理することを前提としています。このコードは、必要な権限が正常に付与されたことを確認してから、ユーザー アクションを実行します。

Kotlin

private lateinit var startAuthorizationIntent: ActivityResultLauncher<IntentSenderRequest>

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?,
): View? {
    // ...
    startAuthorizationIntent =
        registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { activityResult ->
            try {
                // extract the result
                val authorizationResult = Identity.getAuthorizationClient(requireContext())
                    .getAuthorizationResultFromIntent(activityResult.data)
                // continue with user action
                saveToDriveAppFolder(authorizationResult);
            } catch (e: ApiException) {
                // log exception
            }
        }
}

Java

private ActivityResultLauncher<IntentSenderRequest> startAuthorizationIntent;

@Override
public View onCreateView(
    @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// ...
startAuthorizationIntent =
    registerForActivityResult(
        new ActivityResultContracts.StartIntentSenderForResult(),
        activityResult -> {
            try {
            // extract the result
            AuthorizationResult authorizationResult =
                Identity.getAuthorizationClient(requireActivity())
                    .getAuthorizationResultFromIntent(activityResult.getData());
            // continue with user action
            saveToDriveAppFolder(authorizationResult);
            } catch (ApiException e) {
            // log exception
            }
        });
}

サーバー側で Google API にアクセスする場合は、 getServerAuthCode() から AuthorizationResult メソッドを呼び出して 認証コードを取得し、そのコードをバックエンドに送信してアクセス トークンと 更新トークンに交換します。詳しくは、 ユーザーデータへの継続的なアクセスを維持するをご覧ください。

ユーザーデータまたはリソースへの権限を取り消す

以前に付与されたアクセス権を取り消すには、 AuthorizationClient.revokeAccess() を呼び出します。たとえば、ユーザーがアプリからアカウントを削除しようとしていて、アプリに DriveScopes.DRIVE_FILE へのアクセス権が以前に付与されている場合は、次のコードを使用してアクセス権を取り消します。

Kotlin

val requestedScopes: MutableList<Scope> = mutableListOf(DriveScopes.DRIVE_FILE)
RevokeAccessRequest revokeAccessRequest = RevokeAccessRequest.builder()
    .setAccount(account)
    .setScopes(requestedScopes)
    .build()

Identity.getAuthorizationClient(activity)
    .revokeAccess(revokeAccessRequest)
    .addOnSuccessListener { Log.i(TAG, "Successfully revoked access") }
    .addOnFailureListener { e -> Log.e(TAG, "Failed to revoke access", e) }

Java

List<Scopes> requestedScopes = Arrays.asList(DriveScopes.DRIVE_FILE);
RevokeAccessRequest revokeAccessRequest = RevokeAccessRequest.builder()
    .setAccount(account)
    .setScopes(requestedScopes)
    .build();

Identity.getAuthorizationClient(activity)
    .revokeAccess(revokeAccessRequest)
    .addOnSuccessListener(unused -> Log.i(TAG, "Successfully revoked access"))
    .addOnFailureListener(e -> Log.e(TAG, "Failed to revoke access", e));

トークン キャッシュをクリアする

OAuth アクセス トークンは、サーバーから受信するとローカルにキャッシュされるため、アクセスが高速化され、ネットワーク呼び出しが削減されます。これらのトークンは有効期限が切れるとキャッシュから自動的に削除されますが、他の理由で無効になることもあります。 トークンを使用するときに IllegalStateException が発生した場合は、ローカル キャッシュをクリアして、アクセス トークンの次の認可リクエストが OAuth サーバーに送信されるようにします。次のスニペットでは、ローカル キャッシュから invalidAccessToken を削除します。

Kotlin

Identity.getAuthorizationClient(activity)
    .clearToken(ClearTokenRequest.builder().setToken(invalidAccessToken).build())
    .addOnSuccessListener { Log.i(TAG, "Successfully removed the token from the cache") }
    .addOnFailureListener{ e -> Log.e(TAG, "Failed to clear token", e) }

Java

Identity.getAuthorizationClient(activity)
    .clearToken(ClearTokenRequest.builder().setToken(invalidAccessToken).build())
    .addOnSuccessListener(unused -> Log.i(TAG, "Successfully removed the token from the cache"))
    .addOnFailureListener(e -> Log.e(TAG, "Failed to clear the token cache", e));

認可中にユーザー情報を取得する

認可レスポンスには、使用されたユーザー アカウントに関する情報は含まれません。レスポンスには、リクエストされたスコープのトークンのみが含まれます。たとえば、ユーザーの Google ドライブにアクセスするためのアクセス トークンを取得するレスポンスでは、ユーザーが選択したアカウントの ID は公開されませんが、ユーザーのドライブ上のファイルにアクセスするために使用できます。ユーザーの名前やメールアドレスなどの情報を取得するには、次の方法があります。

  • 認可を求める前に、 Credential Manager API を使用してユーザーを Google アカウントでログインさせます。認証情報マネージャーからの認証レスポンスには、メールアドレスなどのユーザー情報が含まれ、アプリのデフォルト アカウントが選択したアカウントに設定されます。必要に応じて、アプリでこのアカウントを追跡できます。後続の認可リクエストでは、このアカウントがデフォルトとして使用され、認可フローのアカウント選択ステップがスキップされます。認可に別のア 0}カウントを使用するには、デフォルト以外のアカウントからの認可をご覧ください。

  • 認可リクエストで、必要なスコープ(Drive scope など)に加えて、userinfoprofileopenid スコープをリクエストします。アクセス トークンが返されたら、任意の HTTP ライブラリを使用して OAuth ユーザー情報エンドポイント(https://www.googleapis.com/oauth2/v3/userinfo)に GET HTTP リクエストを行い、ヘッダーに受信したアクセス トークンを含めて、ユーザー情報を取得します。これは次の curl コマンドと同等です。

    curl -X GET \ "https://www.googleapis.com/oauth2/v1/userinfo?alt=json" \ -H "Authorization: Bearer $TOKEN"
    

    レスポンスは、UserInfo で、リクエストされたスコープに限定され 、JSON 形式でフォーマットされています。

デフォルト以外のアカウントからの認可

認証情報マネージャーを使用して AuthorizationClient.authorize()を実行すると、アプリのデフォルト アカウントが ユーザーが選択したアカウントに設定されます。つまり、後続の認可呼び出しでは、このデフォルト アカウントが使用されます。アカウント セレクタを強制的に表示するには、Credential Manager の clearCredentialState() API を使用して、ユーザーをアプリからログアウトさせます。

ユーザーデータへの継続的なアクセスを維持する

アプリからユーザーのデータにアクセスする必要がある場合は、 AuthorizationClient.authorize() を 1 回呼び出します。後続のセッションでは、付与された権限がユーザーによって削除されない限り、同じ メソッドを呼び出してアクセス トークンを取得し、ユーザー操作なしで目的を達成できます。一方、バックエンド サーバーからオフライン モードでユーザーのデータにアクセスする必要がある場合は、「更新トークン」と呼ばれる別の種類のトークンをリクエストする必要があります。

アクセス トークンは、有効期間が 1 時間と短くなるように意図的に設計されています。アクセス トークンが傍受または侵害された場合でも、有効期間が限られているため、不正使用の可能性を最小限に抑えることができます。有効期限が切れると、トークンは無効になり、使用しようとするとリソース サーバーによって拒否されます。アクセス トークンは有効期間が短いため、サーバーは更新トークンを使用してユーザーデータへの継続的なアクセスを維持します。更新トークンは有効期間の長いトークンで、古いアクセス トークンの有効期限が切れたときに、ユーザー インタラクションなしで、クライアントが認可サーバーから有効期間の短いアクセス トークンをリクエストするために使用します。

更新トークンを取得するには、まずアプリの認可ステップで「オフライン アクセス」をリクエストして認証コード(認可コード)を取得し、サーバーで認証コードを更新トークンに交換する必要があります。有効期間の長い更新トークンは、新しいアクセス トークンを繰り返し取得するために使用できるため、サーバーに安全に保存することが重要です。そのため、セキュリティ上の懸念から、更新トークンをデバイスに保存することは強く推奨されません。代わりに、アクセス トークンとの交換が行われるアプリのバックエンド サーバーに保存する必要があります。

認証コードがアプリのバックエンド サーバーに送信されたら、アカウントの認可ガイドの手順に沿って、サーバーで有効期間の短いアクセス トークンと有効期間の長い更新トークンに交換できます。この交換は、アプリのバックエンドでのみ行う必要があります。

Kotlin

// Ask for offline access during the first authorization request
val authorizationRequest = AuthorizationRequest.builder()
    .setRequestedScopes(requestedScopes)
    .requestOfflineAccess(serverClientId)
    .build()

Identity.getAuthorizationClient(activity)
    .authorize(authorizationRequest)
    .addOnSuccessListener { authorizationResult ->
        startAuthorizationIntent.launch(IntentSenderRequest.Builder(
            pendingIntent!!.intentSender
        ).build())
    }
    .addOnFailureListener { e -> Log.e(TAG, "Failed to authorize", e) }

Java

// Ask for offline access during the first authorization request
AuthorizationRequest authorizationRequest = AuthorizationRequest.builder()
    .setRequestedScopes(requestedScopes)
    .requestOfflineAccess(serverClientId)
    .build();

Identity.getAuthorizationClient(getContext())
    .authorize(authorizationRequest)
    .addOnSuccessListener(authorizationResult -> {
        startAuthorizationIntent.launch(
            new IntentSenderRequest.Builder(
                authorizationResult.getPendingIntent().getIntentSender()
            ).build()
        );
    })
    .addOnFailureListener(e -> Log.e(TAG, "Failed to authorize"));

次のスニペットでは、認可がフラグメントから開始されることを前提としています。

Kotlin

private lateinit var startAuthorizationIntent: ActivityResultLauncher<IntentSenderRequest>

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?,
): View? {
    // ...
    startAuthorizationIntent =
        registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { activityResult ->
            try {
                val authorizationResult = Identity.getAuthorizationClient(requireContext())
                    .getAuthorizationResultFromIntent(activityResult.data)
                // short-lived access token
                accessToken = authorizationResult.accessToken
                // store the authorization code used for getting a refresh token safely to your app's backend server
                val authCode: String = authorizationResult.serverAuthCode
                storeAuthCodeSafely(authCode)
            } catch (e: ApiException) {
                // log exception
            }
        }
}

Java

private ActivityResultLauncher<IntentSenderRequest> startAuthorizationIntent;

@Override
public View onCreateView(
    @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // ...
    startAuthorizationIntent =
        registerForActivityResult(
            new ActivityResultContracts.StartIntentSenderForResult(),
            activityResult -> {
                try {
                    AuthorizationResult authorizationResult =
                        Identity.getAuthorizationClient(requireActivity())
                            .getAuthorizationResultFromIntent(activityResult.getData());
                    // short-lived access token
                    accessToken = authorizationResult.getAccessToken();
                    // store the authorization code used for getting a refresh token safely to your app's backend server
                    String authCode = authorizationResult.getServerAuthCode()
                    storeAuthCodeSafely(authCode);
                } catch (ApiException e) {
                    // log exception
                }
            });
}