microg/GmsCore · warning · RequestHandlingException

NOT_ALLOWED_ERR

NOT_ALLOWED_ERR

Error message

An excluded credential has already been registered with the device

What it means

During registration, ScreenLockTransportHandler checks the request's excludeList (credentials the RP says are already registered). If any excluded credential already exists on this device — either in the key store (store.containsKey) or the known-registration database — it throws RequestHandlingException(ErrorCode.NOT_ALLOWED_ERR) with this message. This mirrors the WebAuthn 'excluded credentials' behavior: the authenticator refuses to create a duplicate credential.

Source

Thrown at play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt:120

    ) = AuthenticatorData(
        rpId.toByteArray().digest("SHA-256"),
        userPresent = userPresent,
        userVerified = userVerified,
        signCount = signCount,
        attestedCredentialData = credentialData
    )

    suspend fun register(
        options: RequestOptions,
        callerPackage: String
    ): AuthenticatorResponseWithUser<AuthenticatorAttestationResponse> {
        if (options.type != RequestOptionsType.REGISTER) throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR)
        val knownRegistrationInfo = database.getKnownRegistrationInfo(options.rpId)
        for (descriptor in options.registerOptions.excludeList.orEmpty()) {
            val credentialBase64 = descriptor.id.toBase64(Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE)
            val excluded = knownRegistrationInfo.any { it.credential == credentialBase64 }
            if (store.containsKey(options.rpId, descriptor.id) || excluded) {
                throw RequestHandlingException(
                    ErrorCode.NOT_ALLOWED_ERR,
                    "An excluded credential has already been registered with the device"
                )
            }
        }
        val (clientData, clientDataHash) = getClientDataAndHash(activity, options, callerPackage)
        val keyId = store.createKey(options.rpId, clientDataHash)
        val publicKey =
            store.getPublicKey(options.rpId, keyId) ?: throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR)

        // We're ignoring the signature object as we don't need it for registration
        val signature = getActiveSignature(options, callerPackage, keyId)

        val skipAttestation = options.registerOptions.skipAttestation
        val useAndroidKey = !skipAttestation && SDK_INT >= 24 &&
            runCatching { store.getCertificateChain(options.rpId, keyId).hasValidLeafCertificate() }.getOrDefault(false)
        val useSafetyNet = !skipAttestation && SDK_INT < 24
        val aaguid = if (useAndroidKey || useSafetyNet) AAGUID else ByteArray(16)

View on GitHub (pinned to 157c9d86ac)

Solutions

  1. Treat this as 'credential already exists': catch the exception and proceed to the sign/authentication flow with the existing credential instead of registering again.
  2. If re-registration is genuinely intended, drop the existing credential or send an empty excludeList after confirming with the user.
  3. Check the device's known registrations for the rpId before initiating registration to detect the duplicate upfront.

Example fix

// before
screenLockHandler.register(options, callerPackage)
// after
try {
    screenLockHandler.register(options, callerPackage)
} catch (e: RequestHandlingException) {
    if (e.errorCode == ErrorCode.NOT_ALLOWED_ERR && e.message?.contains("excluded credential") == true) {
        signInWithExistingCredential(options.rpId)
    } else throw e
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
    screenLockHandler.register(options, callerPackage)
} catch (e: RequestHandlingException) {
    if (e.errorCode == ErrorCode.NOT_ALLOWED_ERR &&
        e.message?.contains("excluded credential") == true) {
        signInWithExistingCredential(options.rpId)
    } else throw e
}

Prevention

When it happens

Trigger: Calling register() with options.registerOptions.excludeList containing a credential id that store.containsKey(options.rpId, id) reports present, or whose base64 form matches an entry in database.getKnownRegistrationInfo(options.rpId).

Common situations: The user re-runs a registration flow they already completed; the RP frontend re-sends registration after a partial success; sync restored credentials so the device already holds the excluded credential.

Related errors


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