0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Android Firebase Authentication:Play Console の署名鍵(SHA-1)まわりの問題

Last updated at Posted at 2024-10-25

他の多くの人がそうであるように、1,2日ほどの時間を Android の Google Sign まわりに吸い取られてしまったのでできる範囲でトラブルシューティングを残しておく。

追記

やはり失敗してしまう。内部テストだけ失敗するので、CredentialManagerまわりの実装が間違って State がクリアできてないか、Android 端末に1度ログインしてしまうとそのGoogleアカウントに何かしら紐づくのか、、

レビューのテスト動画では成功してる..

リリース前レポートで別の問題でエラーが2つ発生していたが、それに付属する動画(人力操作のように見えるが Firebase Test Lab による自動テストのよう)では Google ログイン普通に成功していた..。やはり内部テストやレビュー前のアプリだと署名まわりが何か違うのか?

upload key やはり必要だった?

Then the question arises, which SHA-1 should I add to the Firebase Console? There are two SHA-1 at Play Console. You have to add both SHA-1 fingerprints to Firebase. Why? - To publish your app, you will need to request an App review from the Play Store, you always have to submit your app signed with your Upload keystore. That's why, to make that Upload keystore signed App work properly with those Firebase features, you need to add SHA-1 of Upload keystore in Firebase.

GetGoogleIdOption vs GetSignInWithGoogleOption

Very importantly, we strongly recommend that the above API calls be made in your app as soon as the user lands on your app, without any user interaction (i.e without a need for the user to click on a button). This is very different from making these calls if a user taps on the "Sign-in with Google" button

