Implement passkeys with form autofill in a web app

1. Before you begin

The use of passkeys instead of passwords is a great way for websites to make their user accounts safer, simpler, and easier to use. With a passkey, a user can sign in to a website or an app by using the device's screen lock feature, such as a fingerprint, face, or device PIN. A passkey has to be created, associated with a user account, and have its public key stored on a server before a user can sign in with it.

In this codelab, you turn a basic form-based username and password sign-in into one that supports passkeys and includes the following:

  • A button that creates a passkey after the user signs in.
  • A UI that displays a list of registered passkeys.
  • The existing sign-in form that lets users sign in with a registered passkey through form autofill.

Prerequisites

What you'll learn

  • How to create a passkey.
  • How to authenticate users with a passkey.
  • How to let a form suggest a passkey as a sign-in option.

What you'll need

One of the following device combinations:

  • Google Chrome with an Android device that runs Android 9 or higher, preferably with a biometric sensor.
  • Chrome with a Windows device that runs Windows 10 or higher.
  • Safari 16 or higher with an iPhone that runs iOS 16 or higher, or an iPad that runs iPadOS 16 or higher.
  • Safari 16 or higher or Chrome with an Apple desktop device that runs macOS Ventura or higher.

2. Get set up

In this codelab, you use a service called Glitch, which lets you edit client and server-side code with JavaScript, and deploy it solely from the browser.

Open the project

  1. Open the project in Glitch.
  2. Click Remix to fork the Glitch project.
  3. In the navigation menu at the bottom of Glitch, click Preview > Preview in a new window. Another tab opens in your browser.

The Preview in a new window button in the navigation menu at the bottom of Glitch

Examine the website's initial state

  1. In the preview tab, enter a random username and then click Next.
  2. Enter a random password and then click Sign-in. The password is ignored, but you're still authenticated and land on the home page.
  3. If you want to change your display name, do so. That's all you can do in the initial state.
  4. Click Sign out.

In this state, users must enter a password every time that they log in. You add passkey support to this form so that users can sign in with the device's screen-lock functionality. You can try the end state at https://passkeys-codelab.glitch.me/.

For more information about how passkeys work, see How do passkeys work?.

3. Add an ability to create a passkey

To let users authenticate with a passkey, you need to give them the ability to create and register a passkey, and store its public key on the server.

A passkey user verification dialog appears upon passkey creation.

You want to allow the creation of a passkey after the user logs in with a password, and add a UI that lets users create a passkey and see a list of all registered passkeys on the /home page. In the next section, you create a function that creates and registers a passkey.

Create the registerCredential() function

  1. In Glitch, navigate to the public/client.js file and then scroll to the end.
  2. After the relevant comment, add the following registerCredential() function:

public/client. js

// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {

  // TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.

  // TODO: Add an ability to create a passkey: Create a credential.

  // TODO: Add an ability to create a passkey: Register the credential to the server endpoint.

};

This function creates and registers a passkey on the server.

Obtain the challenge and other options from the server endpoint

Before a passkey is created, you need to request parameters to pass in WebAuthn from the server, including a challenge. WebAuthn is a browser API that lets a user create a passkey and authenticate the user with the passkey. Luckily, you already have a server endpoint that responds with such parameters in this codelab.

  • To obtain the challenge and other options from the server endpoint, add the following code to the registerCredential() function's body after the relevant comment:

public/client.js

// TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.
const options = await _fetch('/auth/registerRequest');

The following code snippet includes sample options that you receive from the server:

