webauthn-server-buildkit
v2.2.0
Published
A comprehensive WebAuthn server package for TypeScript that provides secure, type-safe, and framework-independent biometric authentication
Maintainers
Readme
WebAuthn Server Buildkit
📚 Documentation
- 🤖 AI Integration Guide - Complete integration with
capacitor-biometric-authenticationfrontend - API Reference - Detailed API documentation
- Storage Adapters - Database integration examples
- Troubleshooting Guide - Common issues and solutions
- Migration Guide - Upgrading between versions
- Changelog - Release history
- Portfolio Info File - Resume, portfolio, SEO, and social-content source material
- WebAuthn Specification - W3C WebAuthn standard
A comprehensive WebAuthn server package for TypeScript that provides secure, type-safe, and framework-independent biometric authentication.
Current State
- Package version:
2.2.0 - Verified on:
2026-05-26 - Tests:
166/166passing via thetestscript in the last verification pass - Build: the
buildscript succeeds (ESM + CommonJS + type declarations) - Toolchain: TypeScript 6, ESLint 10, Vitest 4
Features
🔐 Security & Compliance
- Full WebAuthn Level 3 Implementation - Complete server-side implementation following the latest W3C standards
- Secure by Default - Built-in AES-256-GCM session encryption, cryptographically secure challenge generation
- Algorithm Support - ES256/384/512, RS256/384/512, PS256/384/512 and Ed25519 (EdDSA), all verified via Node's native crypto
- Attestation -
noneandpackedself-attestation are cryptographically verified; certificate-chain formats are parsed and reported (not trust-anchored — see Attestation)
🛠️ Developer Experience
- Framework Independent - Works with Express, Fastify, Koa, Next.js, or any Node.js framework
- Full TypeScript Support - 100% type-safe with comprehensive type definitions and strict mode
- Simple API - Intuitive methods with sensible defaults, get started in minutes
- Extensive Configuration - Every WebAuthn option is configurable with user values taking priority
📦 Integration & Storage
- Storage Agnostic - Pluggable adapter system for any database (MongoDB, PostgreSQL, Redis, etc.)
- Session Management - Built-in secure session handling with token-based authentication
- Extension Support - Full support for WebAuthn extensions
- Modern Architecture - ES2022 features, Node.js
>=24.13.0support, ESM and CommonJS builds
Installation
yarn add webauthn-server-buildkitPackage Manager Policy
- Use
yarnfor project-local installs and script execution. - Use
pnpmfor global package installs. - Use
npmonly to install or updatepnpmglobally. - Keep
yarn.lockand remove other lockfiles.
Quick Start
import { WebAuthnServer, MemoryStorageAdapter } from 'webauthn-server-buildkit';
// Initialize the server
const webauthn = new WebAuthnServer({
rpName: 'My App',
rpID: 'localhost',
origin: 'http://localhost:3000',
encryptionSecret: 'your-32-character-or-longer-secret-key-here',
});
// Registration flow
async function handleRegistration(user: UserModel) {
// 1. Generate registration options
const { options, challenge } = await webauthn.createRegistrationOptions(user);
// 2. Send options to client
// ... client performs WebAuthn registration ...
// 3. Verify registration response
const { verified, registrationInfo } = await webauthn.verifyRegistration(
clientResponse,
challenge,
);
if (verified && registrationInfo) {
// Save credential to database
await saveCredential({
...registrationInfo.credential,
userId: user.id,
webAuthnUserID: options.user.id,
});
}
}
// Authentication flow
async function handleAuthentication(credentials: WebAuthnCredential[]) {
// 1. Generate authentication options
const { options, challenge } = await webauthn.createAuthenticationOptions({
allowCredentials: credentials,
});
// 2. Send options to client
// ... client performs WebAuthn authentication ...
// 3. Verify authentication response
const credential = credentials.find((c) => c.id === clientResponse.id);
const { verified, authenticationInfo } = await webauthn.verifyAuthentication(
clientResponse,
challenge,
credential,
);
if (verified && authenticationInfo) {
// Create session
const sessionToken = await webauthn.createSession(
credential.userId,
credential.id,
authenticationInfo.userVerified,
);
return sessionToken;
}
}Verified Package Architecture
src/registration/handles registration option generation and verification.src/authentication/handles authentication option generation and verification.src/session/handles encrypted session lifecycle operations.src/crypto/handles challenge generation, CBOR parsing, COSE operations, and verification helpers.src/adapters/contains the storage abstraction layer and memory adapter.tests/currently covers adapters, crypto, session, registration, and authentication paths.
Verification Commands
yarn test
yarn buildConfiguration
const webauthn = new WebAuthnServer({
// Required
rpName: 'My App', // Relying Party name
rpID: 'example.com', // Relying Party ID (domain)
origin: 'https://example.com', // Expected origin(s)
encryptionSecret: 'secret-key', // Min 32 chars for session encryption
// Optional
sessionDuration: 86400000, // Session duration in ms (default: 24h)
attestationType: 'none', // Attestation preference
enableMobileAttestation: false, // Opt-in to the non-standard mobile JSON path (default: false; see Attestation)
userVerification: 'preferred', // User verification requirement
authenticatorSelection: {
// Authenticator selection criteria
residentKey: 'preferred',
userVerification: 'preferred',
authenticatorAttachment: 'platform',
},
supportedAlgorithms: [-7, -257], // COSE algorithm identifiers
challengeSize: 32, // Challenge size in bytes
timeout: 60000, // Operation timeout in ms
preferredAuthenticatorType: 'localDevice', // Preferred authenticator
storageAdapter: customAdapter, // Custom storage adapter
debug: true, // Enable debug logging
logger: customLogger, // Custom logger function
});Storage Adapters
The package includes an in-memory storage adapter for development. For production, implement your own storage adapter:
import { StorageAdapter } from 'webauthn-server-buildkit';
class MySQLStorageAdapter implements StorageAdapter {
users = {
async findById(id: string | number) {
/* ... */
},
async findByUsername(username: string) {
/* ... */
},
async create(user: Omit<UserModel, 'id'>) {
/* ... */
},
async update(id: string | number, updates: Partial<UserModel>) {
/* ... */
},
async delete(id: string | number) {
/* ... */
},
};
credentials = {
async findById(id: Base64URLString) {
/* ... */
},
async findByUserId(userId: string | number) {
/* ... */
},
async findByWebAuthnUserId(webAuthnUserId: Base64URLString) {
/* ... */
},
async create(credential: Omit<WebAuthnCredential, 'createdAt'>) {
/* ... */
},
async updateCounter(id: Base64URLString, counter: number) {
/* ... */
},
async updateLastUsed(id: Base64URLString) {
/* ... */
},
async delete(id: Base64URLString) {
/* ... */
},
async deleteByUserId(userId: string | number) {
/* ... */
},
};
challenges = {
async create(challenge: ChallengeData) {
/* ... */
},
async find(challenge: string) {
/* ... */
},
async delete(challenge: string) {
/* ... */
},
async deleteExpired() {
/* ... */
},
};
sessions = {
async create(sessionId: string, data: SessionData) {
/* ... */
},
async find(sessionId: string) {
/* ... */
},
async update(sessionId: string, data: Partial<SessionData>) {
/* ... */
},
async delete(sessionId: string) {
/* ... */
},
async deleteExpired() {
/* ... */
},
async deleteByUserId(userId: string | number) {
/* ... */
},
};
}Session Management
Built-in secure session management with encrypted tokens:
// Create session after authentication
const token = await webauthn.createSession(
userId,
credentialId,
userVerified,
{ customData: 'value' }, // Optional additional data
);
// Validate session
const { valid, sessionData } = await webauthn.validateSession(token);
// Refresh session
const newToken = await webauthn.refreshSession(token);
// Revoke session
await webauthn.revokeSession(token);
// Revoke all user sessions
await webauthn.revokeUserSessions(userId);Express.js Example
import express from 'express';
import { WebAuthnServer } from 'webauthn-server-buildkit';
const app = express();
const webauthn = new WebAuthnServer({
rpName: 'My Express App',
rpID: 'localhost',
origin: 'http://localhost:3000',
encryptionSecret: process.env.ENCRYPTION_SECRET,
});
app.use(express.json());
// Registration endpoint
app.post('/api/register/options', async (req, res) => {
const user = req.user; // From your auth middleware
// getUserCredentials should return an array of previously registered credentials for this user
// This comes from your database where you stored credentials during registration
// Each credential object should contain:
// - id: The credential ID (credentialId from registrationInfo.credential)
// - publicKey: The public key bytes (publicKey from registrationInfo.credential)
// - counter: Usage counter for replay protection (counter from registrationInfo.credential)
// - transports: Array of transport methods like ['usb', 'nfc', 'ble', 'internal']
// (transports from registrationInfo.credential or authenticator response)
// You get all this data when you save the credential after successful registration
const credentials = await getUserCredentials(user.id);
const { options } = await webauthn.createRegistrationOptions(user, {
excludeCredentials: credentials,
});
req.session.challenge = options.challenge;
res.json(options);
});
app.post('/api/register/verify', async (req, res) => {
const challenge = req.session.challenge;
const { verified, registrationInfo } = await webauthn.verifyRegistration(req.body, challenge);
if (verified && registrationInfo) {
await saveCredential(registrationInfo.credential);
res.json({ verified: true });
} else {
res.status(400).json({ verified: false });
}
});
// Authentication endpoint
app.post('/api/authenticate/options', async (req, res) => {
const credentials = await getCredentialsByUsername(req.body.username);
const { options } = await webauthn.createAuthenticationOptions({
allowCredentials: credentials,
});
req.session.challenge = options.challenge;
res.json(options);
});
app.post('/api/authenticate/verify', async (req, res) => {
const challenge = req.session.challenge;
const credential = await getCredentialById(req.body.id);
const { verified, authenticationInfo } = await webauthn.verifyAuthentication(
req.body,
challenge,
credential,
);
if (verified && authenticationInfo) {
const token = await webauthn.createSession(
credential.userId,
credential.id,
authenticationInfo.userVerified,
);
res.json({ verified: true, token });
} else {
res.status(401).json({ verified: false });
}
});API Reference
WebAuthnServer
Constructor
new WebAuthnServer(config: WebAuthnServerConfig)Methods
Registration
createRegistrationOptions(user, params?): Generate registration options (params.excludeCredentials,params.authenticatorSelection,params.attestation, …)verifyRegistration(response, challenge, origin?): Verify registration response
Authentication
createAuthenticationOptions(params?): Generate authentication options (params.allowCredentials,params.userVerification,params.rpId, …)verifyAuthentication(response, challenge, credential, origin?): Verify authentication response
Session Management
createSession(userId, credentialId, userVerified, additionalData?): Create sessionvalidateSession(token): Validate session tokenrefreshSession(token): Refresh session tokenrevokeSession(token): Revoke sessionrevokeUserSessions(userId): Revoke all user sessions
Utilities
cleanup(): Clean up expired datagetStorageAdapter(): Get storage adapter instance
Supported Algorithms
- ES256 (ECDSA with SHA-256) - Default
- RS256 (RSASSA-PKCS1-v1_5 with SHA-256) - Default
- ES384 (ECDSA with SHA-384)
- ES512 (ECDSA with SHA-512)
- RS384 (RSASSA-PKCS1-v1_5 with SHA-384)
- RS512 (RSASSA-PKCS1-v1_5 with SHA-512)
- PS256 (RSASSA-PSS with SHA-256)
- PS384 (RSASSA-PSS with SHA-384)
- PS512 (RSASSA-PSS with SHA-512)
- EdDSA (Ed25519)
All algorithms are verified through Node's native crypto — COSE public keys are imported via JWK, so RSA (any modulus size) and P-521 (ES512) work correctly. Use supportedAlgorithms to restrict which algorithms a credential may register with (registration rejects credentials outside the list).
Attestation
What is verified
| Format | Behaviour |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| none | Accepted (no statement to verify). attestationVerified is false. |
| packed (self-attestation) | The signature over authData \|\| clientDataHash is verified with the credential public key. attestationVerified is true. |
| packed (x5c), fido-u2f, tpm, android-key, android-safetynet, apple | Parsed and reported, but the certificate chain is not trust-anchored in this release. attestationVerified is false — verify the chain yourself if you require hardware-attestation trust. |
verifyRegistration returns registrationInfo.fmt and registrationInfo.attestationVerified so you can branch on the outcome.
Mobile attestation (opt-in)
Some native clients (for example the companion capacitor-biometric-authentication package) send a non-standard JSON payload instead of a CBOR WebAuthn attestation. This path is disabled by default (enableMobileAttestation: false) because it trusts a client-supplied { publicKey, credentialId } object without verifying a challenge signature — it provides no cryptographic attestation guarantee. With it disabled, every registration is handled by the standard WebAuthn path, so the path cannot be used to bypass verification.
Enable it only if you fully control the client and transport and enforce freshness/authenticity by other means:
const webauthn = new WebAuthnServer({
rpName: 'My App',
rpID: 'example.com',
origin: [
'https://example.com',
'ios-app://com.example.app', // iOS app bundle identifier
'android-app://com.example.app', // Android app package name
],
encryptionSecret: process.env.ENCRYPTION_SECRET,
enableMobileAttestation: true, // ⚠️ no cryptographic attestation — see above
});When enabled, a warning is logged each time a credential is accepted via the mobile path.
Security Considerations
- Encryption Secret: Use a strong, unique secret of at least 32 characters
- HTTPS Required: Always use HTTPS in production for WebAuthn
- Origin Validation: The package validates origins to prevent phishing
- Counter Tracking: Authenticator counters are tracked to detect cloned credentials
- Session Security: Session tokens are encrypted with AES-256-GCM; keys are derived with HKDF-SHA256 and a 12-byte IV. Upgrading from 2.1.x invalidates existing tokens (users re-authenticate once)
- Mobile Attestation: The mobile JSON path is opt-in and provides NO cryptographic attestation — keep it disabled unless you understand the trade-off (see Attestation)
Error Handling
The package exports typed error classes:
import {
WebAuthnError,
RegistrationError,
AuthenticationError,
VerificationError,
ConfigurationError,
StorageError,
SessionError,
} from 'webauthn-server-buildkit';
try {
await webauthn.verifyRegistration(response, challenge);
} catch (error) {
if (error instanceof RegistrationError) {
console.error('Registration failed:', error.code, error.message);
}
}All error classes extend WebAuthnError (exposing code and statusCode) and are exported as runtime values, so instanceof checks work. The COSEAlgorithmIdentifier, COSEKeyType, and COSEEllipticCurve enums are likewise value exports.
Requirements
- Node.js 24.13.0 or higher (see
enginesinpackage.json) - TypeScript 5.5+ recommended (the package is built and type-checked with TypeScript 6)
Frontend Package
For frontend biometric authentication, use the companion package capacitor-biometric-authentication which works with React, Vue, Angular, or vanilla JavaScript.
License
MIT
👨💻 Author
Ahsan Mahmood
- Website: https://aoneahsan.com
- GitHub: @aoneahsan
- Email: [email protected]
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
