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
- Phishing Resistance: Authenticator signatures are bound directly to the website’s domain (origin). If a user is tricked into visiting a malicious replica domain, the browser will refuse to supply the credential.
- No Server-Side Shared Secrets: Because the server only stores public keys, database breaches do not expose credentials that attackers can reuse.
- Privacy-Preserving Biometrics: Biometric data (fingerprint or facial scans) never leaves the local device; JavaScript only receives a signed cryptographic response indicating that verification succeeded.