What is WebAuthn? What is FIDO2?

The FIDO2 / WebAuthn allows you to create and use strong, attested public key based credentials for the purpose of authenticating users. The API supports the use of BLE, NFC, and USB roaming authenticators (security keys) as well as a platform authenticator, which allows the user to authenticate using their fingerprint or screenlock.

What you'll build...

In this codelab, you are going to build a website with a simple re-authentication functionality using fingerprint sensor. Re-authentication is a concept where a user signs into a website once, then authenticate again as they try to enter important sections of the website, or come back after a certain interval, etc in order to protect the account.

What you'll learn...

You will learn how to call the WebAuthn API and options you can provide in order to cater various occasions. You will also learn re-auth specific best practices and tricks.

What you'll need...

To work on this codelab, we'll be using a service called glitch. This is where you can edit both client and server side code using JavaScript and deploy them instantly. Head to the following URL:

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

See how it works at the beginning

Let's see the initial state of the website first. Click "Show" at the top and press "Next to The Code" to see the live website side by side.

  1. Enter a username and submit (no registration is required, any username will create a new account)
  2. Enter a password and submit (password will be ignored and user will be authenticated nevertheless)
  3. User lands at home page. Clicking "Sign out" will sign you out. Clicking "Try reauth" sends you back to 2.

What are we going to implement?

  1. Let users register a "user verifying platform authenticator" (the Android phone with fingerprint sensor itself can act as one).
  2. Let users re-authenticate themselves to the app using their fingerprint.

You can preview what you are going to build from here.

Remix the code

In https://glitch.com/edit/#!/webauthn-codelab-start, find "Remix to Edit" button at the top right corner. By pressing the button, you can "fork" the glitch and continue with your own version of the project with a new URL.

Let's move on!

You first need to register a credential generated by a user verifying platform authenticator - an authenticator that is embedded onto the platform and verifies the user identity using biometrics or screenlock.

We are adding this feature to the /home page.

Create registerCredential() function

Let's create a function called registerCredential() which registers a credential using a fingerprint.

public/client.js

export const registerCredential = async (opts) => {

};

Feature detection

Now, let's add WebAuthn code. First thing you should do is to detect whether WebAuthn is available. We can achieve this by examining if window.PublicKeyCredential exists. We'll throw an exception if the feature is not available.

public/client.js

if (!window.PublicKeyCredential) {
  throw 'WebAuthn not supported on this browser.';
}

Is User Verifying Platform Authenticator available?

Re-auth is most useful when the authenticator is a user verifying platform authenticator and that is what we will use. There's a handy function that can detect if there is a user verifying platform authenticator available called PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(). If it's not available, throw an exception.

public/client.js

const UVPAA = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
if (!UVPAA) {
  throw 'User Verifying Platform Authenticator not available.';
}

Obtain the challenge and other options from server endpoint: /auth/registerRequest

Before asking the user to provide a credential using fingerprint, 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('/auth/registerRequest', opts);

Here's an example options you will be receiving (aligns with PublicKeyCredentialCreationOptions).

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

To learn about these options, see the official specification of the WebAuthn. Some important ones are explained at "Register a credential" section in the next page.

Create a credential

Because these options are delivered encoded in order to go through HTTP protocol, you have to convert some parameters back to binary - specifically, user.id, challenge and ids included in 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);
  }
}

And finally call the navigator.credentials.create() method in order to create a new credential. With this call, the browser will interact with the authenticator and tries to verify the user's identity using a fingerprint sensor or a screenlock.

public/client.js

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

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

Register the credential to the server endpoint: /auth/registerResponse

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

{
  "id": "...",
  "rawId": "...",
  "type": "public-key",
  "response": {
    "clientDataJSON": "...",
    "attestationObject": "..."
  }
}

Like when you received an option object for registering a credential, you should 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
  };
}

Store the credential id locally so that we can use it for authentication when the user comes back.

public/client.js

localStorage.setItem(`credId`, credential.id);

Finally, send the object to the server and if it returns HTTP code 200, consider the new credential has been successfully registered.

public/client.js

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

Congratulations, you now have the complete registerCredential() function!

It will be nice to have a list of registered credentials along with buttons to remove them.

Build UI placeholder

Let's add UI to list credentials and a button to register a new credential. ul#list will be the placeholder for adding a list of registered credentials.

views/home.html

<h3 class="mdc-typography mdc-typography--headline6">
  Your registered credentials:
</h3>
<section>
  <div id="list"></div>
  <button id="register" class="mdc-fab mdc-ripple-upgraded">
    <i class="mdc-fab__icon material-icons">+</i>
  </button>
</section>

Get a list of credentials and display: getCredentials()

Let's create getCredentials() function so you can get registered credentials and display them in a list. Luckily, we already have a handy endpoint on the server /auth/getKeys which you can fetch registered credentials for the signed-in user.

The returned JSON includes credential information such as id and publicKey. By building HTML you can show them to the user.

views/home.html

