Client Hints
The WebAuthn Client Hints feature allows a Relying Party to request a more specific credential manager or passkey selection experience from the WebAuthn client.
When creating a passkey, WebAuthn Clients display a credential manager selection screen asking users to choose where to store their new passkey. The selector typically defaults to local credential managers because they offer immediate availability and support for synced passkeys, the default credential type in unmanaged, consumer contexts.
During a sign in flow, the WebAuthn client will do its best to help the user select a passkey which is immediately available, and fall back to an external authenticator selection screen. This typically shows an option for FIDO Cross-Device Authentication and security keys. In environments where only security keys are allowed, having additional options can confuse users and lead to unnecessary steps.
The WebAuthn Client Hints feature allows a Relying Party to request a more predictable experience based on their requirements. It is important to note that this is only a hint, and is not used to enforce security policy.
NOTE: Additional use cases will be added in the future.
The primary use case for WebAuthn Client Hints is in workforce and high assurance consumer scenarios where creating passkeys on security keys is required by policy for all or a specific group of users. In these scenarios, if a user were to attempt to create a passkey on a different type of authenticator/credential manager (ex: using cross-device and saving to their personal credential manager on their phone), the Relying Party would reject the passkey, creating confusion for the user, and leaving them somewhat stranded.
There are three primary scenarios which will be referenced throughout this page:
Below is a mapping of the scenarios from the beginning of this article and their solutions.
SCENARIO | SOLUTION |
---|---|
1 (required for all) | Use Client Hints with your existing enrollment experience |
2 (required for some) | Use Client Hints with your existing enrollment experience |
3 (required for none) | Use Client Hints with a dedicated button |
In both scenarios 1 and 2, Client Hints is used to prefer the security key experience during passkey creation for all users.
Simply adapt your existing passkey creation flow to use the hints parameter as shown in the sample code below:
-- snip --
<button type="button" onclick="createPasskeyOnSecurityKey()">Create passkey</button>
-- snip --
<script>
async function createPasskeyOnSecurityKey() {
const credential = await navigator.credentials.create({
publicKey: {
challenge: "challenge-from-server",
rp: { }, // omitted
user: { }, // omitted
pubKeyCredParams: [], // omitted
timeout: 60000,
hints: [ "security-key" ], // this is the WebAuthn Client Hints parameter
authenticatorSelection: {
residentKey: "required",
userVerification: "preferred",
authenticatorAttachment: "cross-platform"
}
}
});
// omitted
};
</script>
For scenario 3, it is recommended to use dedicated buttons for “This device” vs “Security Key”. This can help give the user a more predictable experience based on their selection.
Sample code:
-- snip --
<h1>Create a passkey!</h1>
<p>Would you like to create your passkey on this device or an external USB security key?</p>
<button type="button" onclick="createPasskeyOnLocalDevice()">This device</button>
<button type="button" onclick="createPasskeyOnSecurityKey()">USB Security Key</button>
-- snip --
<script>
async function createPasskeyOnSecurityKey() {
const credential = await navigator.credentials.create({
publicKey: {
challenge: "challenge-from-server",
rp: { }, // omitted
user: { }, // omitted
pubKeyCredParams: [], // omitted
timeout: 60000,
hints: [ "security-key" ],
authenticatorSelection: {
residentKey: "required",
userVerification: "preferred",
authenticatorAttachment: "cross-platform"
}
}
});
// omitted
};
async function createPasskeyOnLocalDevice() {
const credential = await navigator.credentials.create({
publicKey: {
challenge: "challenge-from-server",
rp: { }, // omitted
user: { }, // omitted
pubKeyCredParams: [], // omitted
timeout: 60000,
hints: [ "client-device" ],
authenticatorSelection: {
residentKey: "required",
userVerification: "preferred",
authenticatorAttachment: "platform"
}
}
});
// omitted
};
</script>
While Client Hints primarily targets passkey creation flows, it also supports specific authentication scenarios. Choose your approach based on your configuration and security requirements. Below is a mapping of the scenarios from the beginning of this article and their solutions.
SCENARIO | SOLUTION |
---|---|
1 (required for all) | Use Client Hints |
2 (required for some) | Use identifier-first plus an allow list |
3 (required for none) | Don’t use Client Hints or an allow list; let the WebAuthn Client do its thing |
In scenario 1, Client Hints is used to prefer the security key experience for all users.
Sample code for scenario 1:
<!-- additional code omitted -->
<button type="button" onclick="signIn()">Sign in with a passkey</button>
<!-- additional code omitted -->
<script>
async function signIn() {
const credential = await navigator.credentials.get({
publicKey: {
challenge: "challenge-from-server",
rpId: "", // omitted in example
timeout: 60000,
hints: [ "security-key" ]
}
});
// additional code omitted
};
</script>
In scenario 2, an identifier-first flow is used where the user enters their username, and a request is made to the server for a list of credential IDs for the user. These are then passed in to the WebAuthn request (along with their transports) in the allowCredentials
list. If only passkeys on security keys are included, the WebAuthn Client will show the security key experience.
Sample code:
// call this after the user has been identified
// and the credential IDs have been looked up
async function signIn() {
const credential = await navigator.credentials.get({
publicKey: {
challenge: "challenge-from-server",
rpId: "", // omitted in example
timeout: 60000,
allowCredentials: [
// the passkeys created on security keys
// that are linked to the user's account
{
"id": "qx30Jbh0IJFq4Y3i7r5DY7aECNDnlH4-lldmDeshvTVFZolxwIgIBQfnoxrJKe1z",
"type": "public-key",
"transports": [ "nfc", "usb" ]
},
{
"id": "3WiVDBng9vWlRX8Zkarc-4vpVNt8ysHFhHyYNldgf26n7eHJ4TN9AsBOr36Lsnl2",
"type": "public-key",
"transports": [ "usb" ]
}
]
}
});
// additional code omitted
};
In scenario 3, there is no special configuration, allowing the WebAuthn Client to do what it believes is best based on available context that only it has.
Sample code:
async function signIn() {
const credential = await navigator.credentials.get({
publicKey: {
challenge: "challenge-from-server",
rpId: "", // omitted in example
timeout: 60000,
}
});
// additional code omitted
};
Coming Soon
WebAuthn Client Hints support is rolling out across the ecosystem. Details about support WebAuthn Clients can be found in the Device Support matrix.
A presentation by Tim Cappalli at Authenticate 2024 which dives into a WebAuthn Client’s selection logic: “Peeling back the passkeys onion”.
WebAuthn Spec Reference