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

認証は、ユーザーの身元を確立するもので、一般にユーザーの登録またはログインと呼ばれます。認可は、データまたはリソースへのアクセスを許可または拒否するプロセスです。たとえば、アプリがユーザーの Google ドライブにアクセスするための同意をリクエストします。

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

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

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

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

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

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

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

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

  • 認可リクエストでは、必要なスコープ(Drive scope など)に加えて、userinfo、profile、openid のスコープをリクエストします。アクセス トークンが返されたら、任意の HTTP ライブラリを使用して OAuth userinfo エンドポイント(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() を実行すると、アプリのデフォルト アカウントはユーザーが選択したアカウントに設定されます。つまり、それ以降の承認呼び出しでは、このデフォルトのアカウントが使用されます。アカウント選択ツールを強制的に表示するには、認証情報マネージャーの 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
                }
            });
}