{
  challenge: *****,
  rp: {
    id: "example.com",
  },
  user: {
    id: *****,
    name: "john78",
    displayName: "John",
  },  
  pubKeyCredParams: [{
    alg: -7, type: "public-key"
  },{
    alg: -257, type: "public-key"
  }],
  excludeCredentials: [{
    id: *****,
    type: 'public-key',
    transports: ['internal', 'hybrid'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
}

The protocol between a server and a client isn't part of the WebAuthn specification. However, this codelab's server is designed to return a JSON that's as similar as possible to the PublicKeyCredentialCreationOptions dictionary that's passed to the WebAuthn navigator.credentials.create() API.

The following table isn't exhaustive, but it contains the important parameters in the PublicKeyCredentialCreationOptions dictionary:

Parameters

Descriptions

challenge

A server-generated challenge in an ArrayBuffer object for this registration. This is required but unused during registration unless doing attestation—an advanced topic that isn't covered in this codelab.

user.id

A user's unique ID. This value must be an ArrayBuffer object that doesn't include personal identity information, such as e-mail addresses or usernames. A random, 16-byte value generated per account works well.

user.name

This field should hold a unique identifier for the account that's recognizable by the user, such as their email address or username. It's displayed in the account selector. (If you use a username, use the same value as in password authentication.)

user.displayName

This field is an optional, user-friendly name for the account. It doesn't need to be unique and could be the user's chosen name. If your website doesn't have a suitable value to include here, pass an empty string. This might be displayed on the account selector depending on the browser.

rp.id

A relying party (RP) ID is a domain. A website can specify either its domain or a registrable suffix. For example, if an RP's origin is https://login.example.com:1337, the RP ID can be either login.example.com or example.com. If the RP ID is specified as example.com, the user can authenticate on login.example.com or on any other subdomains of example.com.

pubKeyCredParams

This field specifies the RP's supported public-key algorithms. We recommend setting it to [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]. This specifies support for ECDSA with P-256 and RSA PKCS#1, and supporting these gives complete coverage.

excludeCredentials

Provides a list of already registered credential IDs to prevent registration of the same device twice. If provided, the transports member should contain the result of calling the getTransports() function during the registration of each credential.

authenticatorSelection.authenticatorAttachment

Set to a "platform" value. This indicates that you want an authenticator that's embedded in the platform device so the user won't be prompted to insert something like a USB security key.

authenticatorSelection.requireResidentKey

Set to a Boolean true value. A discoverable credential (resident key) can be used without the server having to provide the ID of the credential and so is compatible with autofill.

authenticatorSelection.userVerification

Set to a "preferred" value or omit it because it's the default value. This indicates whether a user verification that uses the device's screen lock is "required", "preferred", or "discouraged". Setting to a "preferred" value requests user verification when the device is capable.

Create a credential

  1. In the registerCredential() function's body after the relevant comment, convert some parameters encoded with Base64URL back to binary, specifically the user.id and challenge strings, and instances of the id string included in the excludeCredentials array:

public/client.js

// TODO: Add an ability to create a passkey: Create a credential.
// Base64URL decode some values.
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);

if (options.excludeCredentials) {
  for (let cred of options.excludeCredentials) {
    cred.id = base64url.decode(cred.id);
  }
}
  1. On the next line, set authenticatorSelection.authenticatorAttachment to "platform" and authenticatorSelection.requireResidentKey to true. This allows only the use of a platform authenticator (the device itself) with a discoverable credential capability.

public/client.js

// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
  authenticatorAttachment: 'platform',
  requireResidentKey: true
}
  1. On the next line, call the navigator.credentials.create() method to create a credential.

public/client.js

// Invoke the WebAuthn create() method.
const cred = await navigator.credentials.create({
  publicKey: options,
});

With this call, the browser tries to verify the user's identity with the device's screen lock.

Register the credential to the server endpoint

After the user verifies their identity, a passkey is created and stored. The website receives a credential object that contains a public key that you can send to the server to register the passkey.

The following code snippet contains an example credential object:

{
  "id": *****,
  "rawId": *****,
  "type": "public-key",
  "response": {
    "clientDataJSON": *****,
    "attestationObject": *****,
    "transports": ["internal", "hybrid"]
  },
  "authenticatorAttachment": "platform"
}

The following table isn't exhaustive, but it contains the important parameters in the PublicKeyCredential object:

Parameters

Descriptions

id

A Base64URL encoded ID of the created passkey. This ID helps the browser determine whether a matching passkey is in the device upon authentication. This value must be stored in the database on the backend.

rawId

An ArrayBuffer object version of credential ID.

response.clientDataJSON

An ArrayBuffer object encoded client data.

response.attestationObject

An ArrayBuffer encoded attestation object. It contains important information, such as an RP ID, flags, and a public key.

response.transports

A list of transports the device supports: "internal" means that the device supports a passkey. "hybrid" means that it also supports authentication on another device.

authenticatorAttachment

Returns "platform" when this credential is created on a passkey-capable device.

To send the credential object to the server, follow these steps:

  1. Encode the binary parameters of the credential as Base64URL so that it can be delivered to the server as a string:

public/client.js

// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;

// The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
if (cred.authenticatorAttachment) {
  credential.authenticatorAttachment = cred.authenticatorAttachment;
}

// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const attestationObject = base64url.encode(cred.response.attestationObject);

// Obtain transports.
const transports = cred.response.getTransports ? cred.response.getTransports() : [];

credential.response = {
  clientDataJSON,
  attestationObject,
  transports
};
  1. On the next line, send the object to the server:

public/client.js

return await _fetch('/auth/registerResponse', credential);

When you run the program, the server returns HTTP code 200, which indicates that the credential is registered.

Now you have the complete registerCredential() function!

Review the solution code for this section

public/client.js

// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {

  // TODO: Add an ability to create a passkey: Obtain the challenge and other options from server endpoint.
  const options = await _fetch('/auth/registerRequest');
  
  // TODO: Add an ability to create a passkey: Create a credential.
  // Base64URL decode some values.

  options.user.id = base64url.decode(options.user.id);
  options.challenge = base64url.decode(options.challenge);

  if (options.excludeCredentials) {
    for (let cred of options.excludeCredentials) {
      cred.id = base64url.decode(cred.id);
    }
  }

  // Use platform authenticator and discoverable credential.
  options.authenticatorSelection = {
    authenticatorAttachment: 'platform',
    requireResidentKey: true
  }

  // Invoke the WebAuthn create() method.
  const cred = await navigator.credentials.create({
    publicKey: options,
  });

  // TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
  if (cred.authenticatorAttachment) {
    credential.authenticatorAttachment = cred.authenticatorAttachment;
  }

  // Base64URL encode some values.
  const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
  const attestationObject =  
  base64url.encode(cred.response.attestationObject);

  // Obtain transports.
  const transports = cred.response.getTransports ? 
  cred.response.getTransports() : [];

  credential.response = {
    clientDataJSON,
    attestationObject,
    transports
  };

  return await _fetch('/auth/registerResponse', credential);
};

