1. 始める前に
認証情報マネージャーを使用して Android に「Google でログイン」を実装する方法を学びます。
前提条件
学習内容
- Google Cloud プロジェクトと OAuth クライアントを作成します。
- ボトムシートのログインフローを実装します。
- 明示的なボタンのログインフローを実装します。
必要なもの
- Android Studio がインストールされている。
- Android Studio と Emulator のシステム要件を満たすパソコン。
- Java Development Kit(JDK)がインストールされている。
2. Android Studio プロジェクトを作成する
まず、Android Studio で新しいプロジェクトを作成します。
- Android Studio を開き、[New Project] をクリックします。

- [Phone and Tablet > Empty Activity] を選択し、[Next] をクリックします。

- プロジェクト設定を構成します。
- 名前: プロジェクト名を選択します。
- パッケージ名: デフォルトを使用するか、独自のものを選択します。
- Minimum SDK: 最新の安定版または > Android 14 を選択します。

- [Finish] をクリックし、初期プロジェクトのビルドが完了するまで待ちます。

3. Google Cloud プロジェクトを設定する
Google Cloud プロジェクトを作成する
- Google Cloud コンソールに移動し、プロジェクトを選択または作成します。

- [API とサービス] > [OAuth 同意画面] に移動します。

- [開始] をクリックし、必須項目を入力します。
- アプリ名: Android アプリの名前を使用します。
- ユーザー サポートメール: Google アカウントを選択します。
- Audience: [External] を選択します。
- 連絡先情報: メールアドレスを入力します。

- Google API サービス: ユーザーデータに関するポリシーを確認し、[作成] をクリックします。

OAuth クライアントを設定する
認証用のクライアント ID を取得するには、Google Cloud コンソールでウェブ クライアントと Android クライアントの両方を作成する必要があります。
- Android クライアント: アプリのパッケージ名と SHA-1 署名を確認してリクエストを保護します。
- ウェブ クライアント: Google ログイン サービスのバックエンド クライアントとして機能します。
Android 版 OAuth 2.0 クライアントを作成する
- [クライアント] ページで [クライアントを作成] をクリックし、[アプリケーションの種類] として [Android] を選択します。

- アプリのパッケージ名(
MainActivity.ktの 1 行目と一致)を入力します。 - SHA-1 署名を生成します。Android Studio ターミナルを開き、次のコマンドを実行します。macOS/Linux:
Windows:keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
keytool -list -v -keystore "C:\Users\USERNAME\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android
- コマンド出力から SHA-1 フィンガープリントをコピーし、コンソールの [SHA-1 フィンガープリント] フィールドに貼り付けて、[作成] をクリックします。

ウェブ OAuth 2.0 クライアントを作成する
- [クライアントを作成] をもう一度クリックし、[アプリケーションの種類] として [ウェブ アプリケーション] を選択します。
- ウェブ クライアントに名前を付け、[URL] / [オリジン] フィールドは空欄のままにして、[作成] をクリックします。

- 確認ダイアログで、生成されたクライアント ID をコピーします。これは Kotlin コードで使用します。

4. Android Virtual Device をセットアップする
アプリをテストするには、物理的な Android デバイスまたは Android Virtual Device(AVD)を使用できます。
AVD を作成して実行する
- Android Studio でデバイス マネージャーを開き、[Create Virtual Device](または + アイコン)をクリックして、[Medium Phone] を選択します。
- システム イメージとして最新の安定版を選択し、[完了] をクリックします。
- デバイスの横にある再生/実行アイコンをクリックして、エミュレータを起動します。

デバイスで Google アカウントにログインする
- エミュレータで、[設定] アプリを開き、[Google] に移動します。
- [Google アカウントにログイン] をクリックし、画面の指示に沿って操作します。

