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

  1. Inspect the IllegalStateException message (it mirrors the FIDO errorMessage/errorCode) and map it to a FidoErrorResponse code to decide whether to retry or cancel
  2. Handle user-cancellation gracefully: treat the error as a cancelled credential flow, not a crash
  3. Verify PublicKeyCredentialRequestOptions / PublicKeyCredentialCreationOptions are well-formed (rp.id matches the calling app's domain, valid challenge) before launching FIDO
  4. 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

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


AI-assisted analysis of microg/GmsCore@157c9d86ac (2026-09-06). Data as JSON: /api/errors/c9c17902471b1129. Report an issue: GitHub.