Understanding WebAuthn credential protection policy
This post assumes you're already familiar with the basics of WebAuthn.
When creating a WebAuthn credential, you can specify whether it should be discoverable using the
residentKey
option.
const credential = await navigator.credentials.create({
publicKey: {
authenticatorSelection: {
residentKey: "required",
// ...
},
// ...
},
});
However, the relying party cannot control when or how the credential can be discovered. You may
want it to become discoverable only after user verification and hide the account’s existence from
snooping users. This can be especially important for security keys, where unlike devices or
password managers that usually require initial authentication, physical possession alone is often
sufficient to reveal registered credentials. To address this, the CTAP 2.1 specification defines a
new credential protection extension, available through the
credentialProtectionPolicy
extension input. CTAP is the specification that defines how platforms (devices/browsers) and
roaming authenticators (security keys) interact.
const credential = await navigator.credentials.create({
publicKey: {
extensions: {
credentialProtectionPolicy: "userVerificationRequired",
enforceCredentialProtectionPolicy: true,
},
// ...
},
});
If the default value userVerificationOptional is used, the credential can be
discovered and used without user verification. If
userVerificationOptionalWithCredentialIDList
is used, the credential cannot be discovered without user verification, but it can still be
discovered and used without user verification if the credential ID is provided by the relying
party. This matches the security of non-discoverable credentials. Finally,
userVerificationRequired
indicates that the credential cannot be discovered or used without user verification.
It’s important to highlight that the extension controls credential discovery within the authenticator. It is still up to the relying party to verify whether the assertion was made with user verification if they require it.
The related enforceCredentialProtectionPolicy extension input configures what should
happen if the authenticator does not support credential protection policy. If set to
true, the operation will fail if it cannot create a credential at the specified
security level. Note that if you use a non-roaming authenticator that does not support
credentialProtectionPolicy
but the browser does, the request will be rejected. As such, this should only be set to
true
if you want to allow roaming authenticators.
As for browser support for the extension inputs, Chrome and Firefox support them, while Safari does not and will simply ignore them.
Browsers can also silently apply a default value if the relying party does not specify one, which is specifically the case in Chrome.
If residentKey is preferred or
required, Chrome uses
userVerificationOptionalWithCredentialIDList. As noted, this helps prevent someone
with physical access to the authenticator from seeing which accounts are registered on it.
If residentKey is required and
userVerification
is preferred, Chrome will use
userVerificationRequired
instead. The confusing part is that this is not related to credential discovery, but rather serves
as a safety measure to enforce user verification. Chrome assumes the credential is likely to be
used as a single authentication step, since
preferred
is commonly used even for passkey authentication. However, because user verification is still
optional and it is up to the relying party to check for it, if the server does not properly
enforce user verification during authentication, someone could sign in as the user with only
physical access to the authenticator.
The specific behavior is documented in the Chromium documentation.