userVerification deep dive

This document discusses what userVerification is in WebAuthn, and the browser behaviors that result when userVerification is specified during passkey creation or authentication.

Passkeys are built on public key cryptography. By creating a passkey, a public-private key pair is generated, the private key is stored by the passkey provider, and the public key is returned to the relying party's (RP) server to store. The server can authenticate a user by verifying a signature signed by the same passkey using the paired public key. The "user present" (UP) flag on a public key credential proves that someone interacted with the device during the authentication.

User verification is an optional layer of security that seeks to assert that the correct person was present during authentication, not just some person, as user presence asserts. On smartphones, this is usually done by using the screen-lock mechanism, whether that be a biometric or either a PIN or password. Whether user verification was performed is reported in the "UV" flag that is returned in the authenticator data during passkey registration and authentication

A screenshot of a user verification dialog on iCloud Keychain on macOS. The dialog prompts the user to sign in using Touch ID, displaying the origin requesting authentication, as well as the username. At the top right of the dialog is a button labeled 'Cancel'.
User verification dialog on iCloud Keychain on macOS.
A screenshot of a user verification dialog on Chrome for Android. The dialog prompts the user to verify their identity by using facial recognition or fingerprint detection, and displays the origin requesting authentication. At the bottom left is an option to verify using a PIN.
User verification dialog on Android Chrome.

How UP and UV are validated on the server

The user presence (UP) and user verified (UV) boolean flags are signaled to the server in the authenticator data field. During authentication, the contents of the authenticator data field can be validated by verifying the signature using the stored public key. As long as the signature is valid, the server can consider the flags genuine.

A depiction of the authentication data structure. From left to right, each section of the data structure reads 'RP ID HASH' (32 bytes), 'FLAGS' (1 byte), 'COUNTER' (4 bytes, big-endian uint32), 'ATTESTE CRED. DATA' (variable length if present), and 'EXTENSIONS' (variable length if present (CBOR)). The 'FLAGS' section is expanded to show a list of potential flags, labeled from left to right: 'ED', 'AT', '0', 'BS', 'BE', 'UV', '0', and 'UP'.
Authenticator data fields in a public key credential.

On passkey registration and authentication, the server should examine that the UP flag is true, and whether the UV flag is true or false, depending on the requirement.

Specifying the userVerification parameter

Per the WebAuthn specification, the RP can request a user verification with a userVerification parameter on both credential creation and assertion. It accepts 'preferred', 'required', or 'discouraged' which respectively means:

  • 'preferred' (default): Using a user verification method on the device is preferred, but can be skipped if it's not available. The response credential contains a UV flag value of true if user verification was performed, and false if UV was not performed.
  • 'required': Invoking a user verification method available on the device is required. If one is not available, the request fails locally. This means the response credential always returns with the UV flag set to true.
  • 'discouraged': Using a user verification method is discouraged. However, depending on the device, user verification may be performed anyway, and the UV flag can contain true or false.

Sample code for passkey creation:

const publicKeyCredentialCreationOptions = {
  // ...
  authenticatorSelection: {
    authenticatorAttachment: 'platform',
    residentKey: 'required',
    requireResidentKey: true,
    userVerification: 'preferred'
  }
};

const credential = await navigator.credentials.create({
  publicKey: publicKeyCredentialCreationOptions
});

Sample code for passkey authentication:

const publicKeyCredentialRequestOptions = {
  challenge: /* Omitted challenge data... */,
  rpId: 'example.com',
  userVerification: 'preferred'
};

const credential = await navigator.credentials.get({
  publicKey: publicKeyCredentialRequestOptions
});

Which option should you choose for userVerification?

The userVerification value you should use depends on your application requirements, as well as your user experience needs.

When to use userVerification='preferred'

Use userVerification='preferred' if you prioritize user experience over protection.

There are environments where user verification is more friction than protection. For example, on macOS where Touch ID is not available (because the device doesn't support it, it's disabled, or the device is in clamshell mode), the user is asked to enter their system password instead. This causes friction, and the user may abandon authentication entirely. If eliminating friction is more important to you, use userVerification='preferred'.

A screenshot of a passkey dialog on macOS that appears when Touch ID is not available. The dialog contains info such as the origin requesting authentication, as well as the username. At the top right of the dialog is a button labeled 'Cancel'.
A passkey dialog displayed on macOS when Touch ID is not available.

With userVerification='preferred', the UV flag is true if user verification is successfully performed, and false if user verification is skipped. For example, on macOS where Touch ID is not available, it asks the user to click a button to skip user verification, and the public key credential includes a false UV flag.

The UV flag can then be a signal in your risk analysis. If the sign-in attempt seems risky due to other factors, you may want to present additional sign-in challenges to the user if user verification was not performed.

When to use userVerification='required'

Use userVerification='required' if you think both UP and UV are absolutely necessary.

A downside of this option is that the user may experience more friction when signing in. For example, on macOS where Touch ID is not available, the user is asked to enter their system password.

With userVerification='required', you can ensure that user verification is performed on the device. Make sure the server verifies that the UV flag is true.

Conclusion

By leveraging user verification, passkey-relying parties can gauge the likelihood of the device owner signing in. It's their choice whether to require user verification, or make it optional depending on how critical the fallback sign-in mechanism impacts the user flow. Make sure the server checks the UP flag and UV flag for passkey user authentication.