5. 依存関係を追加する
認証と Google ID の統合に必要なライブラリをプロジェクトに追加します。
- [File] > [Project Structure] > [Dependencies] > [app] に移動します。
- [+ > ライブラリの依存関係] をクリックし、
com.google.android.libraries.identity.googleid:googleidを検索して、最新バージョン(1.1.1など)を選択します。 - もう一度 [+ > Library Dependency] をクリックし、
play-services-authを検索して、グループ ID がcom.google.android.gmsのライブラリを選択します。 - [OK] をクリックして変更を適用し、プロジェクトを同期します。

6. ボトムシートのフローを実装する

ボトムシート フローは、Credential Manager API を活用して、ユーザーが Android で Google アカウントを使用してアプリにログインするための効率的な方法を提供します。このフローは、特にリピーター ユーザー向けに、スピードと利便性を重視して設計されており、アプリの起動時にトリガーされる必要があります。
ログイン リクエストを作成する
- まず、
MainActivity.ktを開き、デフォルトのGreeting()関数とGreetingPreview()関数を削除します。 - 3 行目から始まる既存の import ステートメントの後に、次の import ステートメントを追加します。
import android.content.Context import android.os.Build import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.credentials.CredentialManager import androidx.credentials.CustomCredential import androidx.credentials.GetCredentialRequest import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.credentials.exceptions.GetCredentialCustomException import androidx.credentials.exceptions.GetCredentialException import androidx.credentials.exceptions.NoCredentialException import com.google.android.libraries.identity.googleid.GetGoogleIdOption import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import java.security.SecureRandom import java.util.Base64 import kotlinx.coroutines.delay import kotlinx.coroutines.launch const val TAG = "MainActivity" MainActivity.ktファイルのMainActivityクラスの下に、次のコンポーズ可能な関数を追加します。@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @Composable fun BottomSheet(webClientId: String) { val context = LocalContext.current // LaunchedEffect is used to run a suspend function when the composable is first launched. LaunchedEffect(Unit) { // Create a Google ID option with filtering by authorized accounts enabled. val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder() .setFilterByAuthorizedAccounts(true) .setServerClientId(webClientId) .setNonce(generateSecureRandomNonce()) .build() // Create a credential request with the Google ID option. val request: GetCredentialRequest = GetCredentialRequest.Builder() .addCredentialOption(googleIdOption) .build() // Attempt to sign in with the created request using an authorized account val e = signIn(request, context) // If the sign-in fails with NoCredentialException, there are no authorized accounts. // In this case, we attempt to sign in again with filtering disabled. if (e is NoCredentialException) { val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder() .setFilterByAuthorizedAccounts(false) .setServerClientId(webClientId) .setNonce(generateSecureRandomNonce()) .build() val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder() .addCredentialOption(googleIdOptionFalse) .build() //We will build out this function in a moment signIn(requestFalse, context) } } } //This function is used to generate a secure nonce to pass in with our request fun generateSecureRandomNonce(byteLength: Int = 32): String { val randomBytes = ByteArray(byteLength) SecureRandom.getInstanceStrong().nextBytes(randomBytes) return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes) }
コードの分解
LaunchedEffect(Unit): コンポーザブルが最初に表示されたときに、すぐにログイン フローをトリガーします。GetGoogleIdOption.Builder(): Google ID トークン リクエストを構成します。setFilterByAuthorizedAccounts(true): まず、ユーザーがこのアプリに対してすでに承認しているアカウントをフィルタして、サイレント ログインを試みます。これにより、リピーターのユーザーの負担を最小限に抑えます。setNonce(...):generateSecureRandomNonce()によってリクエストごとに生成された安全なランダム ノンスを渡して、リプレイ攻撃を防ぎます。
signIn(request, context): リクエストを実行します。NoCredentialExceptionで失敗した場合(以前に承認されたアカウントが存在しないことを意味します)、フローはsetFilterByAuthorizedAccounts(false)にフォールバックし、ユーザーがデバイスにログインしている Google アカウントから選択できるようにします。
ログイン リクエストを行う
ログイン リクエストが作成されたら、認証情報マネージャーを使用してログイン プロセスを完了できます。リクエストを実行し、発生する可能性のある一般的な例外を処理する signIn という関数を作成します。
MainActivity.kt ファイルの BottomSheet 関数の下に次の関数を追加します。
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
val credentialManager = CredentialManager.create(context)
val failureMessage = "Sign in failed!"
//using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
//on the initial running of our app
delay(250)
return try {
// The getCredential is called to request a credential from Credential Manager.
val result = credentialManager.getCredential(
request = request,
context = context,
)
Log.i(TAG, result.toString())
val credential = result.credential
if (credential is CustomCredential &&
credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
Log.i(TAG, "Signed in as: ${googleIdTokenCredential.id}")
}
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
Log.i(TAG, "(☞゚ヮ゚)☞ Sign in Successful! ☜(゚ヮ゚☜)")
null
} catch (e: GoogleIdTokenParsingException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
e
} catch (e: NoCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": No credentials found", e)
e
} catch (e: GetCredentialCancellationException) {
Toast.makeText(context, "Sign-in cancelled", Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
e
} catch (e: GetCredentialCustomException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
e
} catch (e: GetCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Failure getting credentials", e)
e
}
}
コードの分解
credentialManager.getCredential(...): Credential Manager API を呼び出して、システム アカウント セレクタのボトムシートまたはダイアログを表示します。delay(250): 認証情報マネージャー サービスの初期化が完了する前に、アプリの起動時にボトムシートがすぐにトリガーされた場合に、競合状態を防ぐために一時停止します。- 例外処理: 一般的な認証情報の誤り(キャンセル、認証情報の欠落、トークンの解析に関する問題など)をキャッチしてログに記録し、トーストを使用してユーザーにフィードバックを提供します。
ボトムシート フローをトリガーする
起動時に BottomSheet() を呼び出すように MainActivity クラスを更新します。YOUR_CLIENT_ID_HERE は、ウェブ アプリケーションのクライアント ID に置き換えます。
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
//This will trigger on launch
BottomSheet(webClientId)
}
}
}
}
}
プロジェクトを保存([File] > [Save])して、アプリケーションを実行します。
- 実行ボタン
を押します。 - エミュレータでアプリを起動すると、ログイン ボトムシートが表示されます。[続行] をクリックしてフローをテストします。

