Web Authentication

Web Authentication, or WebAuthn for short, is a W3C recommendation for defining an API enabling the creation and use of strong, attested, scoped, public key-based credentials by web applications, for the purpose of strongly authenticating users.

WebAuthn works hand in hand with other industry standards such as Credential Management Level 1 and FIDO 2.0 Client to Authenticator Protocol 2.

How It Works

WebAuthn has three main entities: the authenticator, the client and the relying party and they work together in two separate use cases: registration and authentication. All communications between the different entities in the diagram are handled by the user agent (usually a web browser).

Web Authentication Entities

Registration makes the authenticator create a new set of public-key credentials that can be used to sign a challenge generated by the relying party. The public part of these new credentials, along with the signed challenge, can be sent back to the relying party for storage. The relying party can later use these credentials to verify the identity of a user whenever required.

Registration

Authentication, in contrast, allows the relying party to send a challenge to the authenticator. This challenge can then be signed with the previously generated public-key credentials and sent back to the relying party. This way, the relying party can verify that a user is in possession of the required credentials, proving their identity.

There are two types of authenticators. Roaming authenticators connect to your devices through USB, Bluetooth or NFC, and platform authenticators are built in.

While roaming authenticators are widely supported in most modern browsers, platform authenticators support is running a bit behind. Find out if your current browser supports roaming and platform authenticators.

Authentication

Both processes work with the help of public-key cryptography and digital signatures. If you are not familiar with how public-key cryptography or digital signatures work, the important thing you need to know is that there is a public key and a private key.

The private key is secret, and only the user (or the authenticator) need to know it. The public key, in contrast, can be seen or stored by anyone. The public key can be used to verify signatures generated by the private key. No other key, other than the private key, can generate a signature that the public key can verify as valid. This way, the relying party can store a public key and use it to verify signatures performed by the user holding the private key.

The API

The Web Authentication API has two main calls: navigator.credentials.create, and navigator.credentials.get.

create can be used to perform the registration step. get can be used to perform the authentication step.

Here is a sample call of the create function inside an EJS template:

navigator.credentials
  .create({
    publicKey: {
      // random, cryptographically secure, at least 16 bytes
      challenge: base64url.decode("<%= challenge %>"),
      // relying party
      rp: {
        name: "Awesome Corp", // sample relying party
      },
      user: {
        id: base64url.decode("<%= id %>"),
        name: "<%= name %>",
        displayName: "<%= displayName %>",
      },
      authenticatorSelection: { userVerification: "preferred" },
      attestation: "direct",
      pubKeyCredParams: [
        {
          type: "public-key",
          alg: -7, // "ES256" IANA COSE Algorithms registry
        },
      ],
    },
  })
  .then((res) => {
    var json = publicKeyCredentialToJSON(res);
    // Send data to relying party's servers
    post("/webauthn/register", {
      state: "<%= state %>",
      provider: "<%= provider %>",
      res: JSON.stringify(json),
    });
  })
  .catch(console.error);

Here is a sample call of the get function in an EJS template:

navigator.credentials
  .get({
    publicKey: {
      // random, cryptographically secure, at least 16 bytes
      challenge: base64url.decode("<%= challenge %>"),
      allowCredentials: [
        {
          id: base64url.decode("<%= id %>"),
          type: "public-key",
        },
      ],
      timeout: 15000,
      authenticatorSelection: { userVerification: "preferred" },
    },
  })
  .then((res) => {
    var json = publicKeyCredentialToJSON(res);
    // Send data to relying party's servers
    post("/webauthn/authenticate", {
      state: "<%= state %>",
      provider: "<%= provider %>",
      res: JSON.stringify(json),
    });
  })
  .catch((err) => {
    alert("Invalid FIDO device");
  });

Glossary