Android Firebase Authentication：Play Console の署名鍵（SHA-1）まわりの問題

Android の Google Sign まわりに1週間近い時間を吸い取られてしまった際のトラブルシューティング。

## 解決策
https://github.com/invertase/react-native-firebase/discussions/7293

> It's worked for me although I'm developing native android app.
When I run apksigner verify --print-certs 90.apk for the apk that signed universal apk downloaded from Play Console bundle explorer.

> Signer (minSdkVersion=33, maxSdkVersion=2147483647) certificate SHA-1 digest:<これは最新のCertificate のSHA1>
Signer (minSdkVersion=24, maxSdkVersion=32) certificate SHA-1 digest:＜これはレガシーのCertificateのSHA1＞
Source Stamp Signer certificate SHA-1 digest:＜これは何のSHA1か結局不明だった＞

> My Android device and Android emulator also Android14(SDK34), so I don't know this response really relevant to this bug, but It worked for me. Thanks a lot, lovely ;)

私の場合いろいろと設定をいじくりまわしたせいで、最終的に Play Console の Legasy Version の SHA-1 も Firebaseコンソール側に追加で設定し（Android Client がもう1つ自動作成される）、google-services.json を再ダウンロードして内部テストにアップロード、実機でGoogleログイン成功した。

ポイントは
- dev, staging などに同じ package name の Android Client, iOS Client があっては非常にダメっぽい。iOSに残っていた
- SHA-1の設定とそれによるGCP側での Android Client の自動作成まわりはシームレスに安定して動いている気がする
- 都度の google-services.json のダウンロードを忘れずに。たまに google-services.json の内容が最新になってないことがあった気がする
- 結局 upload key の SHA1 は不要だった
- デバイス x 環境(GCPプロジェクト）x Googleアカウントは分けた方が無難
　- Android emulator x dev環境(debugキー）x 開発用Googleアカウント
　- 実機 x production環境（Play Console Certificate key) x 個人用Googleアカウント

## 公式ドキュメント
- [Firebase Authentication: Android](https://firebase.google.com/docs/auth/android/google-signin?hl=ja)
- [Google Identity: Android 認証情報マネージャー API](https://developers.google.com/identity/android-credential-manager?hl=ja)
- [Android Developers: Authenticate users with Sign in with Google](https://developer.android.com/identity/sign-in/credential-manager-siwg)（Googleログインとの統合）
- [Sign in your user with Credential Manager](https://developer.android.com/identity/sign-in/credential-manager)(Googleログインに限らない）
- [Android Developers: Sign your app](https://developer.android.com/studio/publish/app-signing)
- [Google Cloud Support](https://console.cloud.google.com/support/offerings) ($29/月で無制限のサポートを受けることができる）

## 前提条件

- dev, staging, production の3環境がある
- Firebase(GCP)の package name はそれぞれ myApp.debug, myApp.debugStaging, myApp の形式

## ログイン部分のコード

まずログイン部分の実装。[Firebase のドキュメント](https://firebase.google.com/docs/auth/android/google-signin?hl=ja)に一連の流れを書いてくれてないので非常に苦労した。

```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. dev, staging で同じ package name の OAuth 2.0 Client を削除する
2. production の Firebase プロジェクトに Play Console の署名鍵の方の SHA-1 と、debug 用の SHA-1 の両方を追加。直後に Android Client が2つ自動で作成される
3. GoogleId を使う形式（下からアカウント一覧 View がでる奴）でうまく行った
4. しかし Apple 側のアプリで Google SignIn をしたりしてるとまた Cannot find a matching credential となった
5. 止むなく旧式の GetSignInWithGoogleOption に変更して、Android エミュレータ、内部テストによる実機、両方のアプリで Google ログインできた
6. がしばらくするとまた失敗するようになった

![スクリーンショット 2024-10-26 0.36.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242610/0a2ac8cb-d452-2e0a-01ad-10731c359f90.png)

## GetGoogleIdOption vs GetSignInWithGoogleOption

 UX的にはログインボタンのタップが不要になる GetGoogleIdOption が良い

> 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

https://stackoverflow.com/questions/77627128/credential-manager-how-do-i-create-a-signinwithgoogle-credential

## SHA-1 フィンガープリントの設定値について
- Android Studio
    - debug
    - release
- Play Console
  -  署名鍵
  -  アップロード鍵


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

リリース用証明書のフィンガープリントを取得するには:
```
keytool -list -v \
-alias <your-key-name> -keystore <path-to-production-keystore>
```

デバッグ用証明書フィンガープリントを取得するには:
```
keytool -list -v \
-alias androiddebugkey -keystore ~/.android/debug.keystore
```

> キーストアのパスワードの入力を求めるプロンプトが表示されます。デバッグ キーストアのデフォルト パスワードは android です。
> https://developers.google.com/android/guides/client-auth?hl=ja

Generate Signed App Bundle して作成した aab の署名の SHA-1
```
cd app/release
keytool -printcert -jarfile app-release.aab
```

Play Console にアップロード後 bundle explorer からダウンロードした Signed apk
```
apksigner verify --print-certs <APKファイル名>.apk
```

## upload key の有無について
各所で upload key も SHA256 もすべて登録しろという記載を見たが自分の場合はいらなかった。

> 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. 
https://ashuvssut.hashnode.dev/android-app-signing#heading-which-keys-do-i-need

とあるが、アップロードキーはあくまで開発者本人からのアップロードかを検証するためだけに使われているのは間違いなさそうで、アップロード→ぐるぐる処理されてしばらく待つの間に Play Console が署名しているのだと思う。実際、Bundle explorer からダウンロードした Signed apk にはアップロードキーの SHA1 の情報はなかった。

Play Console による署名をそもそもオンにしていない場合は、ローカルで専用のリリースキーを別で作って署名などするが、それだとそもそもアップロードキーも表示されない、存在もしないはずだから、やはり不要で正しいのではと思う。

![スクリーンショット 2025-01-28 9.22.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242610/49b25318-c587-e23f-c214-cc48f82cf394.png)
*App integrity の Releases signed by Google Play にチェックがついていれば、たとえ内部テストでも文字通り署名鍵でアップロード後に署名され配布されるよう。ただし内部アプリ共有だと挙動が違うかも*

## Web Client の作り直しについて
これも治ったという声が結構あったが、自分も実施したものの最終的には決して、一番最初に自動生成された Web Client を使って成功した（Firebase コンソール側に設定しなおして google-services.json ダウンロードし直し）

## 気になることメモ

- レビューのテスト動画では成功していた
  - リリース前レポートで別の問題でエラーが2つ発生していたが、それに付属する動画（Firebase Test Lab による自動テスト）では Google ログインが普通に成功していた
- アクセストークンの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.xml` に `default_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 であること（[参考](https://qwertytan.hatenablog.jp/entry/2022/07/28/Android%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%81%A7%E8%A9%B0%E3%81%BE%E3%81%A3%E3%81%9F%E8%A9%B1)）
- 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 アカウントか
- OAuth 同意画面の検証を一度でも進めてしまったか？進めた場合検証が済んでいるか？

### 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 で運用する。

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

### 7. Google アカウント管理で接続を解除しているか？
Google アカウントを管理 > セキュリティ > サードパーティ製のアプリとサービスへの接続。

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

```swift
GIDSignIn.sharedInstance.disconnect()
```

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

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


## 遭遇したエラー

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

### No credential available

単純に Firebase 側に設定した SHA-1 が間違っている時に発生。SHA-1を差し替え → GCP側で旧Android Client を削除 → google-services.json をダウンロードし直して反映で debug（dev) では解決した。

### 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 で補足し、画面にエラー文を表示するなどすると良い。


### OAuth 同意画面
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

## 参考記事
- Finger Print (SHA-1)
    - https://qiita.com/kenmaro/items/a0faf8795f628d80702c
    - https://medium.com/@mvolkanyurtseven/sha1-key-hell-regarding-gmail-authentication-and-sign-in-e710baef5071
- Error
    - https://stackoverflow.com/questions/61768804/getting-16-cannot-find-a-matching-credential-when-doing-one-tap-sign-in-and-s
    - https://stackoverflow.com/questions/77717346/androidx-credentials-exceptions-getcredentialcancellationexception-activity-is
    - https://vocal.media/01/how-to-fix-google-sign-in-error-on-android
- Tutorial
    - https://medium.com/@primo8296/unlocking-doors-harnessing-credential-manager-for-seamless-logins-ebf7a8bcd172
    - https://medium.com/@sama3l/app-signing-and-firebase-authentication-issues-65fd59db7812
    - https://medium.com/@saithitlwin/google-sign-in-for-android-using-credential-manager-dca95eccf794
    - https://www.andrewhornstra.com/pages/google_auth_android/
- About Credential Manager
    - https://speakerdeck.com/clockvoid/google-sign-innoyi-xing-karashi-merucredential-managerhuo-yong
- OAuth
    - https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be
    - https://qiita.com/croquette0212/items/8da7ccaa09abf89d4f51
    - https://kamusoft.hatenablog.jp/entry/2020/06/25/151521
- ProGuard
    - https://fly1tkg.github.io/2012/08/android-proguard/