webauthn-server-buildkit
v2.3.2
Published
Framework-independent WebAuthn server for Node.js — passkeys, attestation, encrypted sessions.
Maintainers
Readme
Docs · npm · GitHub · Changelog · AI Guide · Support
[!IMPORTANT] This package requires Node.js
>=24.13.0and ships no automated test suite — see Limitations before adopting it for a security-critical deployment.
A server-side library for the WebAuthn registration and authentication ceremonies: it generates options, verifies the authenticator's response, and issues an encrypted session token. It is transport- and framework-agnostic — you own the HTTP layer and the database, the library owns the cryptography. Attestation is verified per format, and the result distinguishes cryptographically valid from trust-anchored instead of collapsing the two.
| | |
|---|---|
| Version | 2.3.2 |
| License | MIT |
| Node | >=24.13.0 |
| Platforms | Node.js server only (not a browser or React Native library) |
| Install size | ~149 kB packed · ~633 kB unpacked |
| Types | Bundled .d.ts + .d.cts (ESM + CJS) |
| Runtime deps | 1 (cbor-x, bundled) |
| Status | Stable · actively maintained · no automated tests |
🧭 Table of Contents #
- 💡 Why webauthn-server-buildkit
- ✨ Features
- 📋 Requirements
- 📦 Installation
- 🚀 Quick Start
- 🛠️ Usage
- ⚙️ Configuration
- 🔧 API Reference
- 🧩 Types
- 🧪 Examples
- 🎛️ Advanced Features
- 🚑 Recovery & Troubleshooting
- 🚧 Limitations
- 📚 Documentation
- 🔄 Changelog
- 🤝 Contributing
- 💬 Support
- 📄 License
- 👤 Author
- 🔗 Links
- 🏷️ Keywords
💡 Why webauthn-server-buildkit #
WebAuthn puts the hard parts on the server. The browser hands you a blob; you have to decode CBOR, parse authenticator data, check an RP-ID hash, validate flags, verify a COSE signature, and decide whether an attestation statement means anything. Getting any of it subtly wrong produces a login that works perfectly and protects nothing.
This library does that verification and hands back a typed result. It deliberately stops at the boundary of your application: it never opens a socket, never touches your database directly, and never fetches metadata over the network.
| | webauthn-server-buildkit | A hand-rolled implementation |
|---|---|---|
| Ceremony verification | Challenge, origin, RP-ID hash, flags, counter, signature | Each one is yours to get right |
| Attestation | Seven formats verified; trust-anchoring reported separately | Usually fmt: 'none' and a hope |
| Sessions | AES-256-GCM tokens with HKDF-SHA256 derivation included | Bring your own |
| Storage | Any database via one adapter interface | Coupled to whatever you picked |
| Network calls | None — trust anchors are supplied by you | — |
Not the right tool when — you need a hosted identity provider (use Auth0, Clerk or Stytch); you want the
browser-side navigator.credentials calls (this is server-only — pair it with a client library); you need
FIDO MDS blobs fetched and refreshed for you (you supply them); you are on Node older than 24.13; or your
compliance process requires a vendored test suite in the dependency itself (there is none — see
Limitations).
✨ Features #
- Full ceremony verification — challenge, origin, RP-ID hash, user-presence and user-verification flags, signature counter, and the assertion signature, for both registration and authentication.
- Ten signature algorithms — ES256/384/512, RS256/384/512, PS256/384/512 and Ed25519, all through Node's native
crypto. The algorithm a credential registered with is pinned and re-checked at every assertion. - Per-format attestation —
none,packed(self and x5c),fido-u2f,android-key,tpm,android-safetynetandappleare each cryptographically verified, with trust-anchoring reported as a separate fact. - Single-use challenges — with a storage adapter configured, a challenge is looked up, checked for expiry and operation scope, and consumed so it cannot be replayed.
- Encrypted sessions — AES-256-GCM tokens with HKDF-SHA256 key derivation, a 12-byte IV, and field-length validation before decryption.
- Storage-agnostic — one adapter interface covers users, credentials, challenges and sessions; an in-memory adapter is included for development.
- Framework-independent — Express, Fastify, Koa, Next.js, Hono or a raw
httpserver; the library never sees your request object. - Offline by design — no network calls, ever. Trust anchors come from you.
- Dual ESM + CJS with separate type declarations for each, and one bundled runtime dependency.
📋 Requirements #
| Requirement | Version | Why |
|---|---|---|
| Node.js | >=24.13.0 | Native crypto APIs used for Ed25519 and COSE-to-JWK key import. This floor excludes Node 20 and 22 LTS — check it before adopting. |
| TypeScript | >=5.5 recommended | Only for consumers using the bundled types; built and type-checked with TypeScript 6. |
| A storage adapter | — | Optional for a first run (an in-memory one is included), required in production and required for single-use challenge enforcement. |
📦 Installation #
yarn add webauthn-server-buildkitNo post-install step, no native build, no configuration file. Provide an encryptionSecret of at least 32
characters and you are ready — see Quick Start.
🚀 Quick Start #
import { WebAuthnServer, MemoryStorageAdapter } from 'webauthn-server-buildkit';
const webauthn = new WebAuthnServer({
rpName: 'My App',
rpID: 'localhost',
origin: 'http://localhost:3000',
encryptionSecret: process.env.ENCRYPTION_SECRET!, // 32+ characters
storageAdapter: new MemoryStorageAdapter(), // development only
});
// 1. Server: create the registration options and send them to the browser.
const { options, challenge, webAuthnUserId } = await webauthn.createRegistrationOptions({
id: 1,
username: '[email protected]',
});
// 2. Browser: navigator.credentials.create({ publicKey: options }) -> `response`.
// 3. Server: verify what came back.
const { verified, registrationInfo } = await webauthn.verifyRegistration(response, challenge);
if (verified && registrationInfo) {
await saveCredential({
id: registrationInfo.credential.id,
publicKey: registrationInfo.credential.publicKey,
counter: registrationInfo.credential.counter,
alg: registrationInfo.credential.alg, // persist to enable algorithm pinning
webAuthnUserID: webAuthnUserId, // persist to enable usernameless login
userId: 1,
});
}Store alg and webAuthnUserID — two later protections read them back, and a credential saved without them
silently loses those checks.
🛠️ Usage #
Authenticating
const { options, challenge } = await webauthn.createAuthenticationOptions({
allowCredentials: await getCredentialsForUser(userId),
});
// ... browser runs navigator.credentials.get({ publicKey: options }) ...
const credential = await getCredentialById(response.id);
const { verified, authenticationInfo } = await webauthn.verifyAuthentication(
response,
challenge,
credential,
);authenticationInfo carries advisory signals worth acting on: backupStateChanged (possible credential
migration or clone), userVerificationDowngraded (registered with user verification, asserted without), and
counterSupported: false (the authenticator does not increment its counter, so clone detection is inert —
common for platform passkeys). Full guide:
Authentication.
Issuing a session
const token = await webauthn.createSession(userId, credentialId, authenticationInfo.userVerified);
const { valid, sessionData } = await webauthn.validateSession(token);
await webauthn.revokeSession(token);Tokens are self-contained and encrypted; revokeSession needs a storage adapter to have anything to revoke.
Full guide: Sessions.
Plugging in a database
Implement StorageAdapter — four namespaces (users, credentials, challenges, sessions) of plain
async methods, with no ORM assumptions. Worked MongoDB, PostgreSQL and Redis adapters:
Storage adapters.
⚙️ Configuration #
| Option | Type | Default | What it does |
|---|---|---|---|
| rpName | string | — | Required. Human-readable relying-party name. |
| rpID | string | — | Required. The domain, with no scheme or port. |
| origin | string \| string[] | — | Required. Expected clientData.origin value(s). |
| encryptionSecret | string | — | Required. 32+ characters, used to derive the session key. |
| storageAdapter | StorageAdapter | — | Persistence. Without it, challenge enforcement and revocation are unavailable. |
| sessionDuration | number | 86400000 | Session lifetime in milliseconds. |
| attestationType | 'none' \| 'indirect' \| 'direct' \| 'enterprise' | 'none' | What to request from the authenticator. |
| userVerification | 'required' \| 'preferred' \| 'discouraged' | 'preferred' | User-verification requirement. |
| supportedAlgorithms | COSEAlgorithmIdentifier[] | [-7, -257] | Allowlist, enforced at registration and authentication. |
| challengeSize | number | 32 | Challenge length in bytes. |
| timeout | number | 60000 | Ceremony timeout in milliseconds. |
| enforceChallengeStore | boolean | true | Require, scope-check and consume the challenge from storage. Only applies when a storageAdapter is set. |
| allowCrossOriginIframe | boolean | false | When false, a ceremony with clientData.crossOrigin === true is rejected. |
| originVerifier | (origin: string) => boolean | — | Replaces the default exact-string origin match. |
| metadataService | MetadataService | — | Supplies attestation trust anchors. See Advanced Features. |
| requireTrustedAttestation | boolean | false | Reject attestation that cannot be trust-anchored. |
| enableMobileAttestation | boolean | false | Opt into a non-standard JSON path that performs no cryptographic attestation. |
| debug / logger | boolean / fn | false / — | Diagnostics. |
Every option in detail: Configuration reference.
🔧 API Reference #
| Export | Signature | Docs |
|---|---|---|
| WebAuthnServer | new (config: WebAuthnServerConfig) | → |
| .createRegistrationOptions | (user, params?) => Promise<{ options, challenge, webAuthnUserId }> | → |
| .verifyRegistration | (response, challenge, origin?, opts?) => Promise<{ verified, registrationInfo? }> | → |
| .createAuthenticationOptions | (params?) => Promise<{ options, challenge }> | → |
| .verifyAuthentication | (response, challenge, credential, origin?) => Promise<{ verified, authenticationInfo? }> | → |
| .createSession | (userId, credentialId, userVerified, extra?) => Promise<string> | → |
| .validateSession | (token) => Promise<{ valid, sessionData? }> | → |
| .refreshSession · .revokeSession · .revokeUserSessions | session lifecycle | → |
| .cleanup · .getStorageAdapter | maintenance helpers | → |
| MemoryStorageAdapter | new () => StorageAdapter | → |
| StaticMetadataService | new (opts: StaticMetadataServiceOptions) | → |
| APPLE_WEBAUTHN_ROOT_CA_PEM | string | → |
| verify*Attestation (7 formats) | per-format verifiers | → |
| X.509 / ASN.1 / COSE / CBOR helpers | low-level building blocks | → |
| Error classes · VERSION · SUPPORT_CONFIG | runtime values | → |
🧩 Types #
The types a consumer actually touches:
import type {
WebAuthnServerConfig,
StorageAdapter,
UserModel,
WebAuthnCredential,
SessionData,
VerifiedRegistrationInfo,
VerifiedAuthenticationInfo,
MetadataService,
WebAuthnErrorCode,
} from 'webauthn-server-buildkit';WebAuthnErrorCode is an open union, so a future or custom code still compiles. Every exported type:
Types and errors.
🧪 Examples #
| Goal | Example | |---|---| | Minimal end-to-end script | basic | | Express routes for both ceremonies | express | | MongoDB · PostgreSQL · Redis adapters | storage-adapters |
🎛️ Advanced Features #
- Attestation with honest trust-anchoring — a statement can be cryptographically valid yet chain to no
root you trust.
attestationVerifiedistrueonly when it is both verified and anchored; the two are never conflated.appleanchors against the bundled Apple WebAuthn Root CA with no extra setup; every other certificate-chain format needs roots you supply. Per-format matrix and its stated gaps: Attestation guide. - Supplying trust anchors —
StaticMetadataServicetakes a fixed set of roots, keyed by AAGUID or attestation-cert key id. The package never fetches or bundles FIDO MDS data; you own that feed. API. - Usernameless / discoverable credentials — request a resident key, persist the returned
webAuthnUserId, and look the credential up by the asserteduserHandle, which is then cross-checked (USER_HANDLE_MISMATCH). Registration guide. - The security model in full — what is enforced, what is advisory, and what is delegated to you: Security model.
- Mobile JSON path (opt-in, unverified) —
enableMobileAttestationaccepts a client-supplied{ publicKey, credentialId }without any challenge signature. It is off by default and provides no cryptographic guarantee. Enable it only if you control the client and enforce freshness elsewhere.
🚑 Recovery & Troubleshooting #
| Symptom | Cause | Fix |
|---|---|---|
| CHALLENGE_NOT_FOUND on a request that just worked | The challenge was consumed — it is single-use — or was never persisted | Issue challenges through createRegistrationOptions / createAuthenticationOptions, which store them; do not reuse one |
| CHALLENGE_EXPIRED | Ceremony took longer than timeout | Raise timeout, or re-issue options |
| CROSS_ORIGIN_NOT_ALLOWED | The ceremony ran inside a cross-origin iframe | Move it to a top-level document, or set allowCrossOriginIframe: true deliberately |
| ALGORITHM_MISMATCH | The assertion used an algorithm other than the one registered | Expected — reject it. If it fires for every user, you stored the wrong alg at registration |
| USER_HANDLE_MISMATCH | Asserted userHandle differs from the stored webAuthnUserID | Persist webAuthnUserId from createRegistrationOptions onto the credential |
| ATTESTATION_NOT_TRUSTED | requireTrustedAttestation: true but no root matched | Supply the authenticator's roots via metadataService; only apple is anchored out of the box |
| CREDENTIAL_ALREADY_REGISTERED | The authenticator re-registered an existing credential | Expected — pass existingCredentialIds to enforce it, and surface it to the user |
| Sessions all invalid after upgrading from 2.1.x | Session-token format changed | One-time re-authentication; there is no migration path for old tokens |
| attestationVerified: false on a valid-looking statement | Verified but not trust-anchored | Not a failure. Supply roots, or accept it — see Attestation |
Every code and its HTTP status: Error handling.
🚧 Limitations #
Stated plainly, because this is security code.
- No automated test suite ships with this package, and none is present in the repository. A Vitest suite of 311 tests existed at the 2.3.0 release; the whole testing infrastructure was removed on 2026-06-02. CI runs lint, typecheck and build only. Verification of this library today rests on code review, not on a regression suite — weigh that against your own risk tolerance.
- No certificate revocation checking. There is no CRL or OCSP lookup; the package is offline by design. A revoked-but-unexpired attestation certificate still verifies.
- Trust anchors are yours to supply. Only the Apple WebAuthn Root CA is bundled. Every other
certificate-chain format reports
attestationVerified: falseuntil you provide roots — correct behaviour, but it means out-of-the-box attestation is meaningful for Apple authenticators only. - TPM attestation does not assert the AIK SAN fields (manufacturer, model, version). The
security-critical bindings — key match,
extraDatabinding, AIK signature, AIK EKU, chain — are checked. The SAN is exposed viatrustPathfor you to inspect. android-keydoes not enforce the Keymaster authorization list (purpose,origin, absence ofallApplications). Challenge binding, leaf-key match and hardware-root chaining are enforced.android-safetynetis deprecated by Google and no root for it is bundled; new authenticators do not emit it. Prefer Play Integrity where you control the client.- The mobile JSON path performs no attestation at all. It is opt-in and off by default; nothing about it is a WebAuthn guarantee.
- Server-side only. No browser, React Native or Deno build. Node
>=24.13.0excludes the 20 and 22 LTS lines. - Counter-based clone detection is inert for most platform passkeys, which report a counter of
0forever.counterSupported: falsetells you when that is the case; it is not a defect in the library. - Rate limiting, account lockout, credential-per-user caps and audit logging are yours. The library verifies a ceremony; it does not implement an authentication policy.
📚 Documentation #
| Document | Read it when | |---|---| | Introduction | deciding whether this library fits | | Installation · Quick start | first time using the package | | Registration · Authentication | implementing either ceremony | | Security model | you need to know what is enforced versus delegated | | Attestation | deciding whether to require attestation, and supplying roots | | Storage adapters | wiring a real database | | Sessions · Error handling | issuing tokens, or branching on a failure | | API reference | you need an exact signature | | Migration guide | upgrading across a major version | | Security policy | you found a vulnerability — do not open a public issue | | AI integration guide | a coding agent is implementing against it |
🔄 Changelog #
Latest release: 2.3.2 — documentation only: the at-a-glance table above reported the previous version, because it is a static duplicate of package.json. Full history in the changelog.
🤝 Contributing #
Fork and open a pull request — see
CONTRIBUTING.md for setup,
standards, and how to request collaborator access. main is protected: every change lands through a
reviewed PR.
Security vulnerabilities go to [email protected], not a public issue — see SECURITY.md.
💬 Support #
Questions and bugs: open an issue.
If this package saves you time, you can support its maintenance at aoneahsan.com/payment.
📄 License #
MIT © Ahsan Mahmood — see LICENSE.
👤 Author #
Ahsan Mahmood — aoneahsan.com · GitHub · LinkedIn · [email protected]
🔗 Links #
| | | |---|---| | Documentation | https://webauthn-server-buildkit-docs.aoneahsan.com | | npm | https://www.npmjs.com/package/webauthn-server-buildkit | | Repository | https://github.com/aoneahsan/webauthn-server-buildkit | | Issues | https://github.com/aoneahsan/webauthn-server-buildkit/issues | | Changelog | https://github.com/aoneahsan/webauthn-server-buildkit/blob/main/CHANGELOG.md | | Security policy | https://github.com/aoneahsan/webauthn-server-buildkit/blob/main/SECURITY.md | | Companion client package | https://www.npmjs.com/package/capacitor-biometric-authentication | | W3C WebAuthn Level 3 | https://www.w3.org/TR/webauthn-3/ | | Support the project | https://aoneahsan.com/payment |
🏷️ Keywords #
webauthn · passkeys · fido2 · biometric · authentication · passwordless · attestation · security · typescript · server