気になることやメモ

  • アクセストークンのTTLは3600秒=1時間
  • iOS 側の disconnect()
  • Android 初回ログイン時に iOS 側で作成した既存アカウントがあるかどうかの違い
  • debug でログイン試みてからの内部テスト側をインストールすると成功するケース
  • 内部テストの Play Store への反映にラグがあること
    • Google Play Store アプリを終了し、設定 > アプリ > Google Play Store からストレージとキャッシュの削除を行うと最新が表示される模様
  • Firebase GoogleログインのSDK設定を変更直後に google-service.json をダウンロードしても最新になってなかったり、逆にSDK設定がもとに戻ってたりする?
  • OAuth ウェブアプリケーションクライアントを手動作成すると、auto generated by Google Service の時と比べ google-service.json の client.oauth_client に追加されなくなる(client.services.appinvite_service.other_platform_oauth_client には追加される)
  • 上記によって Andorid Studio の build 配下の values.xmldefault_web_client_id が出力されなくなる
  • Play Console の内部テストのリリース作成ページに「Releases signed by Google Play」とあるのでアプリには Play Console の署名鍵が使われているのは間違いないはず
  • Firebase Authentication > Google の「ウェブ SDK 構成」の変更に失敗する
  • GetGoogleIdOption を使うか GetSignInWithGoogleOption を使うか
  • OAuth 同意画面の作成本当に不要なのか
  • Credential Manager のデジタルアセットリンク
  • Android 実機の Google Play 開発者サービスのキャッシュ(Google Play Store じゃない方)
  • context を activityContext でなく applicationContext を使ってないか(LocalContext.current で取得しているのは activityContext で間違いないはず)
  • Play Console の署名鍵の SHA-1 登録によって自動作成された OAuth Android クライアントの所有権を確認しようとすると「このクライアントは Google Play ストア アプリではないため、所有権を確認できません。」と表示される。ドキュメント見る限り製品版として公開されてないと出来ないっぽい
  • ProGuard による難読化が悪さをしていないか
  • 内部アプリ共有を使うとデバッガーをアタッチできるらしいが、やってみると「アプリを公開する必要があります」と出て app/apk をアップロードできない
  • OAuth 同意画面の検証を1度進めてしまったこと
  • Google Play Store アプリが入ってなく、ウェブ経由でしかテストに参加できない場合
    • Pixel API 28, Pixel 8 API 30 には入っているが Pixel 5 API 30 にはなぜか入っていない
  • 前からずっと Unknown calling package name 'com.google.android.gms' エラーが出ている
  • singingReport したときの release 鍵が null であること(参考
  • OAuth 同意画面まわりの管理 UI が新しくなるタイミングであること
  • OAuth 同意画面の検証ステータスが「Pending developer action」のままである(一度審査に出してしまったので Verification not required」に戻っていない)
  • default_web_client_id などの情報が含まれる app/build/generated/res/processDebugGoogleServices/values/values.xml が Generate Signed App Bundle では生成されないこと(stringResource(id = R.string.default_web_client_id) でアクセスしている)

変数

  • Android スマホ側の Google Play を開発者モードにする
  • Google Play Console の管理者に追加されている Google アカウントでログインするかどうか
  • Google Workspace で管理された Google アカウントか
  • 1Passwordなどの別のサービスでパスキーを生成しているアカウントか
  • OAuth 同意画面の検証を一度でも進めてしまったか?進めた場合検証が済んでいるか?
  • Firebase プロジェクト設定のプライバシーに関する連絡先

公式ドキュメント

前提

まずログイン部分の実装。Firebase のドキュメントに一連の流れを書いてくれてないので非常に苦労した。

LoginScreen.kt
// 旧来の方式?
 val googleIdOption = GetSignInWithGoogleOption.Builder(
        serverClientId = stringResource(id = R.string.web_client_id) // TODO: switch by flavor
    )

    // OneTap signIn 新しい方式?
    // val googleIdOption = GetGoogleIdOption.Builder()
    //    .setServerClientId(stringResource(id = R.string.web_client_id)) // TODO: switch by flavor
        // .setFilterByAuthorizedAccounts(false) // true だと過去最初に認証した Google アカウントしか表示されなくなる
        // .setNonce(generateNonce()) // Nonce を設定したほうがいいはずだが無くても動く
        // .build()
        
    val request = GetCredentialRequest.Builder()
        .addCredentialOption(googleIdOption)
        .build()

        // 中略

        // Google Sign In Button
            Button(
                onClick = {
                    scope.launch {
                    // 本来このあたりを try-catch して snackbarState.showSnackbar(error) とかしてもいいがエラーメッセージを見たところでたいした解決にならない
                            val res = credentialManager.getCredential(
                                context = context,
                                request = request
                            )
                            authService.handleGoogleSignIn(res, onSuccess = onLoginSuccess)
                    }
                },
                modifier = Modifier
            ) {
                Text("Sign In with Google")
            }
AuthService.kt
// ホントは AuthViewModel を挟んで error とかは保持させたい
suspend fun handleGoogleSignIn(
        response: GetCredentialResponse,
        onSuccess: () -> Unit,
        onError: (String) -> Unit
    ) {
        val credential = response.credential

        when (credential) {
            is CustomCredential -> {
                val googleIdTokenCredential = GoogleIdTokenCredential
                    .createFrom(credential.data)
                val googleIdToken = googleIdTokenCredential.idToken
                val firebaseCredential = GoogleAuthProvider.getCredential(googleIdToken, null)
                try {
                    val result = auth.signInWithCredential(firebaseCredential).await()
                    result.user?.let {
                        try {
                            usersRepository.createOrFetchUser(it)
                            onSuccess()
                        } catch (e: FirebaseAuthException) {
                            onError("Failed to createOrFetchUser: ${e.localizedMessage}")
                        }
                    } ?: run {
                        onError("result.user is null")
                    }
                } catch (e: FirebaseAuthException) {
                    onError("Googleサインイン失敗: ${e.localizedMessage}")
                }
            }

            else -> {
               // 本来は creadential が PublicKeyCredential や PasswordCredential のときの分岐もここにまとめられる
            }
        }
    }

参考:https://qiita.com/goretzka_/items/20d7944912018c321ed6

結論

  1. 今回 staging と production 環境として、2つの Firebase プロジェクトを作成していた

  2. staging の方の OAuth クライアント、Android クライアントを削除し、staging の Firebase Authentication から(Google SignIn で作成した)対象アカウントを削除

  3. production の Firebase プロジェクトに Play Console の署名鍵の方の SHA-1 と、debug 用の SHA-1 を追加。直後に Google Cloud の OAuth クライアントに Android クライアントが2つ自動で作成される

  4. について、GoogleId を使う形式(下からアカウント一覧 View がでる奴)で1回うまく行ったが、Apple 側のアプリで Google SignIn をしたりしてるとまた Cannot find a matching credential となったので、止むなく旧式の GetSignInWithGoogleOption を使用して対処した。

上記を行うことで Android エミュレータ、内部テストで Play ストア経由でインストールする実機、両方のアプリで Google ログインできた。debug の SHA-1 は ./gradlew signingReport で確認できる。

スクリーンショット 2024-10-26 0.36.52.png

選択できる変数

1. google-services.json

今回は production or staging なので、production の一択だが、app 配下に配置し直すタイミングはそれがダウンロードできるプロジェクト設定ページでフィンガープリントを追加や削除したときのみでOKな模様。

2. web_client_id

.setServerClientId() に渡す引数。種類「ウェブアプリケーション」の OAuth クライアントの client_id。
これも今回 production を動かしたいので production のもの1択だが、google-service.json は production 用なのに、この web_client_id は staging 用のを使ってしまったのに何か動くとかあるから困る..。

3. 外部プロジェクトのクライアント ID をセーフリストに追加

Google Authentication > ログイン方法の設定値。これは 2 の「認証は staging の OAuth クライアント(ウェブアプリケーション)経由で行ってもOKとしつつ、Firebase は production の方に向ける」といったときに使える模様。が、この形式だと「Play Console の署名鍵の SHA-1 はどっちの Firebase に設定するのか?」とかもあって混乱するので極力使わないほうが良さそう。

4. setFilterByAuthorizedAccounts(true/false)

コードにもコメントしたが、ログイン時の Google アカウント一覧に過去に認証したアカウントのみを表示する(true)かどうかの設定。同じアプリに違う Google アカウントで2つアカウント作成しないために Google は推奨しているが、デバッグしづらいし、そんなに Google アカウントたくさん持ってるユーザーもいないと思うので false で運用する。

5. フィンガープリントの設定値

debug, release, Play Console の署名鍵、アップロード鍵。ここが一番ややこしいが、エミュレータのために debug 用のを1つ(./gradlew signingReport で確認できる Variant: debug の SHA-1)。本番環境(内部テストなど)のアプリために「Play Consle > アプリの署名 > アプリの署名鍵の証明書の SHA-1」を1つ、計2つを設定するだけでよかった。

「Play Console のアップロード鍵」 = 「release の keystore」 = 「Generate Signed App Bundle 時に指定した upload-key.jks」 で間違いないはずで、これは結局 Firebase のフィンガープリントに必要ない模様。

release の keystore の SHA-1 はプロジェクトのルートからだと下記コマンドで確認できる

cd app/release
keytool -printcert -jarfile app-release.aab

6. OAuth クライアント(ウェブアプリケーション、Android クライアント)

1つのウェブアプリケーション(認証はこれ経由で行われる)と、Firebase に登録したフィンガープリントの数だけ Android クライアントが自動作成される。パッケージ名 × SHA-1 の組み合わせは全プロジェクトで1つしか許されないらしく、フィンガープリント登録しても自動作成がされなかった = 他のプロジェクトで同組み合わせの OAuth クライアントが存在している可能性が高い。

7. Google アカウント管理で接続を解除しているか?

Google アカウントを管理 > セキュリティ > サードパーティ製のアプリとサービスへの接続。

8. iOS 側のサインアウトなどで disconnect() しているか

公式ドキュメントには書いていないが、下記を呼ぶことで OAuth 2.0 のアクセストークンを削除できる(= 7のGoogleアカウント管理の接続アプリ一覧から削除される)ようだ。

GIDSignIn.sharedInstance.disconnect()

9. disable automatic sign-in or not

ユーザーが Google アカウント管理で自動ログインを無効にしているかどうか。setAutoSelectEnabled(true) にする場合、無効にしていると当然それが動かない。

10. テストする実機以外で一度でもログインに利用した Google アカウントか?

これらの変数がある中で何をどうするかを設定していかなければならない。

遭遇したエラー

正直どのエラーの原因がこうで、対処法はこう!とまで検証できてないがだいたいこんな感じでエラーがでる。要は「staging なんかなくて、最初から production 1つに OAuth クライアント(ウェブアプリケーション)、フィンガープリントに設定した Play Console 署名鍵の SHA-1、同じく debug 用の SHA-1 の Andorid OAuth クライアントが自動作成されてればいいのだ」状態が作ることが肝要かと。

Google id_token is not allowed to be used with this application

The supplied auth credential is incorrect, malformed or has expired. [ Invalid Idp Response: the Google id_token is not allowed to be used with this application. Its audience (OAuth 2.0 client ID) is , which is not authorized to be used in the project with project_number: xxx. ]

これは google-service.json と web_client_id のプロジェクトが異なっている場合に出る模様? staging のクライアントIDをセーフリストに加えると解消する気もするが、素直に web_client_id を揃えるしかないかなと。

Cannot find a matching credential

During begin sign in, failure response from one tap: 16 [28433] Cannot find a matching credential.

過去に staging で Google SignIn して staging の Firebase Authentication 側にアカウントが作成されていると出る?もしくは、iOS などで OneTap SignIn や OptionID 以外の方法でログイン作成したアカウントがあるときに OneTap 方式でログインしようとして出るのか?

一旦 staging のアカウントを削除しつつ、GetSignInWithGoogleOption を使う。

activity is cancelled by the user

Error getting credential
androidx.credentials.exceptions.GetCredentialCancellationException: activity is cancelled by the user.
at androidx.credentials.playservices.controllers.CredentialProviderController$Companion.maybeReportErrorResultCodeGet(CredentialProviderController.kt:108)

これは本来、文字通り Google SignIn のUI(ボトムシート or ダイアログ)の外部の薄暗いところをタップするなどして操作をキャンセルしたときに表示されるエラーだが、ちゃんと Google アカウントを選択して発生する。
dubug 用の SHA-1 が production の Firebase プロジェクトに設定されてなかったことが原因?
web_client_id の値が間違っていてもこのエラーが出る。

account reauth failed

Error getting credential
androidx.credentials.exceptions.GetCredentialCancellationException: [16] Account reauth failed.
at androidx.credentials.CredentialProviderFrameworkImpl.convertToJetpackGetException$credentials_release(CredentialProviderFrameworkImpl.kt:310)

FirebaseInternalProductService.EnableGoogleSignIn

google.internal.firebase.v1.FirebaseInternalProductService.EnableGoogleSignIn
GCP のログに出ていた。Google SignIn はずっと有効にしていたのだけど。

Pixel 9 Pro API 35 だと 挙動は activity is cancelled by the user と同じだが、エラーメッセージはこれが出る。

Tips

エミュレータで内部テストのアプリをインストールしログを出力

内部テストは実機だとログが見れないが、エミュレータの Chrome で内部テストのリンク(URL)にアクセスして内部テスト用のアプリをインストールすることができる。こうすればログも出力されるのでデバッグしやすい。

エミュレータにURLを貼り付けるには adb shell input text 'https://~' コマンドを使うと良い。

try-catch で囲んでエラーを snackBarState.showSnackbar() で表示

Jetpack Compose 前提の話だが、内部テストの実機ではログが見れないため、サインインエラーのときのエラー文を try-catch で補足し、画面にエラー文を表示するなどすると良い。

よくなかったこと

  • そもそも staging と production 環境として、2つの Firebase プロジェクトを作成し、それらに同じGoogleアカウントを利用してログインをしようとしていたのが、そもそも Android や Firebase の環境構築方法としてベストプラクティスではなかった気がする。少なくとも今回 SHA-1 や Google アカウントがそれぞれに分散したためミスに気づきにくかったし。

その他の注意点

  • production の方は例えば iOS で Google SignIn して作成したアカウントがあってもOK。むしろそこで作成したアカウントに Android でログインしてもログインできるか、統合されるかを確かめたかった。
  • 各個人の「Google アカウント管理 > セキュリティ > サードパーティ製のアプリとサービスへの接続」から staging の接続を解除してもあまり意味はなさそう
  • Google SignIn だけを実装するなら OAuth 同意画面は作成してはいけない!これはそれ以外の Scope で Google アカウントの情報や操作を許可してもらいたいときに必要

余談

StackOverflowなどで SHA-256も、アップロード鍵のSHA-1, SHA-256 もすべて登録しろとあったがそれはやはり必要なかったし、SHA-1 だけでいいとか、SHA-256 は DynamicLink を使う場合に必要とかの意見があった。

そもそも「なぜ署名鍵のSHA-1を登録してるのに、(Google Play Console に署名を代行してもらうための)アップロード鍵の登録までいるのだ」というモヤモヤが消えなかったが、やはり要らないという結論になったよかった。

Googleの公式ドキュメント的にも署名鍵の SHA-1 のみで十分そうに書いてある。

Certain Google Play services (such as Google Sign-in and App Invites) require you to provide the SHA-1 of your signing certificate so we can create an OAuth2 client and API key for your app.
https://developers.google.com/android/guides/client-auth

参考記事

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?