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

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

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

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

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

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

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

プロジェクトを設定する

  1. でプロジェクトを開くか、まだ作成していない場合はプロジェクトを作成します。
  2. で、すべての情報が完全かつ正確であることを確認します。
    1. アプリに正しいアプリ名、アプリのロゴ、アプリのホームページが割り当てられていることを確認します。これらの値は、登録時の [Google でログイン] の同意画面と [サードパーティ製のアプリとサービス] 画面でユーザーに表示されます。
    2. アプリのプライバシー ポリシーと利用規約の URL を指定していることを確認します。
  3. で、アプリの Android クライアント ID を作成します(まだ作成していない場合)。アプリのパッケージ名と SHA-1 署名を指定する必要があります。
    1. に移動します。
    2. [クライアントを作成] をクリックします。
    3. アプリケーションの種類として [Android] を選択します。
  4. で、まだ作成していない場合は、新しい「ウェブ アプリケーション」クライアント ID を作成します。[承認済みの JavaScript 生成元] と [承認済みのリダイレクト URI] のフィールドは、今のところ無視してかまいません。このクライアント ID は、バックエンド サーバーが Google の認証サービスと通信する際に、バックエンド サーバーを識別するために使用されます。
    1. に移動します。
    2. [クライアントを作成] をクリックします。
    3. [ウェブ アプリケーション] タイプを選択します。

依存関係の宣言

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

dependencies {
  // ... other dependencies

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

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

ユーザーが追加のスコープを必要とするアクションを実行するたびに、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.launchIntentSenderRequest.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 (ApiException e) {
                // 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 にアクセスする場合は、AuthorizationResult から getServerAuthCode() メソッドを呼び出して認証コードを取得し、バックエンドに送信してアクセス トークンと更新トークンに交換します。詳しくは、ユーザーのデータへの継続的なアクセスを維持するをご覧ください。

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

以前に付与したアクセス権を取り消すには、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 は明らかにしません。ユーザーの名前やメールアドレスなどの情報を取得するには、次の方法があります。

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

  • 承認リクエストで、必要なスコープ(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 形式です。

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

認証に Credential Manager を使用して AuthorizationClient.authorize() を実行すると、アプリのデフォルト アカウントはユーザーが選択したアカウントに設定されます。つまり、以降の認証呼び出しでは、このデフォルト アカウントが使用されます。アカウント選択ツールを強制的に表示するには、認証情報マネージャーの 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.launchIntentSenderRequest.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
                }
            });
}