4. Build a UI to register and manage passkey credentials

Now that the registerCredential() function is available, you need a button to invoke it. Also, you need to display a list of registered passkeys.

Registered passkeys listed on the /home page

Add placeholder HTML

  1. In Glitch, navigate to the views/home.html file.
  2. After the relevant comment, add a UI placeholder that displays a button to register a passkey and a list of passkeys:

views/home.html

​​<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3 class="mdc-typography mdc-typography--headline6"> Your registered 
  passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-button>

The div#list element is the placeholder for the list.

Check for passkey support

To only show the option to create a passkey to users with devices that support passkeys, you first need to check whether WebAuthn is available. If so, you then need to remove the hidden class to show the Create a passkey button.

To check whether an environment supports passkeys, follow these steps:

  1. At the end of the views/home.html file after the relevant comment, write a conditional that executes if window.PublicKeyCredential, PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable, and PublicKeyCredential.isConditionalMediationAvailable are true.

views/home.html

// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');
// Feature detections
if (window.PublicKeyCredential &&
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  1. In the body of the conditional, check whether the device can create a passkey and then check whether the passkey can be suggested in a form autofill.

views/home.html

try {
  const results = await Promise.all([

    // Is platform authenticator available in this browser?
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

    // Is conditional UI available in this browser?
    PublicKeyCredential.isConditionalMediationAvailable()
  ]);
  1. If all conditions are met, show the button to create a passkey. Otherwise, show a warning message.

views/home.html

    if (results.every(r => r === true)) {

      // If conditional UI is available, reveal the Create a passkey button.
      createPasskey.classList.remove('hidden');
    } else {

      // If conditional UI isn't available, show a message.
      $('#message').innerText = 'This device does not support passkeys.';
    }
  } catch (e) {
    console.error(e);
  }
} else {

  // If WebAuthn isn't available, show a message.
  $('#message').innerText = 'This device does not support passkeys.';
}

