Build your first WebAuthn app

1. Before you begin

The Web Authentication API, also known as WebAuthn, lets you create and use origin-scoped, public-key credentials to authenticate users.

The API supports the use of BLE, NFC, and USB-roaming U2F or FIDO2 authenticators—also known as security keys—as well as a platform authenticator, which lets users authenticate with their fingerprints or screen locks.

In this codelab, you build a website with a simple reauthentication functionality that uses a fingerprint sensor. Reauthentication protects account data because it requires users who already signed in to a website to authenticate again when they try to enter important sections of the website or revisit the website after a certain amount of time.

Prerequisites

  • Basic understanding of how WebAuthn works
  • Basic programming skills with JavaScript

What you'll do

  • Build a website with a simple reauthentication functionality that uses a fingerprint sensor

What you'll need

  • One of the following devices:
    • An Android device, preferably with a biometric sensor
    • An iPhone or iPad with Touch ID or Face ID on iOS 14 or higher
    • A MacBook Pro or Air with Touch ID on macOS Big Sur or higher
    • Windows 10 19H1 or higher with Windows Hello set up
  • One of the following browsers:
    • Google Chrome 67 or higher
    • Microsoft Edge 85 or higher
    • Safari 14 or higher

2. Get set up

In this codelab, you use a service called glitch . This is where you can edit client and server-side code with JavaScript, and deploy them instantly.

Navigate to https://glitch.com/edit/#!/webauthn-codelab-start .

See how it works

Follow these steps to see the initial state of the website:

  1. Click62bb7a6aac381af8.png Show>3343769d04c09851.png In a New Windowto see the live website .
  2. Enter a username of your choice and click Next.
  3. Enter a password and click Sign-in.

The password is ignored, but you're still authenticated. You land at the home page.

  1. Click Try reauth, and repeat the second, third, and fourth steps.
  2. Click Sign out.

Notice that you must enter the password every time that you try to sign in. This emulates a user who needs to reauthenticate before they can access an important section of a website.

Remix the code

  1. Navigate to WebAuthn / FIDO2 API Codelab .
  2. Click the name of your project > Remix Project306122647ce93305.pngto fork the project and continue with your own version at a new URL.

8d42bd24f0fd185c.png

3. Register a credential with a fingerprint

You need to register a credential generated by a UVPA, an authenticator that is built into the device and verifies the user's identity. This is typically seen as a fingerprint sensor depending on the user's device.

You add this feature to the /home page:

260aab9f1a2587a7.png

Create registerCredential() function

Create a registerCredential() function, which registers a new credential.

public/client.js

  export 
  
 const 
  
 registerCredential 
  
 = 
  
 async 
  
 () 
  
 = 
>  
 { 
 }; 
 

Obtain the challenge and other options from server endpoint

Before you ask the user to register a new credential, request that the server return parameters to pass in WebAuthn, including a challenge. Luckily, you already have a server endpoint that responds with such parameters.

Add the following code to registerCredential() .

public/client.js

  const 
  
 opts 
  
 = 
  
 { 
  
 attestation 
 : 
  
 'none' 
 , 
  
 authenticatorSelection 
 : 
  
 { 
  
 authenticatorAttachment 
 : 
  
 'platform' 
 , 
  
 userVerification 
 : 
  
 'required' 
 , 
  
 requireResidentKey 
 : 
  
 false 
  
 } 
 }; 
 const 
  
 options 
  
 = 
  
 await 
  
 _fetch 
 ( 
 '/auth/registerRequest' 
 , 
  
 opts 
 ); 
 

The protocol between a server and a client is not a part of the WebAuthn specification. However, this codelab is designed to align with the WebAuthn specification and the JSON object that you pass to the server is very similar to PublicKeyCredentialCreationOptions so that it's intuitive for you. The following table contains the important parameters that you can pass to the server and explains what they do:

Parameters

Descriptions

attestation

Preference for attestation conveyance— none , indirect , or direct . Choose none unless you need one.

excludeCredentials

Array of PublicKeyCredentialDescriptor so that the authenticator can avoid creating duplicate ones.

authenticatorSelection

authenticatorAttachment

Filter available authenticators. If you want an authenticator attached to the device, use " platform ". For roaming authenticators, use " cross-platform ".

userVerification

Determine whether authenticator local user verification is " required ", " preferred ", or " discouraged ". If you want fingerprint or screen-lock authentication, use " required ".

requireResidentKey

Use true if the created credential should be available for future account picker UX.

To learn more about these options, see 5.4. Options for Credential Creation (dictionary PublicKeyCredentialCreationOptions ) .

The following are example options that you receive from the server.

  { 
  
 "rp" 
 : 
  
 { 
  
 "name" 
 : 
  
 "WebAuthn Codelab" 
 , 
  
 "id" 
 : 
  
 "webauthn-codelab.glitch.me" 
  
 }, 
  
 "user" 
 : 
  
 { 
  
 "displayName" 
 : 
  
 "User Name" 
 , 
  
 "id" 
 : 
  
 "..." 
 , 
  
 "name" 
 : 
  
 "test" 
  
 }, 
  
 "challenge" 
 : 
  
 "..." 
 , 
  
 "pubKeyCredParams" 
 : 
  
 [ 
  
 { 
  
 "type" 
 : 
  
 "public-key" 
 , 
  
 "alg" 
 : 
  
 - 
 7 
  
 }, 
  
 { 
  
 "type" 
 : 
  
 "public-key" 
 , 
  
 "alg" 
 : 
  
 - 
 257 
  
 } 
  
 ], 
  
 "timeout" 
 : 
  
 1800000 
 , 
  
 "attestation" 
 : 
  
 "none" 
 , 
  
 "excludeCredentials" 
 : 
  
 [ 
  
 { 
  
 "id" 
 : 
  
 "..." 
 , 
  
 "type" 
 : 
  
 "public-key" 
 , 
  
 "transports" 
 : 
  
 [ 
  
 "internal" 
  
 ] 
  
 } 
  
 ], 
  
 "authenticatorSelection" 
 : 
  
 { 
  
 "authenticatorAttachment" 
 : 
  
 "platform" 
 , 
  
 "userVerification" 
 : 
  
 "required" 
  
 } 
 } 
 

