GoogleアカウントのOneTap+FirebaseAuth with JetpackCompose
JetpackComposeで、GoogleアカウントのOneTap+FirebaseAuthを実装していきます。
関連記事
- JetpackComposeでFirebaseAuth(パスワード認証)
- JetpackComposeでFirebaseAuth(Google認証)
- JetpackComposeでFirebaseAuth(メールリンク認証)
エンドポイント
GoogleアカウントのOneTap
エンドポイント | |
---|---|
SignInClient.beginSignin(BeginSignInRequest) | OneTap認証画面を表示します |
signInClient.getSignInCredentialFromIntent(resultData: Intent) | OneTap認証画面から戻ってきたときのResultDataをGoogleアカウントのCredentialに変換します |
FirebaseAuth
エンドポイント | |
---|---|
GoogleAuthProvider.getCredential(googleCredential) | GoogleアカウントのCredentialをFirebaseAuthのCredentialに変換します |
Firebase.auth.signInWithCredential(firebaseCredential) | FirebaseAuthのCredentialでサインイン処理を行います |
処理の流れ
流れはフローチャートにしました。
複雑ではないのですが、OneTap認証画面の起動と結果処理のところに、JetpackComposeのクセを感じます。
dependencies
android {
buildFeatures {
buildConfig = true
}
+ def properties = new Properties()
+ properties.load(project.rootProject.file('local.properties').newDataInputStream())
+ def googleOauthServerClientId = properties.getProperty('google_oauth_server_client_id')
defaultConfig {
// ...なんかいろいろ設定...
+ buildConfigField("String", "GOOGLE_OAUTH_SERVER_CLIENT_ID", "${googleOauthServerClientId}")
}
}
dependencies {
+ // for GoogleSignIn
+ implementation 'com.google.android.gms:play-services-auth:20.6.0'
}
def googleOauthServerClientId = properties.getProperty('google_oauth_server_client_id')
buildConfigField("String", "GOOGLE_OAUTH_SERVER_CLIENT_ID", "${googleOauthServerClientId}")
Googleアカウントでの認証には、GoogleCloudConsole>対象アプリ>認証情報>OAuth 2.0 クライアント IDのうち、Web clientのクライアントIDが必要です。
Firebaseコンソールでアプリを作っていると、勝手にできていると思いますので、GoogleCloudConsoleから拾ってきます。
秘密の情報にしておいた方が無難かと思いますので、クライアントIDは「local.properties」に保存しておいて、BuildConfig経由で取得しようと思います。
そのための設定が引用部分になります。
implementation 'com.google.android.gms:play-services-auth:20.6.0'
Googleアカウントの認証には、play-services-authが必要です。
インフラ層の実装
前回作成したクラスを拡張していきます。
+ override suspend fun requestGoogleOneTapAuth(signInClient: SignInClient): PendingIntent {
+ val result = signInClient.beginSignIn(signInRequest).await()
+ return result.pendingIntent
+ }
+ override suspend fun signinGoogleOneTapAuth(signInClient: SignInClient, resultData: Intent) {
+ val googleCredential = signInClient.getSignInCredentialFromIntent(resultData)
+ val firebaseCredential = GoogleAuthProvider.getCredential(googleCredential.googleIdToken, null)
+ auth.signInWithCredential(firebaseCredential).await()
+ }
関数 | 役割 |
---|---|
requestGoogleOneTapAuth | OneTap認証画面を起動するためのPendingIntentを取得する |
signinGoogleOneTapAuth | OneTap認証画面からの戻り値を解析し、FirebaseAuthにSignInする |
ここの説明は、処理の流れと同じですので割愛します。
UseCase層の実装
23/7/28追記
引数のActivityをやめて、ApplicationContextをDIするように修正しました
class GoogleSigninCase @Inject constructor(
@ApplicationContext private val context: Context,
private val accountRepository: AccountRepository,
) {
suspend fun signinOneTap(launcher: ActivityResultLauncher<IntentSenderRequest>) {
val signInClient = Identity.getSignInClient(context)
val pendingIntent = accountRepository.requestGoogleOneTapAuth(signInClient)
val intentSenderRequest = IntentSenderRequest.Builder(pendingIntent).build()
launcher.launch(intentSenderRequest)
}
suspend fun onResultSigninOneTap(result: ActivityResult) {
if (result.resultCode != Activity.RESULT_OK) {
Log.d("SigninViewModel", "onResultGoogleSignIn ${SigninOneTapError(result.data).resultMessage}")
return
}
val resultData = result.data ?: let {
Log.d("SigninViewModel", "onResultGoogleSignIn result.data is null")
return
}
val signInClient = Identity.getSignInClient(context)
accountRepository.signinGoogleOneTapAuth(signInClient, resultData)
}
}
(ViewModelとUseCaseの責務の範囲に関していろいろ迷いました...)
関数 | 役割 |
---|---|
signinOneTap | OneTap認証画面の起動 |
onResultSigninOneTap | OneTap認証画面の結果解析とSignIn処理 |
signinOneTap
Identity.getSignInClient(activity)で、@ComposableでLocalContext.currentの値をActivityにキャストしたものを引数に、SignInClientを取得します。
Identity.getSignInClient(context)で、ApplicationContextをDIしたものを引数に、SignInClientを取得します。
取得したSignInClientを使用して、OneTap認証画面を起動するためのPendingIntentを取得します。
受け取ったPendingIntentを持つIntentSenderRequestを生成して、ActivityResultLauncherで画面起動処理を行うと、OneTap認証画面が起動します。
onResultSigninOneTap
OneTap認証画面が閉じたときに、ActivityResultが返ってきますので、ActivityResult.resultCodeがOKの場合は、SignIn処理を行います。
ViewModel層の実装
+ fun onClickSignInWithGoogleOneTap(launcher: ActivityResultLauncher<IntentSenderRequest>) {
+ viewModelScope.launch(
+ context = CoroutineExceptionHandler { _, throwable ->
+ val message = throwable.toSnackbarMessage()
+ SnackbarManager.showMessage(throwable.toSnackbarMessage())
+ },
+ block = {
+ googleSigninCase.signinOneTap(launcher)
+ },
+ )
+ }
+ fun onResultSignInWithGoogleOneTap(result: ActivityResult) {
+ viewModelScope.launch(
+ context = CoroutineExceptionHandler { _, throwable ->
+ val message = throwable.toSnackbarMessage()
+ SnackbarManager.showMessage(throwable.toSnackbarMessage())
+ },
+ block = {
+ googleSigninCase.onResultSigninOneTap(result)
+ },
+ )
+ }
これはUseCaseを呼び出すだけなので説明は割愛です。
Screenの実装
/* ...いろいろ実装...*/
+val startForResultGoogleSignin = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartIntentSenderForResult(),
+ onResult = { result -> viewModel.onResultSignInWithGoogleOneTap(result) },
+)
+BasicButton(
+ text = R.string.sign_in_with_google,
+ modifier = Modifier.basicButton(),
+ action = { viewModel.onClickSignInWithGoogleOneTap(startForResultGoogleSignin) },
+)
rememberLauncherForActivityResultを使って、ActivityResultLauncherを作成します。
ActivityResultLauncherのonResultに、「viewModel.onResultSignInWithGoogleOneTap」を指定して、結果処理を行います。
BasicButtonのactionが、onClickの時に動く処理です。
「viewModel.onClickSignInWithGoogleOneTap」を指定して、OneTop認証画面の起動を行います。
テストでキャンセルしすぎた場合は、「##66382723##」に電話をかけましょう。制限がオフになります。
オンに戻すときは同じ番号にもう一度電話をかけましょう
https://developers.google.com/identity/one-tap/android/get-saved-credentials?hl=ja#disable-one-tap
いざ実行
コードはこちらです。
23/7/28追記
Identity.getSignInClientの引数をActivityからApplicationContextに変更したソースはこちらです。