WebAuthn Guide: Biometric Login with JavaScript

Web Authentication (WebAuthn) is a modern web standard that enables secure, passwordless authentication by allowing web applications to interact directly with built-in device biometrics (such as Apple Touch ID, Face ID, and Windows Hello) and external hardware security keys (such as YubiKeys). This article explains what WebAuthn is, how public-key cryptography powers its architecture, and how client-side JavaScript utilizes the Credential Management API to register credentials and authenticate users securely without transmitting sensitive passwords over the network.

What is WebAuthn?

WebAuthn is a core component of the FIDO2 standard developed by the FIDO Alliance and the World Wide Web Consortium (W3C). It replaces traditional shared secrets (like passwords or SMS codes) with asymmetric public-key cryptography.

When a user registers with a service using WebAuthn: 1. The user’s device (the Authenticator) generates a unique cryptographic key pair: a private key stored securely in the hardware (e.g., Secure Enclave or TPM) and a public key sent to the website (the Relying Party). 2. For subsequent logins, the server challenges the client to sign a cryptographic payload with the private key. 3. The client unlocks the private key using local biometrics or a PIN to sign the challenge, proving their identity to the server without the private key ever leaving the device.

The Role of JavaScript in WebAuthn

JavaScript bridges the browser and the underlying authentication hardware via the navigator.credentials interface, a component of the Credential Management API. JavaScript handles two primary workflows: Registration (credential creation) and Authentication (credential assertion).

1. Registration Flow (navigator.credentials.create)

During registration, the server issues a creation request containing user details and a random cryptographic challenge. JavaScript passes these options to the browser:

async function registerUser() {
  // 1. Fetch challenge and options from the server
  const response = await fetch('/api/register-challenge');
  const creationOptions = await response.json();

  // 2. Format binary fields (e.g., challenge, user ID) to ArrayBuffers
  creationOptions.publicKey.challenge = Uint8Array.from(
    atob(creationOptions.publicKey.challenge), c => c.charCodeAt(0)
  );
  creationOptions.publicKey.user.id = Uint8Array.from(
    atob(creationOptions.publicKey.user.id), c => c.charCodeAt(0)
  );

  // 3. Prompt the device for biometric or hardware authentication
  const credential = await navigator.credentials.create({
    publicKey: creationOptions.publicKey
  });

  // 4. Send the public key and attestation object back to the server
  await fetch('/api/register-complete', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(credential)
  });
}

When navigator.credentials.create() is invoked, the browser prompts the user to scan a fingerprint, look into a camera, or tap a physical USB/NFC key. Once validated, the generated public key is sent back to the server for storage.

2. Authentication Flow (navigator.credentials.get)

During authentication, the server generates a new random challenge. JavaScript uses navigator.credentials.get() to request a signature from the authenticator:

async function loginUser() {
  // 1. Fetch the authentication challenge from the server
  const response = await fetch('/api/login-challenge');
  const requestOptions = await response.json();

  // 2. Convert the challenge to an ArrayBuffer
  requestOptions.publicKey.challenge = Uint8Array.from(
    atob(requestOptions.publicKey.challenge), c => c.charCodeAt(0)
  );

  // 3. Prompt the user for biometric or security key verification
  const assertion = await navigator.credentials.get({
    publicKey: requestOptions.publicKey
  });

  // 4. Send the signed assertion to the server for cryptographic validation
  await fetch('/api/login-complete', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(assertion)
  });
}

The device signs the server challenge using the private key associated with the website’s domain, and the server validates the signature against the previously stored public key.

Key Security Advantages