Create a credential

  1. Because these options are delivered encoded to go through HTTP protocol, convert some parameters back to binary, specifically, user.id , challenge and instances of id included in the excludeCredentials array:

public/client.js

 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. Call the navigator.credentials.create() method to create a new credential.

With this call, the browser interacts with the authenticator and tries to verify the user's identity with the UVPA.

public/client.js

  const 
  
 cred 
  
 = 
  
 await 
  
 navigator 
 . 
 credentials 
 . 
 create 
 ({ 
  
 publicKey 
 : 
  
 options 
 , 
 }); 
 

Once the user verifies their identity, you should receive a credential object that you can send to the server and register the authenticator.

Register the credential to the server endpoint

Here's an example credential object that you should have received.

  { 
  
 "id" 
 : 
  
 "..." 
 , 
  
 "rawId" 
 : 
  
 "..." 
 , 
  
 "type" 
 : 
  
 "public-key" 
 , 
  
 "response" 
 : 
  
 { 
  
 "clientDataJSON" 
 : 
  
 "..." 
 , 
  
 "attestationObject" 
 : 
  
 "..." 
  
 } 
 } 
 
  1. Like when you received an option object for registering a credential, encode the binary parameters of the credential so that it can be delivered to the server as a string:

public/client.js

  const 
  
 credential 
  
 = 
  
 {}; 
 credential 
 . 
 id 
  
 = 
  
 cred 
 . 
 id 
 ; 
 credential 
 . 
 rawId 
  
 = 
  
 base64url 
 . 
 encode 
 ( 
 cred 
 . 
 rawId 
 ); 
 credential 
 . 
 type 
  
 = 
  
 cred 
 . 
 type 
 ; 
 if 
  
 ( 
 cred 
 . 
 response 
 ) 
  
 { 
  
 const 
  
 clientDataJSON 
  
 = 
  
 base64url 
 . 
 encode 
 ( 
 cred 
 . 
 response 
 . 
 clientDataJSON 
 ); 
  
 const 
  
 attestationObject 
  
 = 
  
 base64url 
 . 
 encode 
 ( 
 cred 
 . 
 response 
 . 
 attestationObject 
 ); 
  
 credential 
 . 
 response 
  
 = 
  
 { 
  
 clientDataJSON 
 , 
  
 attestationObject 
 , 
  
 }; 
 } 
 
  1. Store the credential ID locally so that you can use it for authentication when the user comes back:

public/client.js

  localStorage 
 . 
 set 
 Item 
 ( 
 `credId` 
 , 
  
 credential 
 . 
 id 
 ); 
 
  1. Send the object to the server and, if it returns HTTP code 200 , consider the new credential as successfully registered.

public/client.js

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

You now have the complete registerCredential() function!

Final code for this section

public/client.js

  ... 
 export 
  
 const 
  
 registerCredential 
  
 = 
  
 async 
  
 () 
  
 = 
>  
 { 
  
 const 
  
 opts 
  
 = 
  
 { 
  
 attestation 
 : 
  
 'none' 
 , 
  
 authenticatorSelection 
 : 
  
 { 
  
 authenticatorAttachment 
 : 
  
 'platform' 
 , 
  
 userVerification 
 : 
  
 'required' 
 , 
  
 requireResidentKey 
 : 
  
 false 
  
 } 
  
 } 
 ; 
  
 const 
  
 options 
  
 = 
  
 await 
  
 _fetch 
 ( 
 '/auth/registerRequest' 
 , 
  
 opts 
 ); 
  
 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 
 ); 
  
 } 
  
 } 
  
  
 const 
  
 cred 
  
 = 
  
 await 
  
 navigator 
 . 
 credentials 
 . 
 create 
 ( 
 { 
  
 publicKey 
 : 
  
 options 
  
 } 
 ); 
  
 const 
  
 credential 
  
 = 
  
 {} 
 ; 
  
 credential 
 . 
 id 
  
 = 
  
 cred 
 . 
 id 
 ; 
  
 credential 
 . 
 rawId 
  
 = 
  
 base64url 
 . 
 encode 
 ( 
 cred 
 . 
 rawId 
 ); 
  
 credential 
 . 
 type 
  
 = 
  
 cred 
 . 
 type 
 ; 
  
 if 
  
 ( 
 cred 
 . 
 response 
 ) 
  
 { 
  
 const 
  
 clientDataJSON 
  
 = 
  
 base64url 
 . 
 encode 
 ( 
 cred 
 . 
 response 
 . 
 clientDataJSON 
 ); 
  
 const 
  
 attestationObject 
  
 = 
  
 base64url 
 . 
 encode 
 ( 
 cred 
 . 
 response 
 . 
 attestationObject 
 ); 
  
 credential 
 . 
 response 
  
 = 
  
 { 
  
 clientDataJSON 
 , 
  
 attestationObject 
  
 } 
 ; 
  
 } 
  
 localStorage 
 . 
 set 
 Item 
 ( 
 `credId` 
 , 
  
 credential 
 . 
 id 
 ); 
  
  
 return 
  
 await 
  
 _fetch 
 ( 
 '/auth/registerResponse' 
  
 , 
  
 credential 
 ); 
 } 
 ; 
 ... 
 