const getCredentials = async () => {
  const res = await _fetch('/auth/getKeys');
  const list = document.querySelector('#list');
  if (res.credentials.length === 0) {
    list.innerHTML = '<div>No credentials found.</div>';
    return;
  }
  const creds = html`${repeat(res.credentials, cred => cred.credId,(cred, index) =>
    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">
        <button id="${cred.credId}" class="mdc-button mdc-button--raised" @click="${removeCredential}">Remove</button>
      </div>
    </div>`)}`;
  render(creds, list);
};

Let's display available credentials as soon as user lands on /home by invoking getCredentials().

views/home.html

getCredentials();

Remove the credential: removeCredential()

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

public/client.js

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

views/home.html

import { _fetch, unregisterCredential } from '/client.js';

views/home.html

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

Register a credential

Finally, call registerCredential() to register a new credential when (+) button is clicked. Don't forget to renew the credential list by calling getCredentials() after registration.

Import registerCredential from client.js we created earlier:

views/home.html

import { _fetch, registerCredential, unregisterCredential } from '/client.js';

Invoke registerCredential() with options for navigator.credentials.create() when the button is clicked.

views/home.html

const register = document.querySelector('#register');
register.addEventListener('click', e => {
  registerCredential({
    attestation: 'none',
    authenticatorSelection: {
      authenticatorAttachment: 'platform',
      userVerification: 'required'
    }
  })
  .then(user => {
    getCredential();
  })
  .catch(e => alert(e));
});

Following is important options to remember to pass in registerCredential()(PublicKeyCredentialCreationOptions we referred earlier).

attestation

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

excludeCredentials

Array of credential descriptors 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".

requireResidentKey

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

userVerification

Determine whether authenticator local user verification is "required", "preferred" or "discouraged". If you want fingerprint or screenlock auth happen, use "required".

OK, you should now be able to register a new credential and display information about those registered credentials. You may try it on your live website.

We now have a credential registered and ready to use it as a way to authenticate the user. Let's add re-auth functionality to the website. Here's the user experience:

As soon as a user lands on /reauth, the website asks for re-auth using a fingerprint. When the user succeeds to authenticate, forward the user to /home, otherwise fallback to use the existing form to enter and submit a password.

Create authenticate() function:

Let's create a function called authenticate() which verifies user identity using a fingerprint. We'll be adding JavaScript code here.

public/client.js

export const authenticate = async (opts) => {

};

Feature detection and User Verifying Platform Authenticator check

We can replicate the same behavior we did on registration.

public/client.js

if (!window.PublicKeyCredential) {
  console.info('WebAuthn not supported on this browser.');
  return Promise.resolve(null)
}
const UVPAA = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
if (!UVPAA) {
  console.info('User Verifying Platform Authenticator not available.');
  return Promise.resolve(null);
}

Obtain the challenge and other options from server endpoint: /auth/signinRequest

Before authenticating, let's examine if the user has a stored credential id and set it as a query param if they do. By providing a credential id along with other options, the server can provide relevant allowCredentials and this will make user verification reliable.

public/client.js

let url = '/auth/signinRequest';
const credId = localStorage.getItem(`credId`);
if (credId) {
  url += `?credId=${encodeURIComponent(credId)}`;
}

Before asking 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's an example options you should be receiving (aligns with PublicKeyCredentialRequestOptions).

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

When you receive options from the server, allowCredentials could be one of the following:

allowCredentials

Explanation

An empty array

There are no credentials stored on the server.

Single object in an array

The specified credential id matched one of credentials stored on the server.

Multiple objects in an array

No credential id was specified.

Let's skip WebAuthn when there are no credentials stored for the user (allowCredentials is an empty array).

if (options.allowCredentials.length === 0) {
  console.info('No registered credentials found.');
  return Promise.resolve(null);
}

Locally verify the user and get a credential

Because these options are delivered encoded in order to go through HTTP protocol, you have to convert some parameters back to binary - specifically, challenge and ids included in allowCredentials array:

public/client.js

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

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

And finally call the navigator.credentials.get() method in order to verify the user's identity using a fingerprint sensor or a screenlock.

public/client.js

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

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

Verify the user identity: /auth/signinResponse

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

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

Again, 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
  };
}

Don't forget to store the credential id locally so that we can use it for authentication when the user comes back.

public/client.js

localStorage.setItem(`credId`, credential.id);

Finally, send the object to the server and if it returns HTTP code 200, consider the user has been successfully signed-in.

public/client.js

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

Congratulations, you now have the complete authencation() function!

Apply the biometric authentication when user comes back

To enable the reauth step, all you need is to run the authentication() as soon as user lands /reauth.

Import authenticate from client.js we created earlier.

views/reauth.html

import { _fetch, authenticate } from '/client.js';

Invoke authenticate() immediately.

views/reauth.html

authenticate({}).then(user => {
  if (user) {
    location.href = '/home';
  }
}).catch(e => {
  console.error(e);
  console.info('Authentication failed. Use password to sign-in.');
});

You have successfully finished the codelab - Your first WebAuthn.

What you've learned

Next step

You can learn both by trying out the Your first Android FIDO2 API codelab!

Resources

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