Render registered passkeys in a list

  1. Define a renderCredentials() function that fetches registered passkeys from the server and renders them in a list. Luckily, you already have the /auth/getKeys server endpoint to fetch registered passkeys for the signed-in user.

views/home.html

// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
  const res = await _fetch('/auth/getKeys');
  const list = $('#list');
  const creds = html`${res.length > 0 ? html`
    <mwc-list>
      ${res.map(cred => html`
        <mwc-list-item>
          <div class="list-item">
            <div class="entity-name">
              <span>${cred.name || 'Unnamed' }</span>
          </div>
          <div class="buttons">
            <mwc-icon-button data-cred-id="${cred.id}"  
            data-name="${cred.name || 'Unnamed' }" @click="${rename}"  
            icon="edit"></mwc-icon-button>
            <mwc-icon-button data-cred-id="${cred.id}" @click="${remove}" 
            icon="delete"></mwc-icon-button>
          </div>
         </div>
      </mwc-list-item>`)}
  </mwc-list>` : html`
  <mwc-list>
    <mwc-list-item>No credentials found.</mwc-list-item>
  </mwc-list>`}`;
  render(creds, list);
};
  1. On the next line, invoke the renderCredentials() function to display registered passkeys as soon as the user lands on the /home page as an initialization.

views/home.html

renderCredentials();

Create and register a passkey

To create and register a passkey, you need to call the registerCredential() function that you implemented earlier.

To trigger the registerCredential() function when you click the Create a passkey button, follow these steps:

  1. In the file after the placeholder HTML, find the following import statement:

views/home.html

import { 
  $, 
  _fetch, 
  loading, 
  updateCredential, 
  unregisterCredential, 
} from '/client.js';
  1. At the end of the import statement's body, add the registerCredential() function.

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
  $,
  _fetch,
  loading,
  updateCredential,
  unregisterCredential,
  registerCredential
} from '/client.js';
  1. At the end of the file after the relevant comment, define a register() function that invokes the registerCredential() function and a loading UI, and calls the renderCredentials() after a registration. This clarifies that the browser creates a passkey and shows an error message when something goes wrong.

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
  try {

    // Start the loading UI.
    loading.start();

    // Start creating a passkey.
    await registerCredential();

    // Stop the loading UI.
    loading.stop();

    // Render the updated passkey list.
    renderCredentials();
  1. In the body of the register() function, catch exceptions. The navigator.credentials.create() method throws an InvalidStateError error when a passkey already exists on the device. This is examined with the excludeCredentials array. You show a relevant message to the user in this case. It also throws a NotAllowedError error when the user cancels the authentication dialog. You silently ignore it in this case.

views/home.html

  } catch (e) {

    // Stop the loading UI.
    loading.stop();

    // An InvalidStateError indicates that a passkey already exists on the device.
    if (e.name === 'InvalidStateError') {
      alert('A passkey already exists for this device.');

    // A NotAllowedError indicates that the user canceled the operation.
    } else if (e.name === 'NotAllowedError') {
      Return;

    // Show other errors in an alert.
    } else {
      alert(e.message);
      console.error(e);
    }
  }
};
  1. On the line after the register() function, attach the register() function to a click event for the Create a passkey button.

views/home.html

createPasskey.addEventListener('click', register);

Review the solution code for this section

views/home.html

​​<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3 class="mdc-typography mdc-typography--headline6"> Your registered  
  passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-button>

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
import { 
  $, 
  _fetch, 
  loading, 
  updateCredential, 
  unregisterCredential, 
  registerCredential 
} from '/client.js';

views/home.html

// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');

// Feature detections
if (window.PublicKeyCredential &&
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  try {
    const results = await Promise.all([

      // Is platform authenticator available in this browser?
      PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

      // Is conditional UI available in this browser?
      PublicKeyCredential.isConditionalMediationAvailable()
    ]);
    if (results.every(r => r === true)) {

      // If conditional UI is available, reveal the Create a passkey button.
      createPasskey.classList.remove('hidden');
    } else {

      // If conditional UI isn't available, show a message.
      $('#message').innerText = 'This device does not support passkeys.';
    }
  } catch (e) {
    console.error(e);
  }
} else {

  // If WebAuthn isn't available, show a message.
  $('#message').innerText = 'This device does not support passkeys.';
}

// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
  const res = await _fetch('/auth/getKeys');
  const list = $('#list');
  const creds = html`${res.length > 0 ? html`
  <mwc-list>
    ${res.map(cred => html`
      <mwc-list-item>
        <div class="list-item">
          <div class="entity-name">
            <span>${cred.name || 'Unnamed' }</span>
          </div>
          <div class="buttons">
            <mwc-icon-button data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed' }" @click="${rename}" icon="edit"></mwc-icon-button>
            <mwc-icon-button data-cred-id="${cred.id}" @click="${remove}" icon="delete"></mwc-icon-button>
          </div>
        </div>
      </mwc-list-item>`)}
  </mwc-list>` : html`
  <mwc-list>
    <mwc-list-item>No credentials found.</mwc-list-item>
  </mwc-list>`}`;
  render(creds, list);
};

renderCredentials();

// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
  try {

    // Start the loading UI.
    loading.start();

    // Start creating a passkey.
    await registerCredential();

    // Stop the loading UI.
    loading.stop();

    // Render the updated passkey list.
    renderCredentials();
  } catch (e) {

    // Stop the loading UI.
    loading.stop();

    // An InvalidStateError indicates that a passkey already exists on the device.
    if (e.name === 'InvalidStateError') {
      alert('A passkey already exists for this device.');

    // A NotAllowedError indicates that the user canceled the operation.
    } else if (e.name === 'NotAllowedError') {
      Return;

    // Show other errors in an alert.
    } else {
      alert(e.message);
      console.error(e);
    }
  }
};

createPasskey.addEventListener('click', register);

Try it

If you followed all the steps so far, you implemented the ability to create, register, and display passkeys on the website!

To try it, follow these steps:

  1. In the preview tab, sign in with a random username and password.
  2. Click Create a passkey.
  3. Verify your identity with the device's screen lock.
  4. Confirm that a passkey is registered and displayed under the Your registered passkeys section of the web page.

Registered passkeys listed on the /home page.

Rename and remove registered passkeys

You should be able to rename or delete the registered passkeys on the list. You can check how it works in the code as they come with the codelab.

In Chrome, you can remove registered passkeys from chrome://settings/passkeys on desktop or from the password manager in settings on Android.

For information about how to rename and remove registered passkeys on other platforms, see the respective support pages for those platforms.

5. Add the ability to authenticate with a passkey

Users can now create and register a passkey, and are ready to use it as a way to authenticate to your website safely. Now you need to add a passkey authentication capability to your website.

Create the authenticate() function

  • In the public/client.js file after the relevant comment, create a function called authenticate() that locally verifies the user and then against the server:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {

  // TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.

  // TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.

  // TODO: Add an ability to authenticate with a passkey: Verify the credential.

};

Obtain the challenge and other options from server endpoint

Before you ask the user to authenticate, you need to request parameters to pass in WebAuthn from the server, including a challenge.

  • In the body of the authenticate() function after the relevant comment, call the _fetch() function to send a POST request to the server:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.
const options = await _fetch('/auth/signinRequest');

This codelab's server is designed to return JSON that's as similar as possible to the PublicKeyCredentialRequestOptions dictionary that's passed to the WebAuthn navigator.credentials.get() API. The following code snippet includes example options that you should receive:

{
  "challenge": *****,
  "rpId": "passkeys-codelab.glitch.me",
  "allowCredentials": []
}

The following table isn't exhaustive, but it contains the important parameters in the PublicKeyCredentialRequestOptions dictionary:

Parameters

Descriptions

challenge

A server-generated challenge in an ArrayBuffer object. This is required to prevent replay attacks. Never accept the same challenge in a response twice. Consider it a CSRF token.

rpId

An RP ID is a domain. A website can specify either its domain or a registrable suffix. This value must match the rp.id parameter used when the passkey was created.

allowCredentials

This property is used to find authenticators eligible for this authentication. Pass an empty array or leave it unspecified to let the browser show an account selector.

userVerification

Set to a "preferred" value or omit it because it's the default value. This indicates whether a user verification using the device's screen lock is "required", "preferred", or "discouraged". Setting to a "preferred" value requests user verification when the device is capable.

