Implementing Passwordless Authentication with WebAuthn in a Next.js 13 App Using TypeScript

User avatar placeholder
Written by Tamzid Ahmed

June 19, 2026

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:

  1. 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.
  2. 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

  1. Bootstrap the Next.js 13 app and add TypeScript support.
  2. Create the lib/webauthn.ts helper with generateChallenge using @simplewebauthn/server.
  3. Define API routes in app/api/auth/**/route.ts for register and login.
  4. Build the UI components that trigger navigator.credentials.create() and navigator.credentials.get().
  5. Persist credential data in PostgreSQL or MongoDB, ensuring signCount is updated after each assertion.
  6. Lock down HTTPS, set origin to your production domain, and enable userVerification.
  7. Deploy to Vercel or any Node‑native platform; WebAuthn works in today’s major browsers.
  8. 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.

Leave a Comment