I'm getting the following exception for a specific account on a specific physical device in release only. I'm not sure how to continue debugging the issue.
In an Android app (Kotlin, Jetpack Compose) -- I'm getting a Google account idToken using GoogleSignIn -- I'm sending the idToken to the server for validation
On a nodejs server -- I'm validating the token with GoogleAuth.OAuth2Client.verifyIdToken()
This works in all cases I have encountered except one. A specific account on a specific device (we'll call the device at issue Device A and the account at issue Account A) with the package in release mode only, against the production server.
With that account/device combination (Device A + Account A) I get the following exception on the server:
Exception
Error: No pem found for envelope: {"alg":"RS256","kid":"4qa48u3nvj3498fru03984nmr20938yf7c63b4f8","typ":"JWT"}
at OAuth2Client.verifySignedJwtWithCertsAsync (.../node_modules/google-auth-library/build/src/auth/oauth2client.js:608:19)
at OAuth2Client.verifyIdTokenAsync (.../node_modules/google-auth-library/build/src/auth/oauth2client.js:444:34)
at processTicksAndRejections (internal/process/task_queues.js:93:5)
at async Object.VerifyIdToken (.../server_auth_file.js:666:18)
Client Code (does not break except for 500 code)
import ...
class LoginActivity : ComponentActivity() {
private lateinit var signinActivity: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
signinActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult())
{ result: ActivityResult ->
try{
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
handleSignInResult(task)
} catch (err:ApiException){
// todo -- check error code against GoogleSignInStatusCodes
val intent = Intent()
intent.putExtra("success",false)
intent.putExtra("message","cancelled")
setResult(RESULT_CANCELED, intent)
finish()
}
}
signIn()
}
private fun getGoogleSignInClient(): GoogleSignInClient {
val gso =
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken((this as Context).resources.getString(R.string.APP_CLIENT_ID))
.requestEmail()
.build()
return GoogleSignIn.getClient(this, gso)
}
private fun signIn() {
val signInIntent = getGoogleSignInClient().signInIntent
signinActivity.launch(signInIntent)
}
private fun handleSignInResult(completedTask: Task<GoogleSignInAccount>) {
val accountData = completedTask.getResult(
ApiException::class.java
)
// refresh server access token
val accessToken = refreshAccessToken(applicationContext,accountData.idToken)
val intent = Intent()
intent.putExtra("accessToken",accessToken as Serializable)
setResult(RESULT_OK, intent)
finish()
}
private fun refreshAccessToken(context:Context,idToken: String): AccessToken {
// ==========================================
// ==========================================
// VVVVVV Breaks on this rest call VVVVVVVV
val response = restCallToServer("get/accesstoken", "authtoken=${idToken}")
// ^^^^^^ Breaks on this rest call ^^^^^^^^
// ==========================================
// ==========================================
// creates a serializable token used by rest of app from data provided by server
val accessToken = AccessToken(response!!.getJSONObject("accessToken"))
return accessToken;
}
Server code (breaks with exception listed above)
const GOOGLE_OAUTH_CLIENT = new GoogleAuth.OAuth2Client(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET);
async function VerifyIdToken(idToken,appName){
let credential,googuid;
// vvvvv exception here vvvvv
// vvvvv exception here vvvvv
// vvvvv exception here vvvvv
const ticket = await GOOGLE_OAUTH_CLIENT.verifyIdToken({
idToken,
audience:GOOGLE_CLIENT_ID
});
// ^^^^^ exception here ^^^^^
// ^^^^^ exception here ^^^^^
// ^^^^^ exception here ^^^^^
const credential = ticket.getPayload();
const googuid = credential.sub;
const email = credential.email;
// ...
// access_token generated and tied to user account
// ...
// access_token sent back to client
}
Except for this one case (Device A + Account A + released package)
- No problem on VM debug client -> dev server (local testing)
- No problem on VM debug client -> production server (local testing against live server)
- No problem on VM release client -> production server (Google automated package testing)
- No problem on physical debug client -> production server (Account A + Device A works as expected)
- Physical release client -> production server (A.A + other devices, D.A + other accounts, plus all other combinations seen so far work; D.A + A.A FAILS)
Things I've tried
Cannot currently test a physical device against a dev server
Other accounts (except for Account A) on Device A work without issue
Account A on other devices works without issue
Physical devices work (except D.A + A.A + Release)
Virtual devices work
Debug packages work (including D.A + A.A)
Release packages work (except D.A + A.A)
The automated testing flows Google employs when releasing the app have no issue signing in
Notes
- This is new behavior. I'm not sure what changed to make it show up
- Account A on Device A works with a debug client
- I have a another flavor on the same version that is not exhibiting this in debug or release
- Cutting new versions/AABs doesn't fix the issue
It seems related to the app signature itself (and/or Oauth client ID), the device itself and the account itself. Changing any one of these seems to exhibit normal behavior.
I'm not sure if there's some kind of certificate cached on the device that's causing it to fail. I'm not sure how the KIDs are generated, so insight there might help. I have not tried factory resetting the device, but I would much prefer not to, and I'd like to ascertain for certain that this will not impact other devices in the wild (even though it has yet to, under very limited testing).
The device is an old Samsung GS8 on Android 9 (Pie/28).