Locally verify the user and get a credential

  1. In the authenticate() function's body after the relevant comment, convert the challenge parameter back to binary:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.
// Base64URL decode the challenge.
options.challenge = base64url.decode(options.challenge);
  1. Pass an empty array to the allowCredentials parameter to open an account selector when a user authenticates:

public/client.js

// An empty allowCredentials array invokes an account selector by discoverable credentials.
options.allowCredentials = [];

The account selector uses the user's information stored with the passkey.

  1. Call the navigator.credentials.get() method along with a mediation: 'conditional' option:

public/client.js

// Invoke the WebAuthn get() method.
const cred = await navigator.credentials.get({
  publicKey: options,

  // Request a conditional UI.
  mediation: 'conditional'
});

This option instructs the browser to suggest passkeys conditionally as part of form autofill.

Verify the credential

After the user verifies their identity locally, you should receive a credential object that contains a signature that you can verify on the server.

The following code snippet includes an example PublicKeyCredential object:

{
  "id": *****,
  "rawId": *****,
  "type": "public-key",
  "response": {
    "clientDataJSON": *****,
    "authenticatorData": *****,
    "signature": *****,
    "userHandle": *****
  },
  authenticatorAttachment: "platform"
}

The following table isn't exhaustive, but it contains the important parameters in the PublicKeyCredential object:

Parameters

Descriptions

id

The Base64URL encoded ID of the authenticated passkey credential.

rawId

An ArrayBuffer object version of credential ID.

response.clientDataJSON

An ArrayBuffer object of client data. This field contains information, such as the challenge and the origin that the RP server needs to verify.

response.authenticatorData

An ArrayBuffer object of authenticator data. This field contains information like RP ID.

response.signature

An ArrayBuffer object of the signature. This value is the core of the credential and must be verified on the server.

response.userHandle

An ArrayBuffer object that contains the user ID set at creation time. This value can be used instead of the credential ID if the server needs to pick the ID values that it uses, or if the backend wishes to avoid the creation of an index on credential IDs.

authenticatorAttachment

Returns a "platform" string when this credential comes from the local device. Otherwise returns a "cross-platform" string, notably when the user uses a phone to sign in. If the user needs to use a phone to sign in, prompt them to create a passkey on the local device.

To send the credential object to the server, follow these steps:

  1. In the authenticate() function's body after the relevant comment, encode the binary parameters of the credential so that it can be delivered to the server as a string:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Verify the credential.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;

// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const authenticatorData = base64url.encode(cred.response.authenticatorData);
const signature = base64url.encode(cred.response.signature);
const userHandle = base64url.encode(cred.response.userHandle);

credential.response = {
  clientDataJSON,
  authenticatorData,
  signature,
  userHandle,
};
  1. Send the object to the server:

public/client.js

return await _fetch(`/auth/signinResponse`, credential);

When you run the program, the server returns HTTP code 200, which indicates that the credential is verified.

You now have the full authentication() function!

Review the solution code for this section

public/client.js

// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {

  // TODO: Add an ability to authenticate with a passkey: Obtain the 
  challenge and other options from the server endpoint.
  const options = await _fetch('/auth/signinRequest');

  // TODO: Add an ability to authenticate with a passkey: Locally verify 
  the user and get a credential.
  // Base64URL decode the challenge.
  options.challenge = base64url.decode(options.challenge);

  // The empty allowCredentials array invokes an account selector 
  by discoverable credentials.
  options.allowCredentials = [];

  // Invoke the WebAuthn get() function.
  const cred = await navigator.credentials.get({
    publicKey: options,

    // Request a conditional UI.
    mediation: 'conditional'
  });

  // TODO: Add an ability to authenticate with a passkey: Verify the credential.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // Base64URL encode some values.
  const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
  const authenticatorData = 
  base64url.encode(cred.response.authenticatorData);
  const signature = base64url.encode(cred.response.signature);
  const userHandle = base64url.encode(cred.response.userHandle);

  credential.response = {
    clientDataJSON,
    authenticatorData,
    signature,
    userHandle,
  };

  return await _fetch(`/auth/signinResponse`, credential);
};

6. Add passkeys to the browser autofill

