A production-ready FIDO2 WebAuthn passkeys implementation using Node.js, Express, and @simplewebauthn. Demonstrates passkey registration, authentication, and credential management with a clean web UI.
- Passkey Registration — Generate and verify registration options with ES256/RS256 support
- Passkey Authentication — Full authentication flow with user verification
- Credential Management — Store, list, and delete registered passkeys
- Session Management — Express sessions with secure defaults
- Production Security — Origin validation, RPID enforcement, challenge management, counter verification
- Clean Web UI — Minimal frontend demonstrating the complete flow
git clone https://github.com/IAMDevBox/webauthn-passkeys-demo.git
cd webauthn-passkeys-demo
npm install
npm start
# Open https://localhost:3000 (HTTPS required for WebAuthn)Note: WebAuthn requires HTTPS. For local development, the server auto-generates a self-signed certificate.
├── server.js # Express server with WebAuthn endpoints
├── public/
│ ├── index.html # Registration and login UI
│ └── app.js # Client-side WebAuthn API calls
├── lib/
│ ├── credential-store.js # In-memory credential storage (swap for DB in production)
│ └── challenge-store.js # Challenge management with TTL
├── certs/ # Auto-generated self-signed certs for local dev
├── package.json
└── .env.example
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/register/start |
Generate registration options |
| POST | /api/register/finish |
Verify and store registration |
| POST | /api/login/start |
Generate authentication options |
| POST | /api/login/finish |
Verify authentication response |
| GET | /api/credentials |
List registered credentials |
| DELETE | /api/credentials/:id |
Remove a credential |
Copy .env.example to .env and configure:
RP_NAME=Example Corp
RP_ID=localhost
ORIGIN=https://localhost:3000
PORT=3000
SESSION_SECRET=change-me-in-production- Client requests registration options from
/api/register/start - Server generates a challenge and registration options using
generateRegistrationOptions() - Client calls
navigator.credentials.create()with the options - Client sends the credential response to
/api/register/finish - Server verifies with
verifyRegistrationResponse()and stores the credential
- Client requests authentication options from
/api/login/start - Server generates a challenge and lists allowed credentials
- Client calls
navigator.credentials.get()with the options - Client sends the assertion to
/api/login/finish - Server verifies with
verifyAuthenticationResponse()and updates the counter
- Replace in-memory stores with a database (PostgreSQL, MongoDB, Redis)
- Use a proper session store (connect-redis, connect-pg-simple)
- Set
userVerification: 'required'for sensitive operations - Implement rate limiting on registration and authentication endpoints
- Monitor counter values — a counter going backwards indicates credential cloning
- Passkeys Adoption Guide: Implementing FIDO2 WebAuthn in Production — Complete guide with production deployment strategies
- Cross-Device Passkey Authentication: Hybrid Flow Implementation — How passkeys work across devices
- FIDO vs FIDO2: Understanding the Evolution of Passwordless Authentication — Protocol comparison
- WebAuthn Conditional UI: Streamlined Passwordless Login — Autofill-based passkey login
- IAMDevBox Tools — Free online developer tools for IAM
MIT