4. Build the UI to register, get, and remove credentials

It's nice to have a list of registered credentials and buttons to remove them.

9b5b5ae4a7b316bd.png

Build UI placeholder

Add UI to list credentials and a button to register a new credential. Depending on whether the feature is available or not, you remove the hidden class from either the warning message or the button to register a new credential. ul#list is the placeholder for adding a list of registered credentials.

views/home.html

 < p 
  
 id 
 = 
 "uvpa_unavailable" 
  
 class 
 = 
 "hidden" 
>  
 This 
  
 device 
  
 does 
  
 not 
  
 support 
  
 User 
  
 Verifying 
  
 Platform 
  
 Authenticator 
 . 
  
 You 
  
 can 
 ' 
 t 
  
 register 
  
 a 
  
 credential 
 . 
< / 
 p 
>
< h3 
  
 class 
 = 
 "mdc-typography mdc-typography--headline6" 
>  
 Your 
  
 registered 
  
 credentials: 
< / 
 h3 
>
< section 
>  
< div 
  
 id 
 = 
 "list" 
>< / 
 div 
>
< / 
 section 
>
< mwc 
 - 
 button 
  
 id 
 = 
 "register" 
  
 class 
 = 
 "hidden" 
  
 icon 
 = 
 "fingerprint" 
  
 raised>Add 
  
 a 
  
 credential 
< / 
 mwc 
 - 
 button 
> 

Feature detection and UVPA availability

Follow these steps to check the UVPA availability:

  1. Examine window.PublicKeyCredential to check if WebAuthn is available.
  2. Call PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() to check if a UVPA is available . If they're available, you show the button to register a new credential. If either of them are not available, you show the warning message.

views/home.html

  const 
  
 register 
  
 = 
  
 document 
 . 
 querySelector 
 ( 
 '#register' 
 ); 
 if 
  
 ( 
 window 
 . 
 PublicKeyCredential 
 ) 
  
 { 
  
 PublicKeyCredential 
 . 
 isUserVerifyingPlatformAuthenticatorAvailable 
 () 
  
 . 
 then 
 ( 
 uvpaa 
  
 = 
>  
 { 
  
 if 
  
 ( 
 uvpaa 
 ) 
  
 { 
  
 register 
 . 
 classList 
 . 
 remove 
 ( 
 'hidden' 
 ); 
  
 } 
  
 else 
  
 { 
  
 document 
  
 . 
 querySelector 
 ( 
 '#uvpa_unavailable' 
 ) 
  
 . 
 classList 
 . 
 remove 
 ( 
 'hidden' 
 ); 
  
 } 
  
 }); 
  
 } 
  
 else 
  
 { 
  
 document 
  
 . 
 querySelector 
 ( 
 '#uvpa_unavailable' 
 ) 
  
 . 
 classList 
 . 
 remove 
 ( 
 'hidden' 
 ); 
 } 
 

Get and display a list of credentials

  1. Create a getCredentials() function so that you can get registered credentials and display them in a list. Luckily, you already have a handy endpoint on the server /auth/getKeys from which you can fetch registered credentials for the signed-in user.

The returned JSON includes credential information, such as id and publicKey . You can build HTML to show them to the user.

views/home.html

  const 
  
 getCredentials 
  
 = 
  
 async 
  
 () 
  
 = 
>  
 { 
  
 const 
  
 res 
  
 = 
  
 await 
  
 _fetch 
 ( 
 '/auth/getKeys' 
 ); 
  
 const 
  
 list 
  
 = 
  
 document 
 . 
 querySelector 
 ( 
 '#list' 
 ); 
  
 const 
  
 creds 
  
 = 
  
 html 
 ` 
 $ 
 { 
 res 
 . 
 credentials 
 . 
 length 
 > 
 0 
  
 ? 
  
 res 
 . 
 credentials 
 . 
 map 
 ( 
 cred 
  
 = 
>  
 html 
 ` 
  
< div 
  
 class 
 = 
 "mdc-card credential" 
>  
< span 
  
 class 
 = 
 "mdc-typography mdc-typography--body2" 
> $ 
 { 
 cred 
 . 
 credId 
 } 
< / 
 span 
>  
< pre 
  
 class 
 = 
 "public-key" 
> $ 
 { 
 cred 
 . 
 publicKey 
 } 
< / 
 pre 
>  
< div 
  
 class 
 = 
 "mdc-card__actions" 
>  
< mwc 
 - 
 button 
  
 id 
 = 
 "${cred.credId}" 
  
 @ 
 click 
 = 
 "${removeCredential}" 
  
 raised>Remove 
< / 
 mwc 
 - 
 button 
>  
< / 
 div 
>  
< / 
 div 
> ` 
 ) 
  
 : 
  
 html 
 ` 
  
< p>No 
  
 credentials 
  
 found 
 .</ 
 p 
>  
 ` 
 } 
 ` 
 ; 
  
 render 
 ( 
 creds 
 , 
  
 list 
 ); 
 }; 
 
  1. Invoke getCredentials() to display available credentials as soon as the user lands on the /home page.

views/home.html

 getCredentials(); 

Remove the credential

In the list of credentials, you added a button to remove each credential. You can send a request to /auth/removeKey along with the credId query parameter to remove them.

public/client.js

  export 
  
 const 
  
 unregisterCredential 
  
 = 
  
 async 
  
 ( 
 credId 
 ) 
  
 = 
>  
 { 
  
 localStorage 
 . 
 removeItem 
 ( 
 'credId' 
 ); 
  
 return 
  
 _fetch 
 ( 
 ` 
 / 
 auth 
 / 
 removeKey 
 ? 
 credId 
 =$ 
 { 
 encodeURIComponent 
 ( 
 credId 
 )} 
 ` 
 ); 
 }; 
 
  1. Append unregisterCredential to the existing import statement.

