Skip to main content

User verification

Introduction

Passkeys are essentially private keys stored on a device or cloud account. Authentication using a passkey demonstrates a single authentication factor - "something I have". A malicious actor could use a victims (unlocked) device to authenticate using their stored passkey.

You can specify whether a secondary authentication factor should also be employed. Secondary authentication is known as user verification, and requires the user to locally authenticate against their device.

info

Strictly speaking we can't guarantee a user will be authenticated using biometrics. The specifications simply require the device to verify the user:

User verification MAY be instigated through various authorization gesture modalities; for example, through a touch plus pin code, password entry, or biometric recognition (e.g., presenting a fingerprint)

In practice though, all devices that have biometric capabilities e.g. Touch ID will use them to verify the user.

warning

In the context of passkeys, user verification simply means asking the device to verify the current user has permission to use the passkey. It does not refer to any form of identify verification. We debated using the term "secondary authentication" instead of user verification as we feel it's more appropriate, however user verification is the term used in the underlying WebAuthn spec and we decided not to diverge.

Verification levels

The passkey specification allows developers to choose from one of 3 options:

  • Required - For this particular operation the user must authenticate
  • Preferred - The user must authenticate if the device has this capability
  • Discouraged - The user should not be prompted to authenticate

Setting the level

The passlock client library allows you to specify the user verification level by passing an option to the register or authenticate function:

login.ts
await authenticatePasskey({ ... userVerification: "preferred" })

Which level should I choose?

TL;DR

In most cases, preferred strikes the best balance between security and usability.

It's tempting to go for required as it's the most secure. However, be aware of a few gotchas:

  • Not all devices support biometrics
  • Some devices may be temporarily unable to perform biometric authentication:
    • Laptop with a fingerprint sensor in clamshell mode
    • Smartphone in "driving" mode / Apple CarPlay / Android Auto

To complicate matters, the passkey specification doesn't actually mandate biometric authentication, just "user verification". A password protected passkey satisfies the spec. This means that a device may choose to prompt the user for a password if biometrics aren't available. Alternatively it could throw an error.

To further complicate things, the device may prompt for a password during a registration request, yet error out during a registration request, or vice versa 🤯

For these reasons we recommend a level of preferred.

Mix and match levels

The verfication level can be set on a case by case basis. For example you could default to preferred then set a "verified device" cookie on the device. Subsequent calls could use discouraged to ensure a frictionless experience.

Was the user actually verified?

If you choose a setting of preferred (recommended) it's still useful to know whether the device performed user verification or not. This way you could choose to perform some additional checks (e.g. sending a one time code) to users who can't be verified.

You can find this information in the authStatement that is returned when you exchange your authentication token.:

{
...
"authStatement": {
"authType": "passkey",
"userVerified": false,
"authTimestamp": "2024-01-25T12:01:07.295Z"
},
}

Global overrides

In the (hopefully unlikely) event that you experience a security breach, you might want to quickly bump all authentication operations up to a more stringent level of [user verification][user-verification]. In this scenario, you wouldn't want to wait until you've pushed new code, flushed CDNs etc.

That's why we've introduced a tenancy wide "floor". Client side code can request any level of verification, but the Passlock backend will use either the requested level, or the tenancy override, whichever is the most stringent.

Changes to the tenancy override take effect immediately.

warning

User verifcation can (and usually should) be specified during frontend registration or authentication calls. This tenancy wide override is really intended for emergencies. Please see the user verification page to learn more.