{"record":{"id":"423fc11c19f996f7","repo":"microg/GmsCore","slug":"null","errorCode":null,"errorMessage":"null","messagePattern":"null","errorType":"exception","errorClass":"MissingPinException","httpStatus":null,"severity":"info","filePath":"play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt","lineNumber":232,"sourceCode":"            // PREFERRED is the default, according to the standard\n            // https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification\n            // If preferred, only return true if connection is capable of user verification\n            else -> connection.hasClientPin || connection.hasUserVerificationSupport\n        }\n        // If the authenticator has a built-in verification method, let that take precedence over\n        // client PIN\n        val requiresPin = requireUserVerification && !connection.hasUserVerificationSupport && connection.hasClientPin\n\n        val (response, keyHandle) = when {\n            connection.hasCtap2Support && (requireResidentKey || requiresPin) -> {\n                try {\n                    var pinToken: ByteArray? = null\n\n                    // If we previously requested a pin and the user cancelled it (ie. pinRequested\n                    // is true and pin is still null), don't throw the exception, and pass the request\n                    // to the authenticator without a pin.\n                    if (requiresPin && !pinRequested && pin == null) {\n                        throw MissingPinException()\n                    }\n\n                    if (requiresPin && pin != null && SDK_INT >= 23) {\n                        pinToken = ctap2getPinToken(connection, pin)\n                    }\n\n                    // Authenticators seem to give a response even without a PIN token, so we'll allow\n                    // the client to call this even without having a PIN token set\n                    ctap2register(connection, options, clientDataHash, requireResidentKey, requireUserVerification, pinToken)\n                } catch (e: Ctap2StatusException) {\n                    if (e.status == 0x36.toByte()) {\n                        throw MissingPinException()\n                    } else if (e.status == 0x31.toByte()) {\n                        throw WrongPinException()\n                    } else {\n                        throw e\n                    }\n                }","sourceCodeStart":214,"sourceCodeEnd":250,"githubUrl":"https://github.com/microg/GmsCore/blob/157c9d86ac46c195a86c2f15ab55c84036223f95/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt#L214-L250","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Catch MissingPinException, prompt the user for the device PIN, and retry register() with pin set and pinRequested = true.","Set userVerification to 'discouraged'/'preferred' in the request if the flow does not truly require UV, avoiding the PIN path.","Detect the authenticator's PIN requirement up front (ctap2 clientPin info) and collect the PIN before the first attempt.","Tell the user they can remove/set the PIN via their platform's security-key manager if PIN entry is undesirable."],"exampleFix":"// before\nval resp = handler.register(options, callerPackage)\n// after\nvar pin: String? = null\nvar pinRequested = false\nval resp = try {\n    handler.register(options, callerPackage, pinRequested, pin)\n} catch (e: MissingPinException) {\n    pinRequested = true\n    pin = promptUserForPin() // AuthenticatorActivity PIN dialog\n    handler.register(options, callerPackage, pinRequested, pin)\n}","handlingStrategy":"try-catch","validationCode":"// pre-check authenticator options via ctap2 getInfo: if clientPin set and uv unsupported, PIN will be needed","typeGuard":null,"tryCatchPattern":"try { handler.register(options, pkg, pinRequested, pin) } catch (e: MissingPinException) { val pin = promptPin(); handler.register(options, pkg, true, pin) }","preventionTips":["Probe the authenticator's clientPin/UV capabilities before the ceremony","Only set userVerification='required' when the flow truly needs it","Always drive register/sign through the activity flow that can show the PIN dialog"],"tags":["fido","webauthn","ctap2","pin","user-verification"],"backgroundTag":"authentication-required","analyzedSha":"157c9d86ac46c195a86c2f15ab55c84036223f95","analyzedAt":"2026-09-06T17:27:33.892Z","contentChangedAt":"2026-09-06T17:27:33.892Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}