microg/GmsCore · info · MissingPinException

null

Error message

null

What it means

CTAP2 registration requires a PIN when the authenticator enforces user verification (requiresPin) but has no built-in UV. If no PIN was supplied and the client has not already asked the user for one (pinRequested false), the handler throws MissingPinException (message "null") up to the activity, which should prompt the user for the PIN and retry with it.

Source

Thrown at play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt:232

            // PREFERRED is the default, according to the standard
            // https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification
            // If preferred, only return true if connection is capable of user verification
            else -> connection.hasClientPin || connection.hasUserVerificationSupport
        }
        // If the authenticator has a built-in verification method, let that take precedence over
        // client PIN
        val requiresPin = requireUserVerification && !connection.hasUserVerificationSupport && connection.hasClientPin

        val (response, keyHandle) = when {
            connection.hasCtap2Support && (requireResidentKey || requiresPin) -> {
                try {
                    var pinToken: ByteArray? = null

                    // If we previously requested a pin and the user cancelled it (ie. pinRequested
                    // is true and pin is still null), don't throw the exception, and pass the request
                    // to the authenticator without a pin.
                    if (requiresPin && !pinRequested && pin == null) {
                        throw MissingPinException()
                    }

                    if (requiresPin && pin != null && SDK_INT >= 23) {
                        pinToken = ctap2getPinToken(connection, pin)
                    }

                    // Authenticators seem to give a response even without a PIN token, so we'll allow
                    // the client to call this even without having a PIN token set
                    ctap2register(connection, options, clientDataHash, requireResidentKey, requireUserVerification, pinToken)
                } catch (e: Ctap2StatusException) {
                    if (e.status == 0x36.toByte()) {
                        throw MissingPinException()
                    } else if (e.status == 0x31.toByte()) {
                        throw WrongPinException()
                    } else {
                        throw e
                    }
                }

View on GitHub (pinned to 157c9d86ac)

Solutions

  1. Catch MissingPinException, prompt the user for the device PIN, and retry register() with pin set and pinRequested = true.
  2. Set userVerification to 'discouraged'/'preferred' in the request if the flow does not truly require UV, avoiding the PIN path.
  3. Detect the authenticator's PIN requirement up front (ctap2 clientPin info) and collect the PIN before the first attempt.
  4. Tell the user they can remove/set the PIN via their platform's security-key manager if PIN entry is undesirable.

Example fix

// before
val resp = handler.register(options, callerPackage)
// after
var pin: String? = null
var pinRequested = false
val resp = try {
    handler.register(options, callerPackage, pinRequested, pin)
} catch (e: MissingPinException) {
    pinRequested = true
    pin = promptUserForPin() // AuthenticatorActivity PIN dialog
    handler.register(options, callerPackage, pinRequested, pin)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check authenticator options via ctap2 getInfo: if clientPin set and uv unsupported, PIN will be needed

Try / catch

try { handler.register(options, pkg, pinRequested, pin) } catch (e: MissingPinException) { val pin = promptPin(); handler.register(options, pkg, true, pin) }

Prevention

When it happens

Trigger: register() on a CTAP2 connection where requiresPin is true (requireUserVerification && !hasUserVerificationSupport && hasClientPin) and pin == null while pinRequested == false, i.e. the first attempt before any PIN prompt.

Common situations: Registering on a security key that has a PIN set but no biometrics, with an RP requesting userVerification='required'; first call in the retry loop before AuthenticatorActivity collects the PIN; caller invoking the handler directly, bypassing the PIN prompt UI.

Related errors


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