What is WebAuthn?

The Web Authentication API is an authentication specification that allows Websites to authenticate users with built-in authenticators like Apple's TouchID and Windows Hello, or using security keys like Yubikey.

WebAuthn Login with Cotter

It utilizes public-key cryptography instead of passwords. When the user registers, a private-public key pair is generated for the account, and the private key is stored securely in the user's device, while the public key is sent to the server. The server can then ask the user's device to sign a challenge using the private key to authenticate the user.

Registration with WebAuthn

Usually, a website will ask the user to enter a username and password. With WebAuthn, the website will generate this public-private key pair and send the public key to the server and store the private key securely in the user's device.

WebAuthn Registration Flow

Logging in with WebAuthn

This is where websites usually check whether the user has provided the right username and password. With WebAuthn, the website will send a challenge and check if the browser can sign the challenge using the private key that's stored in the user's device.

WebAuthn Login Flow

Implementation

We've built a simple way to implement WebAuthn using Cotter that you can do in just a few minutes.

Try WebAuthn in Action.

We've made a simple site for you to try it out: https://cotter.herokuapp.com/

  1. Make sure you're using Google Chrome on a laptop that supports TouchID/Windows Hello.
  2. Registration: If this is your first time logging in, you'll be prompted to enable TouchID or Windows Hello after your email is verified.
  3. Login: Go to an incognito tab and open this URL again. You need to allow third-party cookies (eye icon on URL bar). Try logging in with the same email, and you'll be prompted to log in using WebAuthn.

Short Guide for Implementing WebAuthn with React

yarn add cotter

Implement Login with WebAuthn

  1. Import Cotter
  2. Call signInWithWebAuthnOrLink to use WebAuthn with Magic Link as the fallback method, followed by showEmailForm or showPhoneForm, and get the response as a promise.
  3. Setup a <div> with id="cotter-form-container" that will contain the form.
import React, { useEffect, useState } from "react";
import Cotter from "cotter"; //  1️⃣  Import Cotter

function App() {
  const [payload, setpayload] = useState(null);

  //  2️⃣ Initialize and show the form
  useEffect(() => {
    var cotter = new Cotter(API_KEY_ID); // 👈 Specify your API KEY ID here
    cotter
      .signInWithWebAuthnOrLink()  // 👈 sign in using WebAuthn
      .showEmailForm()
      .then(response => {
        setpayload(response); // show the response in our state
      })
      .catch(err => console.log(err));
  }, []);

  return (
    <div>
      {/*  3️⃣  Put a <div> that will contain the form */}
      <div id="cotter-form-container" style={{ width: 300, height: 300 }} />
      
      <pre>{JSON.stringify(payload, null, 4)}</pre>
    </div>
  );
}

export default App;
App.js

​You'll need an API_KEY_ID , create a project, and copy the API KEY from the dashboard.

  • If the email is not recognized as a user, then Cotter will ask the user to verify their email by sending a MagicLink. After the user verified their email, Cotter's SDK will ask the user to enable WebAuthn login by registering the current device.
  • If the email is a user that has WebAuthn set up, then Cotter will try to authenticate the user with WebAuthn. Alternatively, the user can choose to send a magic link to their email to login.

Implementation with vanilla JS

To learn more about WebAuthn, here's a more in-depth explanation about how to implement WebAuthn with purely JavaScript. Check out Apple's guide from WWDC20.

Registration

Step 1: Your site requests the server for registering WebAuthn.

Ask the user to enter some identifier (username, email, etc), then send the request to your server asking to register a new WebAuthn credential.

Step 2: The server specifies some options for creating the new keypair.

The server specify a PublicKeyCredentialCreationOptions object that contains some required and optional fields for creating the new PublicKeyCredential (our keypair).

const optionsFromServer = {
    "challenge": "random_string", // need to convert to ArrayBuffer
    "rp": {  					  // my website info
      "name": "My Website",
      "id": "mywebsite.com"
    },
    "user": {                     // user info
      "name": "[email protected]",  				
      "displayName": "Anthony",
      "id": "USER_ID_12345678910" // need to convert to ArrayBuffer
    },
    "pubKeyCredParams": [
      {
        "type": "public-key",
        "alg": -7				  // Accepted Algorithm
      }
    ],
    "authenticatorSelection": {
    	authenticatorAttachment: "platform",
    },
    "timeout": 60000              // in milliseconds
};

rp : This is for specifying information about the relying party. The relying party is the website where the user is registering/logging-in into. If the user is registering to your website, then your website is the relying party.

  • id: The host's domain name, without the scheme or port. For example, if the RP's origin is https://login.example.com:1337, then the id is login.example.com or example.com, but not m.login.example.com.

