microg/GmsCore · error · IllegalStateException
err.errorMessage ?: err.errorCode.toString()
Error message
err.errorMessage ?: err.errorCode.toString()
What it means
microG's IdentityCredentialChooserActivity converts a FIDO2 authenticator error response into an IllegalStateException whose message is the FIDO error message (or the error code if no message). It is thrown in handleFidoResult when the FIDO2 API returns an AuthenticatorErrorResponse instead of a credential, so the caller of CredentialManager receives an exception instead of a public-key credential result.
Source
Thrown at play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentityCredentialChooserActivity.kt:198
REQ_CODE_FIDO -> handleFidoResult(resultCode, data)
REQ_CODE_SIGN_IN -> handleSignInResult(resultCode, data)
else -> finishWithGetException(GetCredentialUnknownException("Unexpected requestCode=$requestCode"))
}
}
private fun handleFidoResult(resultCode: Int, data: Intent?) {
Log.d(TAG, "handleFidoResult: data: $data")
if (resultCode != RESULT_OK || data == null) {
return if (isCreatePath) finishWithCreateException(CreateCredentialUnknownException("Passkey flow canceled"))
else finishWithGetException(GetCredentialCancellationException("Passkey flow canceled"))
}
runCatching {
val credentialBytes = data.getByteArrayExtra(FIDO2_KEY_CREDENTIAL_EXTRA)
?: throw IllegalStateException("FIDO returned no credential")
val publicKeyCredential = com.google.android.gms.fido.fido2.api.common.PublicKeyCredential
.deserializeFromBytes(credentialBytes)
(publicKeyCredential.response as? AuthenticatorErrorResponse)?.let { err ->
throw IllegalStateException(err.errorMessage ?: err.errorCode.toString())
}
val json = publicKeyCredential.toJson()
val credData = Bundle().apply {
putString(if (isCreatePath) PUBKEY_RES_REG_JSON_KEY else PUBKEY_RES_AUTH_JSON_KEY, json)
}
Log.d(TAG, "handleFidoResult: $credData")
finishWithCredential(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL, credData)
}.onFailure { e ->
Log.e(TAG, "handleFidoResult failed", e)
val msg = e.message ?: "FIDO result error"
if (isCreatePath) finishWithCreateException(CreateCredentialUnknownException(msg))
else finishWithGetException(GetCredentialUnknownException(msg))
}
}
private fun handleSignInResult(resultCode: Int, data: Intent?) {
if (resultCode != RESULT_OK || data == null) {
return finishWithGetException(GetCredentialCancellationException("Sign-in canceled"))View on GitHub (pinned to 157c9d86ac)
Solutions
- Inspect the IllegalStateException message (it mirrors the FIDO errorMessage/errorCode) and map it to a FidoErrorResponse code to decide whether to retry or cancel
- Handle user-cancellation gracefully: treat the error as a cancelled credential flow, not a crash
- Verify PublicKeyCredentialRequestOptions / PublicKeyCredentialCreationOptions are well-formed (rp.id matches the calling app's domain, valid challenge) before launching FIDO
- Retest on a device with an actual or virtual FIDO2 authenticator
Example fix
// before
throw IllegalStateException(err.errorMessage ?: err.errorCode.toString())
// after
val errCode = err.errorCode
if (errCode == ErrorCode.USER_CANCELED) {
finishWithGetException(GetCredentialCancellationException("FIDO canceled by user"))
} else {
throw IllegalStateException("FIDO error ${errCode}: ${err.errorMessage}")
} Defensive patterns
Strategy: try-catch
Validate before calling
// before launching FIDO, ensure options are valid
require(options.rp.id == callingPackageDomain) { "rp.id must match app domain" }
require(options.challenge.isNotEmpty()) { "challenge required" } Type guard
fun isAuthenticatorErrorResponse(resp: AuthenticatorResponse) = resp is AuthenticatorErrorResponse
Try / catch
try {
val cred = credentialsManager.getCredential(request)
} catch (e: IllegalStateException) {
// FIDO returned AuthenticatorErrorResponse; inspect message for FIDO code
if (e.message?.contains("cancel", ignoreCase = true) == true) return canceled()
throw e
} Prevention
- Always handle the user-cancel FIDO path as a normal flow, not a crash
- Validate rp.id, challenge and user verification requirements before starting FIDO
- Test on devices/emulators that actually provide a FIDO2 authenticator
- Keep microG updated for FIDO result-handling fixes
When it happens
Trigger: The FIDO2 fido2Api.getPendingIntent flow completes and onActivityResult passes the result intent to handleFidoResult; the deserialized PublicKeyCredential.response is an AuthenticatorErrorResponse (user cancelled the authenticator prompt, timeout, no authenticator available, or the authenticator rejected the request).
Common situations: User dismisses the biometric/security-key prompt; device has no FIDO2 authenticator or the relying party id/challenge is invalid; testing on emulators lacking hardware-backed key stores.
Related errors
- FIDO returned no credential
- Sign-in result missing credential
- deleteAll was set to true but keys were also provided
- Element in keys cannot be null or empty
- deleteAll=true but keys are provided
AI-assisted analysis of microg/GmsCore@157c9d86ac (2026-09-06).
Data as JSON: /api/errors/c9c17902471b1129.
Report an issue: GitHub.