views/home.html

  import 
  
 { 
 _fetch 
 , 
 unregisterCredential 
 } 
 from 
  
 '/client.js' 
 ; 
 
  1. Add a function to call when the user clicks Remove.

views/home.html

  const 
  
 removeCredential 
  
 = 
  
 async 
  
 e 
  
 = 
>  
 { 
  
 try 
  
 { 
  
 await 
  
 unregisterCredential 
 ( 
 e 
 . 
 target 
 . 
 id 
 ); 
  
 getCredentials 
 (); 
  
 } 
  
 catch 
  
 ( 
 e 
 ) 
  
 { 
  
 alert 
 ( 
 e 
 ); 
  
 } 
 }; 
 

Register a credential

You can call registerCredential() to register a new credential when the user clicks Add a credential.

  1. Append registerCredential to the existing import statement.

views/home.html

  import 
  
 { 
 _fetch 
 , 
 registerCredential 
 , 
 unregisterCredential 
 } 
 from 
  
 '/client.js' 
 ; 
 
  1. Invoke registerCredential() with options for navigator.credentials.create() .

Don't forget to renew the credential list by calling getCredentials() after registration.

views/home.html

  register 
 . 
 addEventListener 
 (' 
 click 
 ', 
  
 e 
  
 = 
>  
 { 
  
 registerCredential 
 (). 
 then 
 ( 
 user 
  
 = 
>  
 { 
  
 getCredentials 
 (); 
  
 }). 
 catch 
 ( 
 e 
  
 = 
>  
 alert 
 ( 
 e 
 )); 
 }); 
 

Now you should be able to register a new credential and display information about it. You may try it on your live website.

Final code for this section

views/home.html

  ... 
< p 
 id 
 = 
 "uvpa_unavailable" 
 class 
 = 
 "hidden" 
> This 
 device 
 does 
 not 
 support 
 User 
 Verifying 
 Platform 
 Authenticator 
 . 
 You 
 can 
 't register a credential. 
< / 
 p 
>
      < h3 
 class 
 = 
 "mdc-typography mdc-typography--headline6" 
> Your 
 registered 
 credentials 
 : 
< / 
 h3 
>
      < section 
>
        < div 
 id 
 = 
 "list" 
>< / 
 div 
>
        < mwc 
 - 
 fab 
 id 
 = 
 "register" 
 class 
 = 
 "hidden" 
 icon 
 = 
 "add" 
>< / 
 mwc 
 - 
 fab 
>
      < / 
 section 
>
      < mwc 
 - 
 button 
 raised><a 
 href 
 = 
 "/reauth" 
> Try 
 reauth 
< / 
 a 
>< / 
 mwc 
 - 
 button 
>
      < mwc 
 - 
 button><a 
 href 
 = 
 "/auth/signout" 
> Sign 
 out 
< / 
 a 
>< / 
 mwc 
 - 
 button 
>
    < / 
 main 
>
    < script 
 type 
 = 
 "module" 
> import 
  
 { 
 _fetch 
 , 
 registerCredential 
 , 
 unregisterCredential 
 } 
 from 
  
 '/client.js' 
 ; 
 import 
  
 { 
 html 
 , 
 render 
 } 
 from 
  
 'https://unpkg.com/lit-html@1.0.0/lit-html.js?module' 
 ; 
 const 
 register 
 = 
 document 
 . 
 querySelector 
 ( 
 '#register' 
 ); 
 if 
 ( 
 window 
 . 
 PublicKeyCredential 
 ) 
 { 
 PublicKeyCredential 
 . 
 isUserVerifyingPlatformAuthenticatorAvailable 
 () 
 . 
 then 
 ( 
 uvpaa 
 = 
> { 
 if 
 ( 
 uvpaa 
 ) 
 { 
 register 
 . 
 classList 
 . 
 remove 
 ( 
 'hidden' 
 ); 
 } 
 else 
 { 
 document 
 . 
 querySelector 
 ( 
 '#uvpa_unavailable' 
 ) 
 . 
 classList 
 . 
 remove 
 ( 
 'hidden' 
 ); 
 } 
 }); 
 } 
 else 
 { 
 document 
 . 
 querySelector 
 ( 
 '#uvpa_unavailable' 
 ) 
 . 
 classList 
 . 
 remove 
 ( 
 'hidden' 
 ); 
 } 
 const 
 getCredentials 
 = 
 async 
 () 
 = 
> { 
 const 
 res 
 = 
 await 
 _fetch 
 ( 
 '/auth/getKeys' 
 ); 
 const 
 list 
 = 
 document 
 . 
 querySelector 
 ( 
 '#list' 
 ); 
 const 
 creds 
 = 
 html 
 `$ 
 { 
 res 
 . 
 credentials 
 . 
 length 
> 0 
 ? 
 res 
 . 
 credentials 
 . 
 map 
 ( 
 cred 
 = 
> html 
 ` 
< div 
 class 
 = 
 "mdc-card credential" 
>
            < span 
 class 
 = 
 "mdc-typography mdc-typography--body2" 
> $ 
 { 
 cred 
 . 
 credId 
 } 
< / 
 span 
>
            < pre 
 class 
 = 
 "public-key" 
> $ 
 { 
 cred 
 . 
 publicKey 
 } 
< / 
 pre 
>
            < div 
 class 
 = 
 "mdc-card__actions" 
>
              < mwc 
 - 
 button 
 id 
 = 
 "$ 
 {cred.credId} 
 " 
 @click 
 = 
 "$ 
 {removeCredential} 
 " 
 raised>Remove 
< / 
 mwc 
 - 
 button 
>
            < / 
 div 
>
          < / 
 div 
> ` 
 ) 
 : 
 html 
 ` 
< p>No 
 credentials 
 found 
 .</ 
 p 
> ` 
 } 
 ` 
 ; 
 render 
 ( 
 creds 
 , 
 list 
 ); 
 }; 
 getCredentials 
 (); 
 const 
 removeCredential 
 = 
 async 
 e 
 = 