When the user returns, you want the user to sign in as easily and securely as possible. If you add a Sign in with a passkey button to the login page, the user can press the button, select a passkey in the browser's account selector, and use screen lock to verify identity.

However, the transition from a password to a passkey doesn't happen to all users at once. This means that you can't get rid of passwords until all users transition to passkeys, so you need to leave the password-based sign-in form until then. Although, if you leave a password form and a passkey button, users will have to make a needless choice between which one to use to sign in. Ideally, you want a straightforward sign-in process.

This is where a conditional UI comes in. A conditional UI is a WebAuthn feature where you can make a form input field to suggest a passkey as part of autofill items in addition to passwords. If a user taps on a passkey in the autofill suggestions, the user is asked to use the device's screen lock to locally verify their identity. This is a seamless user experience because the user action is almost identical to that of a password based sign-in.

A passkey suggested as part of form autofill.

Enable a conditional UI

To enable a conditional UI, all you need to do is add a webauthn token in the autocomplete attribute of an input field. With the token set, you can call the navigator.credentials.get() method with the mediation: 'conditional' string to conditionally trigger the screen lock UI.

  • To enable a conditional UI, replace the existing username input fields with the following HTML after the relevant comment in the view/index.html file:

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
  type="text"
  id="username"
  class="mdc-text-field__input"
  aria-labelledby="username-label"
  name="username"
  autocomplete="username webauthn"
  autofocus />

Detect features, invoke WebAuthn, and enable a conditional UI

  1. In the view/index.html file after the relevant comment, replace the existing import statement with the following code:

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
  $,
  _fetch,
  loading,
  authenticate 
} from "/client.js";

This code imports the authenticate() function that you implemented earlier.

  1. Confirm that the window.PulicKeyCredential object is available and that the PublicKeyCredential.isConditionalMediationAvailable() method returns a true value, and then call the authenticate() function:

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
if (
  window.PublicKeyCredential &&
  PublicKeyCredential.isConditionalMediationAvailable
) {
  try {

    // Is conditional UI available in this browser?
    const cma =
      await PublicKeyCredential.isConditionalMediationAvailable();
    if (cma) {

      // If conditional UI is available, invoke the authenticate() function.
      const user = await authenticate();
      if (user) {

        // Proceed only when authentication succeeds.
        $("#username").value = user.username;
        loading.start();
        location.href = "/home";
      } else {
        throw new Error("User not found.");
      }
    }
  } catch (e) {
    loading.stop();

    // A NotAllowedError indicates that the user canceled the operation.
    if (e.name !== "NotAllowedError") {
      console.error(e);
      alert(e.message);
    }
  }
}

Review the solution code for this section

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
  type="text"
  id="username"
  class="mdc-text-field__input"
  aria-labelledby="username-label"
  name="username"
  autocomplete="username webauthn"
  autofocus 
/>

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import { 
  $, 
  _fetch, 
  loading, 
  authenticate 
} from '/client.js';

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.        
// Is WebAuthn avaiable in this browser?
if (window.PublicKeyCredential &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  try {

    // Is a conditional UI available in this browser?
    const cma= await PublicKeyCredential.isConditionalMediationAvailable();
    if (cma) {

      // If a conditional UI is available, invoke the authenticate() function.
      const user = await authenticate();
      if (user) {

        // Proceed only when authentication succeeds.
        $('#username').value = user.username;
        loading.start();
        location.href = '/home';
      } else {
        throw new Error('User not found.');
      }
    }
  } catch (e) {
    loading.stop();

    // A NotAllowedError indicates that the user canceled the operation.
    if (e.name !== 'NotAllowedError') {
      console.error(e);
      alert(e.message);
    }
  }
}

Try it

You implemented the creation, registration, display, and authentication of passkeys on your website.

To try it, follow these steps:

  1. Navigate to the preview tab.
  2. If necessary, sign out.
  3. Click the username text box. A dialog appears.
  4. Select the account with which you want to sign in.
  5. Verify your identity with the device's screen lock. You're redirected to the /home page and signed in.

A dialog that prompts you to verify your identity with your saved password or passkey.

7. Congratulations!

You finished this codelab! If you have any questions, ask them on the FIDO-DEV mailing list or on StackOverflow with a passkey tag.

Learn more