Discoverable credentials deep dive

While FIDO credentials such as passkeys aim to replace passwords, most of them can also free the user from typing in a username. This enables users to authenticate by selecting an account from a list of passkeys they have for the current website.

Earlier versions of security keys were designed as 2-step authentication methods, and required the IDs of potential credentials, thus requiring entry of a username. Credentials that a security key can find without knowing their IDs are called discoverable credentials. Most FIDO credentials created today are discoverable credentials; particularly passkeys stored in a password manager or on a modern security key.

To ensure your credentials are discoverable, specify residentKey and requireResidentKey when the passkey is created.

Relying parties (RPs) can use discoverable credentials by omitting allowCredentials during passkey authentication. In these cases, the browser or system show the user a list of available passkeys, identified by the user.name property set at creation time. If the user selects one, the user.id value will be included in the resulting signature. The server can then use that or the returned credential ID to look up the account instead of a typed username.

Account selector UIs, like the ones discussed earlier, never show non-discoverable credentials.

requireResidentKey and residentKey

To create a discoverable credential, specify authenticatorSelection.residentKey and authenticatorSelection.requireResidentKey on navigator.credentials.create() with the values indicated as follows.

async function register () {
  // ...

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

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

  // This does not run until the user selects a passkey.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // ...
}

residentKey:

  • 'required': A discoverable credential must be created. If it can't be created, NotSupportedError is returned.
  • 'preferred': The RP prefers creating a discoverable credential, but accepts a non-discoverable credential.
  • 'discouraged': The RP prefers creating a non-discoverable credential, but accepts a discoverable credential.

requireResidentKey:

  • This property is retained for backward compatibility from WebAuthn Level 1, an older version of the specification. Set this to true if residentKey is 'required', otherwise set it to false.

allowCredentials

RPs can use allowCredentials on navigator.credentials.get() to control the passkey authentication experience. There are usually three types of passkey authentication experiences:

With discoverable credentials, RPs can show a modal account selector for the user to select an account to sign in with, followed by user verification. This is suitable for passkey authentication flow initiated by pressing a button dedicated to passkey authentication.

To achieve this user experience, omit or pass an empty array to allowCredentials parameter in navigator.credentials.get().

async function authenticate() {
  // ...

  const publicKeyCredentialRequestOptions = {
    // Server generated challenge:
    challenge: ****,
    // The same RP ID as used during registration:
    rpId: 'example.com',
    // You can omit `allowCredentials` as well:
    allowCredentials: []
  };

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

  // This does not run until the user selects a passkey.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;
  
  // ...
}

Show a passkey form autofill

The modal account selector described above works well if most users use passkeys and have them available on the local device. For a user that does not have local passkeys, the modal dialog still appears and will offer the user to present a passkey from another device. While transitioning your users to passkeys, you may want to avoid that UI for users that haven't set one up.

Instead, the selection of a passkey may be folded into autofill prompts for the fields in a traditional sign-in form, alongside saved usernames and passwords. This way, a user with passkeys can "fill" the sign-in form by selecting their passkey, users with saved username/password pairs can select those, and users with neither can still type in their username and password.

This user experience is ideal when the RP is under a migration with a mixed usage of passwords and passkeys.

To achieve this user experience, in addition to passing an empty array to allowCredentials property or omitting the parameter, specify mediation: 'conditional' on navigator.credentials.get() and annotate an HTML username input field with autocomplete="username webauthn" or a password input field with autocomplete="password webauthn".

The call to navigator.credentials.get() will not cause any UI to be shown, but if the user focuses the annotated input field, any available passkeys will be included in the autofill options. If the user selects one, they will go through the regular device unlock verification, and only then will the promise returned by .get() resolve with a result. If the user doesn't select a passkey, the promise never resolves.

async function authenticate() {
  // ...

  const publicKeyCredentialRequestOptions = {
    // Server generated challenge:
    challenge: ****,
    // The same RP ID as used during registration:
    rpId: 'example.com',
    // You can omit `allowCredentials` as well:
    allowCredentials: []
  };

  const cred = await navigator.credentials.get({
    publicKey: publicKeyCredentialRequestOptions,
    signal: abortController.signal,
    // Specify 'conditional' to activate conditional UI
    mediation: 'conditional'
  });

  // This does not run until the user selects a passkey.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;
  
  // ...
}
<input type="text" name="username" autocomplete="username webauthn" ...>

You can learn how to build this user experience at Sign in with a passkey through form autofill, as well as the Implement passkeys with form autofill in a web app codelab.

Reauthentication

In some cases, such as when using passkeys for reauthentication, the identifier of the user is already known. In this case, we would like to use a passkey without the browser or OS showing any form of an account selector. This can be achieved by passing a list of credential IDs in the allowCredentials parameter.

In that case, if any of the named credentials are available locally, the user is prompted for device unlock straight away. If not, the user is prompted to present another device (a phone or a security key) that holds a valid credential.

To achieve this user experience, provide a list of credential IDs for the signing in user. The RP should be able to query them because the user is already known. Provide credential IDs as PublicKeyCredentialDescriptor objects in the allowCredentials property in navigator.credentials.get().

async function authenticate() {
  // ...

  const publicKeyCredentialRequestOptions = {
    // Server generated challenge:
    challenge: ****,
    // The same RP ID as used during registration:
    rpId: 'example.com',
    // Provide a list of PublicKeyCredentialDescriptors:
    allowCredentials: [{
      id: ****,
      type: 'public-key',
      transports: [
        'internal',
        'hybrid'
      ]
    }, {
      id: ****,
      type: 'public-key',
      transports: [
        'internal',
        'hybrid'
      ]
    }, ...]
  };

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

  // This does not run until the user selects a passkey.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;
  
  // ...
}

A PublicKeyCredentialDescriptor object consists of:

  • id: An ID of the public key credential the RP has obtained on the passkey registration.
  • type: This field is usually 'public-key'.
  • transports: A hint of transports supported by the device holding this credential, used by browsers to optimize the UI that asks the user to present an external device. This list, if provided, should contain the result of calling getTransports() during the registration of each credential.

Summary

Discoverable credentials make the passkey sign-in experience much more user friendly by allowing them to skip entering a username. With the combination of residentKey, requireResidentKey, and allowCredentials, RPs can achieve sign-in experiences that:

  • Show a modal account selector.
  • Show a passkey form autofill.
  • Reauthentication.

Use discoverable credentials wisely. By doing so, you can design sophisticated passkey sign-in experiences that users will find seamless and be more likely to engage with.