Learn how to add secure biometric login to your web app with our easy step-by-step guide. Enhance user security today!

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
The Rise of Biometric Authentication
Biometric authentication has evolved from sci-fi fantasy to everyday reality. Your users already unlock their phones with their fingerprints and faces—why not bring that same convenience to your web application? Beyond the cool factor, biometrics offers a compelling blend of security and usability that traditional passwords simply can't match.
The good news is that implementing biometric authentication no longer requires deep platform-specific knowledge. The Web Authentication API (WebAuthn) provides a standardized way to implement biometric login across browsers and devices. It's part of the FIDO2 specifications and is supported by all major browsers including Chrome, Firefox, Safari, and Edge.
How Biometric Authentication Actually Works
Before diving into code, let's understand what happens behind the scenes:
This approach keeps sensitive biometric data on the user's device while giving your server cryptographic assurance that authentication succeeded.
Step 1: Set Up Your Backend
You'll need server-side components to handle registration and verification. Here's a Node.js example using Express and the @simplewebauthn/server library:
// Install dependencies:
// npm install express @simplewebauthn/server base64url
const express = require('express');
const { generateRegistrationOptions, verifyRegistrationResponse,
generateAuthenticationOptions, verifyAuthenticationResponse } = require('@simplewebauthn/server');
const base64url = require('base64url');
const app = express();
app.use(express.json());
// In-memory storage (use a database in production)
const userDB = {};
// Random ID generator helper
const generateRandomID = () => base64url(Buffer.from(Array(32).fill(0).map(() => Math.floor(Math.random() * 256))));
// Your domain - MUST match your site's origin
const rpID = 'example.com';
const rpName = 'Your App Name';
const origin = `https://${rpID}`;
Step 2: Registration Endpoint - Let Users Register Their Biometrics
app.post('/api/register/options', (req, res) => {
const { username } = req.body;
// Create a new user or get existing one
const user = userDB[username] || {
id: generateRandomID(),
username,
devices: [],
currentChallenge: null
};
// Save user if new
if (!userDB[username]) userDB[username] = user;
// Generate registration options
const options = generateRegistrationOptions({
rpName,
rpID,
userID: user.id,
userName: username,
// Prevent users from re-registering the same device
excludeCredentials: user.devices.map(device => ({
id: device.credentialID,
type: 'public-key',
transports: device.transports || ['internal']
})),
authenticatorSelection: {
// Require biometrics when available
userVerification: 'preferred',
// Prefer platform authenticators (like TouchID, FaceID, Windows Hello)
authenticatorAttachment: 'platform'
}
});
// Save the challenge for verification later
user.currentChallenge = options.challenge;
res.json(options);
});
app.post('/api/register/verify', async (req, res) => {
const { username, attestationResponse } = req.body;
const user = userDB[username];
if (!user) {
return res.status(400).json({ error: 'User not found' });
}
try {
// Verify the attestation response
const verification = await verifyRegistrationResponse({
credential: attestationResponse,
expectedChallenge: user.currentChallenge,
expectedOrigin: origin,
expectedRPID: rpID
});
if (verification.verified) {
// Add the new device to user's devices
const { credentialID, credentialPublicKey } = verification.registrationInfo;
user.devices.push({
credentialID,
credentialPublicKey,
transports: attestationResponse.transports || ['internal'],
registered: new Date().toISOString()
});
return res.json({ success: true });
}
res.status(400).json({ error: 'Verification failed' });
} catch (error) {
console.error(error);
res.status(500).json({ error: error.message });
}
});
Step 3: Authentication Endpoints - Let Users Log In With Biometrics
app.post('/api/login/options', (req, res) => {
const { username } = req.body;
const user = userDB[username];
if (!user || user.devices.length === 0) {
return res.status(400).json({ error: 'User not registered with biometrics' });
}
// Generate authentication options
const options = generateAuthenticationOptions({
rpID,
// Only allow credentials registered to this user
allowCredentials: user.devices.map(device => ({
id: device.credentialID,
type: 'public-key',
transports: device.transports || ['internal']
})),
userVerification: 'preferred'
});
// Save challenge for verification
user.currentChallenge = options.challenge;
res.json(options);
});
app.post('/api/login/verify', async (req, res) => {
const { username, assertionResponse } = req.body;
const user = userDB[username];
if (!user) {
return res.status(400).json({ error: 'User not found' });
}
// Find the authenticator that was used
const matchingDevice = user.devices.find(device =>
device.credentialID === assertionResponse.id);
if (!matchingDevice) {
return res.status(400).json({ error: 'Authenticator not registered for this user' });
}
try {
// Verify the authentication response
const verification = await verifyAuthenticationResponse({
credential: assertionResponse,
expectedChallenge: user.currentChallenge,
expectedOrigin: origin,
expectedRPID: rpID,
authenticator: {
credentialPublicKey: matchingDevice.credentialPublicKey,
credentialID: matchingDevice.credentialID,
counter: matchingDevice.counter || 0
}
});
if (verification.verified) {
// Update the authenticator's counter
matchingDevice.counter = verification.authenticationInfo.newCounter;
// Here you'd typically create a session or JWT token
return res.json({
success: true,
token: 'your-auth-token-here' // Generate real token in production
});
}
res.status(400).json({ error: 'Verification failed' });
} catch (error) {
console.error(error);
res.status(500).json({ error: error.message });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
Step 4: Frontend Implementation
Now let's implement the client-side code using the @simplewebauthn/browser library:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Biometric Login Demo</title>
<script src="https://cdn.jsdelivr.net/npm/@simplewebauthn/browser/dist/bundle/index.umd.min.js"></script>
</head>
<body>
<h1>Biometric Authentication Demo</h1>
<div id="register-section">
<h2>Register</h2>
<input type="text" id="register-username" placeholder="Username">
<button id="register-button">Register with Biometrics</button>
<div id="register-status"></div>
</div>
<div id="login-section">
<h2>Login</h2>
<input type="text" id="login-username" placeholder="Username">
<button id="login-button">Login with Biometrics</button>
<div id="login-status"></div>
</div>
<script>
// Registration process
document.getElementById('register-button').addEventListener('click', async () => {
const username = document.getElementById('register-username').value;
const statusEl = document.getElementById('register-status');
if (!username) {
statusEl.textContent = 'Please enter a username';
return;
}
try {
statusEl.textContent = 'Starting registration...';
// 1. Get registration options from server
const optionsRes = await fetch('/api/register/options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
});
const options = await optionsRes.json();
if (!optionsRes.ok) {
throw new Error(options.error || 'Failed to get registration options');
}
// 2. Pass options to browser's WebAuthn API
statusEl.textContent = 'Please follow the instructions to authenticate...';
const attestation = await SimpleWebAuthnBrowser.startRegistration(options);
// 3. Send response to server for verification
const verificationRes = await fetch('/api/register/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, attestationResponse: attestation })
});
const verificationResult = await verificationRes.json();
if (!verificationRes.ok || !verificationResult.success) {
throw new Error(verificationResult.error || 'Registration failed');
}
statusEl.textContent = 'Registration successful! You can now log in with biometrics.';
} catch (error) {
console.error(error);
statusEl.textContent = `Error: ${error.message}`;
}
});
// Login process
document.getElementById('login-button').addEventListener('click', async () => {
const username = document.getElementById('login-username').value;
const statusEl = document.getElementById('login-status');
if (!username) {
statusEl.textContent = 'Please enter a username';
return;
}
try {
statusEl.textContent = 'Starting login...';
// 1. Get authentication options from server
const optionsRes = await fetch('/api/login/options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
});
const options = await optionsRes.json();
if (!optionsRes.ok) {
throw new Error(options.error || 'Failed to get login options');
}
// 2. Pass options to browser's WebAuthn API
statusEl.textContent = 'Please follow the instructions to authenticate...';
const assertion = await SimpleWebAuthnBrowser.startAuthentication(options);
// 3. Send response to server for verification
const verificationRes = await fetch('/api/login/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, assertionResponse: assertion })
});
const verificationResult = await verificationRes.json();
if (!verificationRes.ok || !verificationResult.success) {
throw new Error(verificationResult.error || 'Authentication failed');
}
// Store token for authenticated session
localStorage.setItem('authToken', verificationResult.token);
statusEl.textContent = 'Login successful!';
} catch (error) {
console.error(error);
statusEl.textContent = `Error: ${error.message}`;
}
});
</script>
</body>
</html>
Graceful Degradation
Not all devices support biometrics. Implement feature detection and provide password fallback:
// Check if WebAuthn is supported
if (!window.PublicKeyCredential) {
// WebAuthn not supported - hide biometric options and show password login
document.getElementById('biometric-section').style.display = 'none';
document.getElementById('password-section').style.display = 'block';
} else {
// Check if the device has biometric capability
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
.then(available => {
if (!available) {
// Platform cannot verify users biometrically
document.getElementById('biometric-warning').textContent =
'Your device doesn\'t support biometric authentication. You can still register a security key.';
}
});
}
Security & Storage Considerations
Account Recovery
Always provide recovery options for users who:
This typically means providing email-based recovery links or backup codes at registration time.
Performance Impact
Biometric authentication generally completes faster than typing a password, though the initial prompt might feel like an extra step. To mitigate this:
The Business Case for Biometrics
Besides the security benefits, here's how biometrics can positively impact your metrics:
Start Small
Consider these implementation strategies:
Platform-Specific Optimizations
Different platforms have different biometric capabilities:
The WebAuthn API abstracts these differences, but testing across platforms remains important.
Once implemented, track these metrics:
Implementing biometric authentication isn't just a technical upgrade—it's a user experience transformation. As passwords increasingly become a liability rather than a protection, biometrics offer a path toward more secure, convenient authentication.
The WebAuthn standard has finally made cross-platform biometric authentication practical for web applications. With the implementation approach outlined here, you can bring this technology to your users today, positioning your application at the forefront of modern authentication practices.
Remember: the goal isn't just to replace passwords with something more secure, but to make security so seamless that it enhances rather than hinders the user experience.
Explore the top 3 practical biometric login use cases to enhance your web app’s security and user experience.
A security layer that uses unique physical traits to authorize sensitive banking operations and payments, dramatically reducing fraud while eliminating the friction of traditional authentication methods.
A privacy-preserving authorization system allowing medical professionals and patients to access sensitive health information through inherent biological identifiers rather than forgettable credentials.
A frictionless access system that enables employees to authenticate across physical spaces and digital systems using the same biometric identifier, eliminating credential juggling.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â