- ログインが成功したことを確認するトースト通知が表示されます。

7. ボタンフローを実装する

ボタンフローでは、ユーザーがログインまたは登録するための明示的なオプションが提供されます。標準のブランディングを使用すると、一貫性のあるエクスペリエンスが実現します。「Google でログイン」のブランドの取り扱いガイドラインに準拠した事前承認済みのアセットを使用します。
ブランド アイコンを追加する
- ブランド アセットをこちらからダウンロードし、ZIP ファイルを解凍します。
signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.pngをコピーします。- Android Studio で、ファイルを [res] > [drawable] フォルダに貼り付け、
siwg_button.pngに名前を変更して、[OK] をクリックします。
ボタンフローのコード
このフローは同じ signIn ヘルパー関数を再利用しますが、GetGoogleIdOption ではなく GetSignInWithGoogleOption を渡します。ボトムシート フローとは異なり、明示的なボタンフローでは、保存された認証情報やパスキーが事前にフィルタリングされたり、自動的にプロンプトが表示されたりすることはありません。このコンポーズ可能な関数を BottomSheet 関数の下に貼り付けます。
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val onClick: () -> Unit = {
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
.Builder(serverClientId = webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()
coroutineScope.launch {
signIn(request, context)
}
}
Image(
painter = painterResource(id = R.drawable.siwg_button),
contentDescription = "",
modifier = Modifier
.fillMaxSize()
.clickable(enabled = true, onClick = onClick)
)
}
コードの分解
GetSignInWithGoogleOption: ボトムシート フローとは異なり、明示的なボタン フローでは、このオプションを使用して、自動フィルタリングなしで Google アカウントを選択するようユーザーに促します。coroutineScope.launch: ボタンがクリックされたときに、中断signIn関数を非同期で実行するコルーチンを起動します。Image: ブランドのsiwg_buttonドローアブルを表示し、フローをトリガーするクリック リスナーをアタッチします。
ボタンを UI レイアウトに追加する
自動 BottomSheet と明示的な ButtonUI の両方を縦に並べて表示するように、MainActivity レイアウトを更新します。
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//This will trigger on launch
BottomSheet(webClientId)
//This requires the user to press the button
ButtonUI(webClientId)
}
}
}
}
}
}
ボタンフローをテストする
- アプリケーションを実行します。
- シート領域の外側をクリックして、最初のボトムシートを閉じます。
- [Sign in with Google](Google でログイン)ボタンをクリックしてログイン ダイアログを開き、アカウントを選択します。

