Web biometrics transform how users prove their identity online. Instead of typing passwords, users can use physical characteristics like fingerprints or facial recognition. When you unlock your phone with your fingerprint, youβre using biometric authentication - the web version works similarly.
Modern browsers support several biometric authentication methods:
The Web Authentication API (WebAuthn) powers biometric authentication across modern browsers:
Chrome 67+ β Full Support
Firefox 60+ β Full Support
Safari 13+ β Full Support
Edge 18+ β Full Support
iOS 14+ β Full Support
Android 7+ β Full Support (with hardware)
Each browser handles biometrics slightly differently:
// BiometricAuth.jsx
import React, { useState } from "react";
import {
startRegistration,
startAuthentication,
} from "@simplewebauthn/browser";
import { supabase } from "../lib/supabase";
export function BiometricAuth() {
const [status, setStatus] = useState("idle");
const registerBiometric = async () => {
try {
setStatus("registering");
// Get registration options from server
const { data } = await supabase.functions.invoke(
"get-registration-options"
);
// Start browser registration process
const response = await startRegistration(data.options);
// Verify with server
await supabase.functions.invoke("verify-registration", {
body: { response },
});
setStatus("registered");
} catch (error) {
setStatus("error");
console.error(error);
}
};
return (
<div>
<button onClick={registerBiometric} disabled={status === "registering"}>
Register Fingerprint
</button>
{status === "registered" && (
<div>Fingerprint registered successfully</div>
)}
</div>
);
}
create table public.credentials (
id uuid primary key default uuid_generate_v4(),
user_id uuid references auth.users not null,
public_key bytea not null,
counter bigint default 0,
created_at timestamptz default now()
);
-- Enable row-level security
alter table public.credentials enable row level security;
-- Allow users to manage their own credentials
create policy "Users manage own credentials"
on public.credentials
for all
using (auth.uid() = user_id);
// verify-registration.ts
import { verifyRegistrationResponse } from "@simplewebauthn/server";
export async function handler(req: Request) {
const { response } = await req.json();
const verification = await verifyRegistrationResponse({
response,
expectedOrigin: "https://your-app.com",
expectedRPID: "your-app.com",
});
if (verification.verified) {
await supabase.from("credentials").insert({
user_id: req.user.id,
public_key: verification.registrationInfo.credentialPublicKey,
});
}
return new Response(JSON.stringify({ verified: verification.verified }));
}
HTTPS represents a fundamental requirement for biometric authentication. Without it, browsers block WebAuthn operations entirely. The security model depends on challenge-response verification - each authentication attempt generates a unique challenge that expires after use. This prevents replay attacks where malicious actors could capture and reuse authentication data.
Public key storage influences the overall security posture. Storing public keys in databases requires careful consideration of the storage format - while some databases handle byte arrays natively, others need base64 encoding. The storage must maintain the exact byte representation of the key without any corruption that could occur during type conversions.
Biometric authentication flows start with sensor availability checks. Different devices expose different capabilities - laptops might offer fingerprint readers, while phones could provide facial recognition. The application needs to detect these capabilities and present appropriate options to users.
const checkBiometricCapabilities = async () => {
if (!window.PublicKeyCredential) {
return { available: false, reason: "API_NOT_SUPPORTED" };
}
const available =
await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
if (!available) {
return { available: false, reason: "NO_AUTHENTICATOR" };
}
// Check for specific capabilities
const capabilities = await navigator.credentials
.get({
publicKey: {
challenge: new Uint8Array(32),
userVerification: "preferred",
},
})
.catch(() => null);
return {
available: true,
capabilities: {
fingerprint: capabilities?.fingerprint ?? false,
faceID: capabilities?.faceID ?? false,
},
};
};
Status feedback during biometric operations proves crucial. Users need clear indicators of:
function BiometricStatus({ state, onRetry }) {
const states = {
waiting: {
icon: <ScanningIcon />,
message: "Waiting for biometric verification...",
},
success: {
icon: <CheckIcon />,
message: "Verification complete",
},
error: {
icon: <ErrorIcon />,
message: "Verification failed",
action: <button onClick={onRetry}>Try Again</button>,
},
};
const current = states[state];
return (
<div className="flex items-center gap-2">
{current.icon}
<span>{current.message}</span>
{current.action}
</div>
);
}
Browser compatibility extends beyond simple version checks. Different browsers implement the WebAuthn spec with subtle variations:
const getBrowserCapabilities = () => {
const ua = navigator.userAgent;
const isSafari = /^((?!chrome|android).)*safari/i.test(ua);
const isChrome = /chrome|chromium/i.test(ua);
return {
requiresUserGesture: isSafari,
supportsResidentKeys: isChrome,
maxTimeout: isSafari ? 300000 : 600000, // Safari has shorter timeout
};
};
Timeout handling affects reliability. Biometric operations can fail if they take too long, but timeouts that are too short might interrupt slow sensors or users who need more time:
const withTimeout = (promise, ms) => {
let timeoutId;
const timeout = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error("Biometric verification timed out"));
}, ms);
});
return Promise.race([promise, timeout]).finally(() => {
clearTimeout(timeoutId);
});
};
// Usage
const authenticateWithTimeout = () => {
const capabilities = getBrowserCapabilities();
return withTimeout(startAuthentication(options), capabilities.maxTimeout);
};
Concurrent request management prevents race conditions where multiple authentication attempts overlap. A simple semaphore pattern helps:
class BiometricLock {
constructor() {
this.locked = false;
}
async acquire() {
if (this.locked) {
throw new Error("Biometric operation in progress");
}
this.locked = true;
}
release() {
this.locked = false;
}
}
const biometricLock = new BiometricLock();
async function authenticateUser() {
try {
await biometricLock.acquire();
const result = await startAuthentication(options);
return result;
} finally {
biometricLock.release();
}
}
Biometric authentication brings native-level security to web applications. The WebAuthn API provides a robust foundation for implementing these features, while libraries like SimpleWebAuthn simplify the integration process. As browser support continues to evolve, biometric authentication will likely become increasingly prevalent in web applications.
The WebAuthn specification and browser documentation provide extensive technical details about implementation possibilities and security considerations.