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 Remixto 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 passkeybutton.

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 passkeybutton, 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 passkeybutton.

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 passkeyssection 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 passkeybutton 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

Design a Mobile Site
View Site in Mobile | Classic
Share by: