Imagine logging into your app by simply touching a fingerprint sensor or tapping a smart key—no more passwords, no more guesswork. Passwordless authentication is not a future promise; it exists today, powered by the WebAuthn standard.
This article walks you through a full implementation of WebAuthn in a modern Next.js 13 application with TypeScript, covering browser APIs, server‑side verification, data persistence, and best‑practice security measures.
What is Passwordless Authentication with WebAuthn?
How WebAuthn Works
WebAuthn is a web standard that lets users prove ownership of a cryptographic key pair without sending a password. The browser bridges your app to an authenticator—hardware key, built‑in biometrics, or a phone—that generates a credential.
The flow can be summarized in two main stages:
- Registration (Credential Creation): The server sends a challenge and a list of acceptable relying party identifiers. The browser then calls navigator.credentials.create() and returns a public key credential to the server.
- Authentication (Assertion): The server issues a fresh challenge. The client uses navigator.credentials.get() to sign the challenge with the private key stored in the authenticator, producing an assertion that the server can verify.
Benefits Over Passwords
Passwords can be phished, reused, or stolen. WebAuthn removes these risks by binding authentication to a device that is difficult to duplicate or intercept. It also delivers a friction‑less experience, especially when paired with biometrics or OS‑level passkeys.
Setting Up Your Next.js 13 App
Install Dependencies
Use the built‑in app directory and type‑safe APIs with TypeScript:
“`bash
npm i next react react-dom
npm i -D typescript @types/react
npm i @simplewebauthn/server @simplewebauthn/browser
“`
Folder Structure & App Router
Next.js 13 encourages colocated routes. Create the following:
app/profile/page.tsx– user dashboard.app/api/auth/[...role]/route.ts– API for registration & login.lib/webauthn.ts– shared crypto helpers.
Client‑Side: Building the Sign‑Up / Sign‑In Flow
Creating Credentials with navigator.credentials.create()
When the user clicks Register, fetch the challenge from /api/auth/register and call:
“`ts
const assertion = await navigator.credentials.create({
publicKey: {/* challenge payload */}
});
“`
Requesting Authentication with navigator.credentials.get()
For Login, obtain a new challenge and run:
“`ts
const assertion = await navigator.credentials.get({
publicKey: {/* challenge payload */}
});
“`
Send the result back to /api/auth/login for verification.
Server‑Side: API Routes and Security
Handling Credential Creation Requests
Use @simplewebauthn/server to build a verification pipeline:
“`ts
import { verifyRegistrationResponse } from ‘@simplewebauthn/server’;
export async function POST(request: Request) {
const body = await request.json();
const { registrationResponse } = body;
const expectedChallenge = ‘…’; // Store in session
const verification = await verifyRegistrationResponse({
response: registrationResponse,
expectedChallenge,
expectedOrigin: ‘https://yourapp.com’,
expectedRPID: ‘yourapp.com’,
requireUserVerification: true,
});
if (verification.verified) {
// Persist credentialDescriptor.publicKey to DB
}
}
“`
Verifying Assertions from Clients
The login route mirrors the verify pattern:
“`ts
import { verifyAuthenticationResponse } from ‘@simplewebauthn/server’;
export async function POST(request: Request) {
const body = await request.json();
const authResponse = body.authenticationResponse;
const storedCred = /* fetch from DB using user id */;
const verification = await verifyAuthenticationResponse({
response: authResponse,
expectedChallenge: ‘…’,
expectedOrigin: ‘https://yourapp.com’,
expectedRPID: ‘yourapp.com’,
authenticator: storedCred,
requireUserVerification: true,
});
if (verification.verified) {
// Set auth cookie / JWT
}
}
“`
Storing Credential Metadata
Keep only the credentialID, publicKey, and signCount. Never store private keys—those reside on the authenticator.
Full Code Sample & Deployment Checklist
- Bootstrap the Next.js 13 app and add TypeScript support.
- Create the
lib/webauthn.tshelper withgenerateChallengeusing @simplewebauthn/server. - Define API routes in
app/api/auth/**/route.tsfor register and login. - Build the UI components that trigger
navigator.credentials.create()andnavigator.credentials.get(). - Persist credential data in PostgreSQL or MongoDB, ensuring
signCountis updated after each assertion. - Lock down HTTPS, set
originto your production domain, and enableuserVerification. - Deploy to Vercel or any Node‑native platform; WebAuthn works in today’s major browsers.
- Test cross‑device scenarios: a Windows PC, a macOS laptop, and an Android phone with a PIN lock.
Conclusion
Adding passwordless authentication with WebAuthn to a Next.js 13 app elevates security, reduces support tickets, and future‑proofs your login flow. By leveraging modern browser APIs, strong cryptography, and type safety, you deliver a user experience that feels effortless and remains unbreakable. Start prototyping today—your users will thank you, and your dev team will appreciate the reduced password maintenance overhead.