> { 
 try 
 { 
 await 
 unregisterCredential 
 ( 
 e 
 . 
 target 
 . 
 id 
 ); 
 getCredentials 
 (); 
 } 
 catch 
 ( 
 e 
 ) 
 { 
 alert 
 ( 
 e 
 ); 
 } 
 }; 
 register 
 . 
 addEventListener 
 ( 
 'click' 
 , 
 e 
 = 
> { 
 registerCredential 
 ({ 
 attestation 
 : 
 'none' 
 , 
 authenticatorSelection 
 : 
 { 
 authenticatorAttachment 
 : 
 'platform' 
 , 
 userVerification 
 : 
 'required' 
 , 
 requireResidentKey 
 : 
 false 
 } 
 }) 
 . 
 then 
 ( 
 user 
 = 
> { 
 getCredentials 
 (); 
 }) 
 . 
 catch 
 ( 
 e 
 = 
> alert 
 ( 
 e 
 )); 
 }); 
< / 
 script 
> ... 
 

public/client.js

  ... 
 export 
  
 const 
  
 unregisterCredential 
  
 = 
  
 async 
  
 ( 
 credId 
 ) 
  
 = 
>  
 { 
  
 localStorage 
 . 
 removeItem 
 ( 
 'credId' 
 ); 
  
 return 
  
 _fetch 
 ( 
 ` 
 / 
 auth 
 / 
 removeKey 
 ? 
 credId 
 =$ 
 { 
 encodeURIComponent 
 ( 
 credId 
 )} 
 ` 
 ); 
 }; 
 ... 
 

5. Authenticate the user with a fingerprint

You now have a credential registered and ready to use as a way to authenticate the user. Now you add reauthentication functionality to the website. Here's the user experience:

When a user lands on the /reauth page, they see an Authenticatebutton if biometric authentication is possible. Authentication with a fingerprint (UVPA) starts when they tap Authenticate, successfully authenticate, and then land on the /home page. If biometric authentication is not available or an authentication with biometric fails, the UI falls back to use the existing password form.

b8770c4e7475b075.png

Create authenticate() function

Create a function called authenticate() , which verifies the user's identity with a fingerprint. You add JavaScript code here:

public/client.js

  export 
  
 const 
  
 authenticate 
  
 = 
  
 async 
  
 () 
  
 = 
>  
 { 
 }; 
 

Obtain the challenge and other options from server endpoint

  1. Before authentication, examine if the user has a stored credential ID and set it as a query parameter if they do.

When you provide a credential ID along with other options, the server can provide relevant allowCredentials and this makes user verification reliable.

public/client.js

  const 
  
 opts 
  
 = 
  
 {} 
 ; 
 let 
  
 url 
  
 = 
  
 '/auth/signinRequest' 
 ; 
 const 
  
 credId 
  
 = 
  
 localStorage 
 . 
 getItem 
 ( 
 `credId` 
 ); 
 if 
  
 ( 
 credId 
 ) 
  
 { 
  
 url 
  
 += 
  
 `?credId=${encodeURIComponent(credId)}` 
 ; 
 } 
 
  1. Before you ask the user to authenticate, ask the server to send back a challenge and other parameters. Call _fetch() with opts as an argument to send a POST request to the server.

public/client.js

  const 
  
 options 
  
 = 
  
 await 
  
 _fetch 
 ( 
 url 
 , 
  
 opts 
 ); 
 

Here are example options you should receive (aligns with PublicKeyCredentialRequestOptions ).

  { 
  
 "challenge" 
 : 
  
 "..." 
 , 
  
 "timeout" 
 : 
  
 1800000 
 , 
  
 "rpId" 
 : 
  
 "webauthn-codelab.glitch.me" 
 , 
  
 "userVerification" 
 : 
  
 "required" 
 , 
  
 "allowCredentials" 
 : 
  
 [ 
  
 { 
  
 "id" 
 : 
  
 "..." 
 , 
  
 "type" 
 : 
  
 "public-key" 
 , 
  
 "transports" 
 : 
  
 [ 
  
 "internal" 
  
 ] 
  
 } 
  
 ] 
 } 
 