- 結果を確認します。Logcat をチェックして、ユーザー名/メールアドレスの出力が確認できていることを確認します。
8. まとめ
おめでとうございます!Android 認証情報マネージャーを使用して「Google でログイン」を正常に実装しました。
参考情報
MainActivity.kt の完全なコード
参考までに、MainActivity.kt のコード全体を次に示します。
package com.example.example
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.example.example.ui.theme.ExampleTheme
import android.content.ContentValues.TAG
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.credentials.exceptions.GetCredentialException
import androidx.compose.foundation.clickable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.credentials.CredentialManager
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialCustomException
import androidx.credentials.exceptions.NoCredentialException
import androidx.credentials.GetCredentialRequest
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import java.security.SecureRandom
import java.util.Base64
import kotlinx.coroutines.CoroutineScope
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//This will trigger on launch
BottomSheet(webClientId)
//This requires the user to press the button
ButtonUI(webClientId)
}
}
}
}
}
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun BottomSheet(webClientId: String) {
val context = LocalContext.current
// LaunchedEffect is used to run a suspend function when the composable is first launched.
LaunchedEffect(Unit) {
// Create a Google ID option with filtering by authorized accounts enabled.
val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
// Create a credential request with the Google ID option.
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()
// Attempt to sign in with the created request using an authorized account
val e = signIn(request, context)
// If the sign-in fails with NoCredentialException, there are no authorized accounts.
// In this case, we attempt to sign in again with filtering disabled.
if (e is NoCredentialException) {
val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOptionFalse)
.build()
signIn(requestFalse, context)
}
}
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val onClick: () -> Unit = {
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
.Builder(serverClientId = webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()
coroutineScope.launch {
signIn(request, context)
}
}
Image(
painter = painterResource(id = R.drawable.siwg_button),
contentDescription = "",
modifier = Modifier
.fillMaxSize()
.clickable(onClick = onClick)
)
}
fun generateSecureRandomNonce(byteLength: Int = 32): String {
val randomBytes = ByteArray(byteLength)
SecureRandom.getInstanceStrong().nextBytes(randomBytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
val credentialManager = CredentialManager.create(context)
val failureMessage = "Sign in failed!"
//using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
//on the initial running of our app
delay(250)
return try {
// The getCredential is called to request a credential from Credential Manager.
val result = credentialManager.getCredential(
request = request,
context = context,
)
Log.i(TAG, result.toString())
val credential = result.credential
if (credential is CustomCredential &&
credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
Log.i(TAG, "Signed in as: ${googleIdTokenCredential.id}")
}
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
Log.i(TAG, "(☞゚ヮ゚)☞ Sign in Successful! ☜(゚ヮ゚☜)")
null
} catch (e: GoogleIdTokenParsingException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
e
} catch (e: NoCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": No credentials found", e)
e
} catch (e: GetCredentialCancellationException) {
Toast.makeText(context, "Sign-in cancelled", Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
e
} catch (e: GetCredentialCustomException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
e
} catch (e: GetCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Failure getting credentials", e)
e
}
}