FIDO2 is the latest set of specifications from the FIDO Alliance, aiming to enable passwordless authentication. It comprises two main components:
Key Benefits of FIDO2:
Before diving into the implementation, let's understand why FIDO2 is worth your time:
✅ No More Password Headaches
✅ Superior Security
✅ Better User Experience
Here's what we'll build:
// Required packages for Node.js
npm install fido2-lib express body-parser
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ Browser │ ←──► │ Server │ ←──► │ Database │
│ (WebAuthn) │ │ (FIDO2Lib) │ │ │
│ │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
First, let's set up our Express server with FIDO2 capabilities:
const express = require('express');
const { Fido2Lib } = require('fido2-lib');
const app = express();
// Initialize FIDO2
const f2l = new Fido2Lib({
timeout: 60000,
rpId: "example.com",
rpName: "FIDO Example App",
challengeSize: 32,
attestation: "none"
});
app.use(express.json());
Create an endpoint to start the registration process:
app.post('/auth/register-begin', async (req, res) => {
try {
const user = {
id: crypto.randomBytes(32),
name: req.body.username,
displayName: req.body.displayName
};
const registrationOptions = await f2l.attestationOptions();
// Add user info to the options
registrationOptions.user = user;
registrationOptions.challenge = Buffer.from(registrationOptions.challenge);
// Store challenge for verification
req.session.challenge = registrationOptions.challenge;
req.session.username = user.name;
res.json(registrationOptions);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Here's the frontend JavaScript to handle registration:
async function registerUser() {
// 1. Get registration options from server
const response = await fetch('/auth/register-begin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: '[email protected]' })
});
const options = await response.json();
// 2. Create credentials using WebAuthn
const credential = await navigator.credentials.create({
publicKey: {
...options,
challenge: base64ToBuffer(options.challenge),
user: {
...options.user,
id: base64ToBuffer(options.user.id)
}
}
});
// 3. Send credentials to server
await fetch('/auth/register-complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: credential.id,
rawId: bufferToBase64(credential.rawId),
response: {
attestationObject: bufferToBase64(
credential.response.attestationObject
),
clientDataJSON: bufferToBase64(
credential.response.clientDataJSON
)
}
})
});
}
// Helper functions
function bufferToBase64(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}
function base64ToBuffer(base64) {
return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
}
Server-side authentication endpoint:
app.post('/auth/login-begin', async (req, res) => {
try {
const assertionOptions = await f2l.assertionOptions();
// Get user's registered credentials from database
const user = await db.getUser(req.body.username);
assertionOptions.allowCredentials = user.credentials.map(cred => ({
id: cred.credentialId,
type: 'public-key'
}));
req.session.challenge = assertionOptions.challenge;
req.session.username = req.body.username;
res.json(assertionOptions);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Client-side authentication:
async function loginUser() {
// 1. Get authentication options
const response = await fetch('/auth/login-begin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: '[email protected]' })
});
const options = await response.json();
// 2. Get assertion from authenticator
const assertion = await navigator.credentials.get({
publicKey: {
...options,
challenge: base64ToBuffer(options.challenge),
allowCredentials: options.allowCredentials.map(cred => ({
...cred,
id: base64ToBuffer(cred.id)
}))
}
});
// 3. Verify with server
await fetch('/auth/login-complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: assertion.id,
rawId: bufferToBase64(assertion.rawId),
response: {
authenticatorData: bufferToBase64(
assertion.response.authenticatorData
),
clientDataJSON: bufferToBase64(
assertion.response.clientDataJSON
),
signature: bufferToBase64(
assertion.response.signature
)
}
})
});
}
// Check if WebAuthn is supported
if (!window.PublicKeyCredential) {
console.log('WebAuthn not supported');
// Fall back to traditional authentication
return;
}
// Check if user verifying platform authenticator is available
const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
if (!available) {
console.log('Platform authenticator not available');
// Consider security key instead
}
// Client-side error handling
try {
const credential = await navigator.credentials.create({/*...*/});
} catch (error) {
switch (error.name) {
case 'NotAllowedError':
console.log('User declined to create credential');
break;
case 'SecurityError':
console.log('Origin not secure');
break;
default:
console.error('Unknown error:', error);
}
}
function base64UrlEncode(buffer) {
const base64 = bufferToBase64(buffer);
return base64.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
describe('FIDO2 Authentication', () => {
it('should generate registration options', async () => {
const response = await fetch('/auth/register-begin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: '[email protected]' })
});
const options = await response.json();
expect(options).toHaveProperty('challenge');
expect(options).toHaveProperty('rp');
expect(options.rp.name).toBe('FIDO Example App');
});
});
// Using Chrome's Virtual Authenticator Environment
const virtualAuthenticatorOptions = {
protocol: 'ctap2',
transport: 'internal',
hasResidentKey: true,
hasUserVerification: true,
isUserConsenting: true
};
const authenticator = await driver.addVirtualAuthenticator(
virtualAuthenticatorOptions
);
if (window.location.protocol !== 'https:') {
throw new Error('FIDO2 requires HTTPS');
}
const expectedOrigin = 'https://example.com';
const clientDataJSON = JSON.parse(
new TextDecoder().decode(credential.response.clientDataJSON)
);
if (clientDataJSON.origin !== expectedOrigin) {
throw new Error('Invalid origin');
}
if (!timingSafeEqual(
storedChallenge,
credential.response.challenge
)) {
throw new Error('Challenge mismatch');
}
✅ HTTPS configured
✅ Error handling implemented
✅ Browser support detection
✅ Backup authentication method
✅ Rate limiting enabled
✅ Logging system in place
✅ Security headers configured
Resources:
Need help? Join Discord community for support.
*** This is a Security Bloggers Network syndicated blog from Meet the Tech Entrepreneur, Cybersecurity Author, and Researcher authored by Deepak Gupta - Tech Entrepreneur, Cybersecurity Author. Read the original post at: https://guptadeepak.com/implementing-fido2-authentication-a-developers-step-by-step-guide/