The most important option here is allowCredentials . When you receive options from the server, allowCredentials should be either a single object in an array or an empty array depending on whether a credential with the ID in the query parameter is found on the server side.

  1. Resolve the promise with null when allowCredentials is an empty array so that the UI falls back to asking for a password.
  if 
  
 ( 
 options 
 . 
 allowCredentials 
 . 
 length 
  
 === 
  
 0 
 ) 
  
 { 
  
 console 
 . 
 info 
 (' 
 No 
  
 registered 
  
 credentials 
  
 found 
 .'); 
  
 return 
  
 Promise 
 . 
 resolve 
 ( 
 null 
 ); 
 } 
 

Locally verify the user and get a credential

  1. Because these options are delivered encoded in order to go through HTTP protocol, convert some parameters back to binary, specifically challenge and instances of id included in the allowCredentials array:

public/client.js

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

for (let cred of options.allowCredentials) {
  cred.id = base64url.decode(cred.id);
} 
  1. Call the navigator.credentials.get() method to verify the user's identity with a UVPA.

public/client.js

  const 
  
 cred 
  
 = 
  
 await 
  
 navigator 
 . 
 credentials 
 . 
 get 
 ({ 
  
 publicKey 
 : 
  
 options 
 }); 
 

Once the user verifies their identity, you should receive a credential object that you can send to the server and authenticate the user.

Verify the credential

Here's an example PublicKeyCredential object ( response is AuthenticatorAssertionResponse ) that you should have received:

  { 
  
 "id" 
 : 
  
 "..." 
 , 
  
 "type" 
 : 
  
 "public-key" 
 , 
  
 "rawId" 
 : 
  
 "..." 
 , 
  
 "response" 
 : 
  
 { 
  
 "clientDataJSON" 
 : 
  
 "..." 
 , 
  
 "authenticatorData" 
 : 
  
 "..." 
 , 
  
 "signature" 
 : 
  
 "..." 
 , 
  
 "userHandle" 
 : 
  
 "" 
  
 } 
 } 
 
  1. Encode the binary parameters of the credential so that it can be delivered to the server as a string:

public/client.js

  const 
  
 credential 
  
 = 
  
 {}; 
 credential 
 . 
 id 
  
 = 
  
 cred 
 . 
 id 
 ; 
 credential 
 . 
 type 
  
 = 
  
 cred 
 . 
 type 
 ; 
 credential 
 . 
 rawId 
  
 = 
  
 base64url 
 . 
 encode 
 ( 
 cred 
 . 
 rawId 
 ); 
 if 
  
 ( 
 cred 
 . 
 response 
 ) 
  
 { 
  
 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 and, if it returns HTTP code 200 , consider the user as successfully signed in:

public/client.js

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

You now have the complete authentication() function!

Final code for this section

public/client.js

  ... 
 export 
  
 const 
  
 authenticate 
  
 = 
  
 async 
  
 () 
  
 = 
>  
 { 
  
 const 
  
 opts 
  
 = 
  
 {} 
 ; 
  
 let 
  
 url 
  
 = 
  
 '/auth/signinRequest' 
 ; 
  
 const 
  
 credId 
  
 = 
  
 localStorage 
 . 
 getItem 
 ( 
 `credId` 
 ); 
  
 if 
  
 ( 
 credId 
 ) 
  
 { 
  
 url 
  
 += 
  
 `?credId=${encodeURIComponent(credId)}` 
 ; 
  
 } 
  
  
 const 
  
 options 
  
 = 
  
 await 
  
 _fetch 
 ( 
 url 
 , 
  
 opts 
 ); 
  
  
 if 
  
 ( 
 options 
 . 
 allowCredentials 
 . 
 length 
  
 === 
  
 0 
 ) 
  
 { 
  
 console 
 . 
 info 
 ( 
 'No registered credentials found.' 
 ); 
  
 return 
  
 Promise 
 . 
 resolve 
 ( 
 null 
 ); 
  
 } 
  
 options 
 . 
 challenge 
  
 = 
  
 base64url 
 . 
 decode 
 ( 
 options 
 . 
 challenge 
 ); 
  
 for 
  
 ( 
 let 
  
 cred 
  
 of 
  
 options 
 . 
 allowCredentials 
 ) 
  
 { 
  
 cred 
 . 
 id 
  
 = 
  
 base64url 
 . 
 decode 
 ( 
 cred 
 . 
 id 
 ); 
  
 } 
  
 const 
  
 cred 
  
 = 
  
 await 
  
 navigator 
 . 
 credentials 
 . 
 get 
 ( 
 { 
  
 publicKey 
 : 
  
 options 
  
 } 
 ); 
  
 const 
  
 credential 
  
 = 
  
 {} 
 ; 
  
 credential 
 . 
 id 
  
 = 
  
 cred 
 . 
 id 
 ; 
  
 credential 
 . 
 type 
  
 = 
  
 cred 
 . 
 type 
 ; 
  
 credential 
 . 
 rawId 
  
 = 
  
 base64url 
 . 
 encode 
 ( 
 cred 
 . 
 rawId 
 ); 
  
 if 
  
 ( 
 cred 
 . 
 response 
 ) 
  
 { 
  
 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. Enable reauthentication experience

Build UI

When the user comes back, you want them to reauthenticate as easily and securely as possible. This is where biometric authentication shines. However, there are cases in which biometric authentication may not work:

  • The UVPA is not available.
  • The user has not registered any credentials on their device yet.
  • The storage is cleared and the device no longer remembers the credential ID.
  • The user is unable to verify their identity for some reason, such as when their finger is wet or they're wearing a mask.

That is why it's always important that you provide other sign-in options as fallbacks. In this codelab, you use the form-based password solution.

19da999b0145054.png

  1. Add UI to show an authentication button that invokes the biometric authentication in addition to the password form.

Use the hidden class to selectively show and hide one of them depending on the user's state.

views/reauth.html

 <div id="uvpa_available" class="hidden">
  <h2>
    Verify your identity
  </h2>
  <div>
    <mwc-button id="reauth" raised>Authenticate</mwc-button>
  </div>
  <div>
    <mwc-button id="cancel">Sign-in with password</mwc-button>
  </div>
</div> 
  1. Append class="hidden" to the form:

views/reauth.html

 <form id="form" method="POST" action="/auth/password" class="hidden"> 

Feature detection and UVPA availability

Users must sign in with a password if one of these conditions is met:

  • WebAuthn is not available.
  • UVPA is not available.
  • A credential ID for this UVPA is not discoverable.

Selectively show the authentication button or hide it:

views/reauth.html

  if 
  
 ( 
 window 
 . 
 PublicKeyCredential 
 ) 
  
 { 
  
 PublicKeyCredential 
 . 
 isUserVerifyingPlatformAuthenticatorAvailable 
 () 
  
 . 
 then 
 ( 
 uvpaa 
  
 = 
>  
 { 
  
 if 
  
 ( 
 uvpaa 
 && 
 localStorage 
 . 
 getItem 
 ( 
 `credId` 
 )) 
  
 { 
  
 document 
  
 . 
 querySelector 
 ( 
 '#uvpa_available' 
 ) 
  
 . 
 classList 
 . 
 remove 
 ( 
 'hidden' 
 ); 
  
 } 
  
 else 
  
 { 
  
 form 
 . 
 classList 
 . 
 remove 
 ( 
 'hidden' 
 ); 
  
 } 
  
 } 
 ); 
  
 } 
  
 else 
  
 { 
  
 form 
 . 
 classList 
 . 
 remove 
 ( 
 'hidden' 
 ); 
 } 
 

Fallback to password form

The user should also be able to choose to sign in with a password.

Show the password form and hide the authentication button when the user clicks Sign in with password:.

views/reauth.html

  const 
  
 cancel 
  
 = 
  
 document 
 . 
 querySelector 
 ( 
 '#cancel' 
 ); 
 cancel 
 . 
 addEventListener 
 ( 
 'click' 
 , 
  
 e 
  
 = 
>  
 { 
  
 form 
 . 
 classList 
 . 
 remove 
 ( 
 'hidden' 
 ); 
  
 document 
  
 . 
 querySelector 
 ( 
 '#uvpa_available' 
 ) 
  
 . 
 classList 
 . 
 add 
 ( 
 'hidden' 
 ); 
 }); 
 

c4a82800889f078c.png

Invoke the biometric authentication

Finally, enable the biometric authentication.

  1. Append authenticate to the existing import statement:

views/reauth.html

  import 
  
 { 
 _fetch 
 , 
 authenticate 
 } 
 from 
  
 '/client.js' 
 ; 
 
  1. Invoke authenticate() when the user taps Authenticateto start the biometric authentication.

Make sure that a failure on biometric authentication falls back to the password form.

views/reauth.html

  const 
  
 button 
  
 = 
  
 document 
 . 
 querySelector 
 ( 
 '#reauth' 
 ); 
 button 
 . 
 addEventListener 
 ( 
 'click' 
 , 
  
 e 
  
 = 
>  
 { 
  
 authenticate 
 () 
 . 
 then 
 ( 
 user 
  
 = 
>  
 { 
  
 if 
  
 ( 
 user 
 ) 
  
 { 
  
 location 
 . 
 href 
  
 = 
  
 '/home' 
 ; 
  
 } 
  
 else 
  
 { 
  
 throw 
  
 'User not found.' 
 ; 
  
 } 
  
 }) 
 . 
 catch 
 ( 
 e 
  
 = 
>  
 { 
  
 console 
 . 
 error 
 ( 
 e 
 . 
 message 
  
 || 
  
 e 
 ); 
  
 alert 
 ( 
 'Authentication failed. Use password to sign-in.' 
 ); 
  
 form 
 . 
 classList 
 . 
 remove 
 ( 
 'hidden' 
 ); 
  
 document 
 . 
 querySelector 
 ( 
 '#uvpa_available' 
 ) 
 . 
 classList 
 . 
 add 
 ( 
 'hidden' 
 ); 
  
 }); 
  
 }); 
 

Final code for this section

views/reauth.html

  ... 
  
< main 
  
 class 
 = 
 "content" 
>  
< div 
  
 id 
 = 
 "uvpa_available" 
  
 class 
 = 
 "hidden" 
>  
< h2 
>  
 Verify 
  
 your 
  
 identity 
  
< / 
 h2 
>  
< div 
>  
< mwc 
 - 
 button 
  
 id 
 = 
 "reauth" 
  
 raised>Authenticate 
< / 
 mwc 
 - 
 button 
>  
< / 
 div 
>  
< div 
>  
< mwc 
 - 
 button 
  
 id 
 = 
 "cancel" 
> Sign 
 - 
 in 
  
 with 
  
 password 
< / 
 mwc 
 - 
 button 
>  
< / 
 div 
>  
< / 
 div 
>  
< form 
  
 id 
 = 
 "form" 
  
 method 
 = 
 "POST" 
  
 action 
 = 
 "/auth/password" 
  
 class 
 = 
 "hidden" 
>  
< h2 
>  
 Enter 
  
 a 
  
 password 
  
< / 
 h2 
>  
< input 
  
 type 
 = 
 "hidden" 
  
 name 
 = 
 "username" 
  
 value 
 = 
 "{{username}}" 
  
 / 
>  
< div 
  
 class 
 = 
 "mdc-text-field mdc-text-field--filled" 
>  
< span 
  
 class 
 = 
 "mdc-text-field__ripple" 
>< / 
 span 
>  
< label 
  
 class 
 = 
 "mdc-floating-label" 
  
 id 
 = 
 "password-label" 
> password 
< / 
 label 
>  
< input 
  
 type 
 = 
 "password" 
  
 class 
 = 
 "mdc-text-field__input" 
  
 aria 
 - 
 labelledby 
 = 
 "password-label" 
  
 name 
 = 
 "password" 
  
 / 
>  
< span 
  
 class 
 = 
 "mdc-line-ripple" 
>< / 
 span 
>  
< / 
 div 
>  
< input 
  
 type 
 = 
 "submit" 
  
 class 
 = 
 "mdc-button mdc-button--raised" 
  
 value 
 = 
 "Sign-In" 
  
 / 
>  
< p 
  
 class 
 = 
 "instructions" 
> password 
  
 will 
  
 be 
  
 ignored 
  
 in 
  
 this 
  
 demo 
 . 
< / 
 p 
>  
< / 
 form 
>  
< / 
 main 
>  
< script 
  
 src 
 = 
 "https://unpkg.com/material-components-web@7.0.0/dist/material-components-web.min.js" 
>< / 
 script 
>  
< script 
  
 type 
 = 
 "module" 
>  
 new 
  
 mdc 
 . 
 textField 
 . 
 MDCTextField 
 ( 
 document 
 . 
 querySelector 
 ( 
 ' 
 . 
 mdc 
 - 
 text 
 - 
 field 
 ' 
 )); 
  
 import 
  
 { 
  
 _fetch 
 , 
  
 authenticate 
  
 } 
  
 from 
  
 ' 
 / 
 client 
 . 
 js 
 ' 
 ; 
  
 const 
  
 form 
  
 = 
  
 document 
 . 
 querySelector 
 ( 
 '# 
 form 
 ' 
 ); 
  
 form 
 . 
 addEventListener 
 ( 
 ' 
 submit 
 ' 
 , 
  
 e 
  
 = 
>  
 { 
  
 e 
 . 
 preventDefault 
 (); 
  
 const 
  
 form 
  
 = 
  
 new 
  
 FormData 
 ( 
 e 
 . 
 target 
 ); 
  
 const 
  
 cred 
  
 = 
  
 {}; 
  
 form 
 . 
 forEach 
 (( 
 v 
 , 
  
 k 
 ) 
  
 = 
>  
 cred 
 [ 
 k 
 ] 
  
 = 
  
 v 
 ); 
  
 _fetch 
 ( 
 e 
 . 
 target 
 . 
 action 
 , 
  
 cred 
 ) 
  
 . 
 then 
 ( 
 user 
  
 = 
>  
 { 
  
 location 
 . 
 href 
  
 = 
  
 ' 
 / 
 home 
 ' 
 ; 
  
 }) 
  
 . 
 catch 
 ( 
 e 
  
 = 
>  
 alert 
 ( 
 e 
 )); 
  
 }); 
  
 if 
  
 ( 
 window 
 . 
 PublicKeyCredential 
 ) 
  
 { 
  
 PublicKeyCredential 
 . 
 isUserVerifyingPlatformAuthenticatorAvailable 
 () 
  
 . 
 then 
 ( 
 uvpaa 
  
 = 
>  
 { 
  
 if 
  
 ( 
 uvpaa 
 && 
 localStorage 
 . 
 getItem 
 ( 
 ` 
 credId 
 ` 
 )) 
  
 { 
  
 document 
  
 . 
 querySelector 
 ( 
 '# 
 uvpa_available 
 ' 
 ) 
  
 . 
 classList 
 . 
 remove 
 ( 
 ' 
 hidden 
 ' 
 ); 
  
 } 
  
 else 
  
 { 
  
 form 
 . 
 classList 
 . 
 remove 
 ( 
 ' 
 hidden 
 ' 
 ); 
  
 } 
  
 }); 
  
  
 } 
  
 else 
  
 { 
  
 form 
 . 
 classList 
 . 
 remove 
 ( 
 ' 
 hidden 
 ' 
 ); 
  
 } 
  
 const 
  
 cancel 
  
 = 
  
 document 
 . 
 querySelector 
 ( 
 '# 
 cancel 
 ' 
 ); 
  
 cancel 
 . 
 addEventListener 
 ( 
 ' 
 click 
 ' 
 , 
  
 e 
  
 = 
>  
 { 
  
 form 
 . 
 classList 
 . 
 remove 
 ( 
 ' 
 hidden 
 ' 
 ); 
  
 document 
  
 . 
 querySelector 
 ( 
 '# 
 uvpa_available 
 ' 
 ) 
  
 . 
 classList 
 . 
 add 
 ( 
 ' 
 hidden 
 ' 
 ); 
  
 }); 
  
 const 
  
 button 
  
 = 
  
 document 
 . 
 querySelector 
 ( 
 '# 
 reauth 
 ' 
 ); 
  
 button 
 . 
 addEventListener 
 ( 
 ' 
 click 
 ' 
 , 
  
 e 
  
 = 
>  
 { 
  
 authenticate 
 (). 
 then 
 ( 
 user 
  
 = 
>  
 { 
  
 if 
  
 ( 
 user 
 ) 
  
 { 
  
 location 
 . 
 href 
  
 = 
  
 ' 
 / 
 home 
 ' 
 ; 
  
 } 
  
 else 
  
 { 
  
 throw 
  
 ' 
 User 
  
 not 
  
 found 
 . 
 ' 
 ; 
  
 } 
  
 }). 
 catch 
 ( 
 e 
  
 = 
>  
 { 
  
 console 
 . 
 error 
 ( 
 e 
 . 
 message 
  
 || 
  
 e 
 ); 
  
 alert 
 ( 
 ' 
 Authentication 
  
 failed 
 . 
  
 Use 
  
 password 
  
 to 
  
 sign 
 - 
 in 
 . 
 ' 
 ); 
  
 form 
 . 
 classList 
 . 
 remove 
 ( 
 ' 
 hidden 
 ' 
 ); 
  
 document 
 . 
 querySelector 
 ( 
 '# 
 uvpa_available 
 ' 
 ). 
 classList 
 . 
 add 
 ( 
 ' 
 hidden 
 ' 
 ); 
  
 }); 
  
  
 }); 
  
< / 
 script 
> ... 
 

7. Congratulations!

You finished this codelab!

Learn more

Special thanks to Yuriy Ackermann from FIDO Alliance for your help.

Create a Mobile Website
View Site in Mobile | Classic
Share by: