How to Actually Implement "Sign in With Google" on Android (Jetpack Compose)
Background
Recently at pinplanet, I was integrating Google’s social auth into our native Jetpack Compose app. We had previously created our endpoints to implement this on iOS, but the flow of events is slightly different on Android. While going through the documentation from Google, several things stood out to me as incorrect or targeted at a different audience. I have less than a year’s worth of experience building for Android, so there were things in the documentation that were misleading for me.
The purpose of this post is not to simply reproduce the steps outlined by the documentation provided by Google, but to shorten the reader’s time to correct what I think are mistakes in its presentation.
We do not use Firebase, so this overview will not use anything Firebase related.
1. Creating an API Credential with SHA-1
As explained in the Setup your Google APIs console project section of the tutorial, we needed four OAuth 2.0 Client IDs (three Android, one web application).
- One web application ID for the backend
- Two Android IDs for development, one for each developer
- One Android ID for the production app
The difference from the instructions on the OAuth client page linked in the tutorial is that my working production Android client ID is not using the SHA-1 for the keystore, but the App signing key certificate found in the developer console under Setup in the App signing subsection.
Yes, the instructions on the OAuth client page tell you to get the keystore SHA-1. No, that is not correct for sign in with Google. There are several SHA-1 signatures associated with an application, but only the App signing key certificate is necessary for production. The tutorial leaves the task of getting the correct SHA-1 to the API Credentials page, but the API Credentials page does not communicate the requirements for sign in with Google.
This is not the case for debug/development credentials. The way to build those credentials is to use the SHA-1 given by the debug keystore. The command (found on the API Credentials Page) is
keytool -keystore path/to/debug/keystore -list -v
the default password is “android” and teh debug keystore is found in the application project root directory. The debug keystore will have a different SHA-1 for each developer, so you do need to set up one for each developer.
Note that the ownership of the last one can be verified. This is not indicative that the SHA-1 is correct. In my experience, I was able to verify ownership using the production keystore SHA-1 intermittently. It would sometimes verify ownership successfully and sometimes it would fail. Only the correct SHA-1 consistently verified with Google.
As a final note on this topic, the only WEB_CLIENT_ID
you need to use is the one for the web application ID. The
others are not necessary for this. I have not yet found a use for these.
2. Filtering Google IDs When Instantiating a Google Sign in Request
In the tutorial
section Instantiate a Google sign-in request,
there is a code block for creating and configuring the GetGoogleIdOption
s returned by the request, copied below.
val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(WEB_CLIENT_ID)
.setAutoSelectEnabled(true)
.setNonce(<nonce string to use when generating a Google ID token>)
.build()
Notice the first filter, .setFilterByAuthorizedAccounts(true)
. This is a bad option to use unless handled
properly. This option is a convenience for your users where it will filter only to accounts that have been already
authorized for this application. The issue is that new users will not have any accounts which have already authorized
your application. This will result in no credentials (and a NoCredentialException
). Further, I have not been able to
get this option to actually filter as it is supposed to. Overall, I do not recommend using this option despite it being
a convenience for returning users.
Set this option to false
instead. The setAutoSelectEnabled
option was also not necessary for me.
val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(WEB_CLIENT_ID)
.setNonce(<nonce string to use when generating a Google ID token>)
.build()
Alternatively, if you want to first try to see if the users have authorized accounts, you can create two option objects,
two request objects, and a try {...} catch {...}
around the credentialManager.getCredential
method call.
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import androidx.credentials.GetCredentialRequest
val authorizedGoogleIdOptions: GetGoogleIdOption =
GetGoogleIdOption.Builder().setFilterByAuthorizedAccounts(false)
.setServerClientId(BuildConfig.WEB_CLIENT_ID).setNonce("")
.setAutoSelectEnabled(false).build()
val allGoogleIdOptions: GetGoogleIdOption =
GetGoogleIdOption.Builder().setFilterByAuthorizedAccounts(false)
.setServerClientId(BuildConfig.WEB_CLIENT_ID).setNonce("")
.setAutoSelectEnabled(false).build()
val requestAuthorized = GetCredentialRequest.Builder()
.addCredentialOption(authorizedGoogleIdOptions).build()
val requestAll = GetCredentialRequest.Builder()
.addCredentialOption(allGoogleIdOptions).build()
viewModelScope.launch {
try {
val result = credentialManager.getCredential(
request = requestAuthorized,
context = context,
)
// Handle sign in
} catch (e: NoCredentialException) {
try {
val result = credentialManager.getCredential(
request = requestAll,
context = context,
)
// Handle sign in
} catch (e: GetCredentialException) {
Log.e(TAG, "failed", e)
// Handle failure
}
} catch (e: GetCredentialException) {
Log.e(TAG, "failed", e)
// Handle failure
}
The code above can be better abstracted to avoid the cruft of nesting the success and error handling in the top level function, but the locality here shows the entirety of the logic in one place.
As a small side note, most applications I have looked at handle a canceled request (which
throws GetCredentialCancellationException
, a subclass of GetCredentialException
) with a toast at the bottom of the
screen.
3. Imports
The documentation does not really include anything in the way of specifying where the imports come from, which can lead
to some searching and confusion. Some Classes exist across android.credentials
, android.service.credentials
,
and androidx.credentials
. My understanding is that the former two are for API 13 and newer, while older versions will
rely on the latter. I do not use the former two anywhere in the pinplanet app. Here are my relevant imports:
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.credentials.CustomCredential
import androidx.credentials.GetCredentialResponse
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.GoogleIdTokenCredential
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
Portions of this page are reproduced from or modifications based on work created and shared by the Android Open Source Project and used according to terms described in the Creative Commons 2.5 Attribution License.