pubKeyCredParams : What public key types are acceptable to the server.

  • alg : A number that describes what algorithm the server accepts, and is described in the COSE registry under COSE Algorithms. For example, -7 is for ES256 algorithm.

authenticatorSelection : (Optional) Restrict authenticator to be either platform or cross-platform. Use platform to allow authenticators like Windows Hello or TouchID. Use cross-platform to allow authenticators like Yubikey.

Step 3: In the front-end, use the options to create a new keypair.

Using our creationOptions , we can now tell the browser to generate a new keypair.

// make sure you've converted the strings to ArrayBuffer
// as mentioned above
const credential = await navigator.credentials.create({
    publicKey: optionsFromServer 
});

The credential that is returned will look like below:

PublicKeyCredential {
    id: 'ABCDESKa23taowh09w0eJG...',
    rawId: ArrayBuffer(59),
    response: AuthenticatorAttestationResponse {
        clientDataJSON: ArrayBuffer(121),
        attestationObject: ArrayBuffer(306),
    },
    type: 'public-key'
}

Step 4: Send the credential to your server

First, you might need to convert the ArrayBuffers to either base64 encoded strings or just to string. You'll need to decode this in your server.

Follow the specifications to validate the credential in your server. You should then store the Credential information to allow the user to login with this WebAuthn Credential.

Login

Step 1: Send a request to your server to login

This allows the server to send a challenge that your front-end needs to sign.

Step 2: The server sends a challenge and a list of WebAuthn credentials that the user can log in from.

The server specify a PublicKeyCredentialRequestOptions object that contains the challenge to sign and a list of WebAuthn credentials that the user has registered previously.

const optionsFromServer = {
    "challenge": "somerandomstring",  // Need to convert to ArrayBuffer
    "timeout": 60000,
    "rpId": "mywebsite.com",
    "allowCredentials": [
      {
        "type": "public-key",
        "id": "AdPc7AjUmsefw37..."   // Need to convert to ArrayBuffer
      }
    ]
}

Step 3: The front-end signs the challenge

// make sure you've converted the strings to ArrayBuffer
// as mentioned above
const assertion = await navigator.credentials.get({
    publicKey: optionsFromServer
});

The assertion that is returned looks like this:

PublicKeyCredential {
    id: 'ABCDESKa23taowh09w0eJG...',	// The WebAuthn Credential ID
    rawId: ArrayBuffer(59),
    response: AuthenticatorAssertionResponse {
        authenticatorData: ArrayBuffer(191),
        clientDataJSON: ArrayBuffer(118),
        signature: ArrayBuffer(70),     // The signature that we need to verify
        userHandle: ArrayBuffer(10),
    },
    type: 'public-key'
}

Step 4: Send the assertion to your server and verify it

You might need to convert the ArrayBuffers to a string before sending it to the server. Follow the specifications on verifying the assertion.

When the assertion is verified, this means that the user has successfully logged in. This would be the place to generate your session tokens or set your cookies and return to the front-end.

A few things to think about:

If the user logs in with their laptop's TouchID, how do you allow them to log in from someone else's laptop?

It might be a bad user experience if they can only log in from their own laptop. A possible way is to use WebAuthn as an alternative, and always have a fallback login method (for example, using magic links or OTP).

Adding more than one WebAuthn credential for one account.

You might want to have a "Settings" page that allows your user to allow WebAuthn login from other devices. For example, they want to login with WebAuthn both from their laptop and their iPad.

The browser doesn't know which credentials you have saved in your server for a user. If your user already registered their laptop's WebAuthn credential, you need to tell the browser so that it doesn't create a new credential. Use the excludeCredentials in the PublicKeyCredentialCreationOptions.

Support for WebAuthn

Not all browsers support WebAuthn yet, but it's growing. Check out FIDO's website for a list of Browsers and Platforms that supports WebAuthn.

That's It!

This should cover the basics about registering and logging-in with WebAuthn, and help you implement it on your site. This post is written by the team at Cotter – we are building lightweight, fast, and passwordless login solution for websites and mobile apps.

If you want to implement WebAuthn, our documentation might help:


References

We referred to these incredibly helpful articles to write this post:


Questions & Feedback

If you need help or have any feedback, ping us on Cotter's Slack Channel! We're here to help.

Ready to use Cotter?

If you enjoyed this post and want to integrate Cotter into your website or app, you can create a free account and check out our documentation.