Why Use Middleware for JWT Auth in Next.js?
Next.js middleware (src/middleware.ts or middleware.ts in the project root) runs on the Edge Runtime — Cloudflare Workers-style V8 isolates deployed at the CDN edge, before the request ever reaches your Node.js server. This makes middleware the ideal place to enforce authentication: a request to a protected page is rejected at the edge without spinning up a full server-side render. The tradeoffs:
- Pro: sub-millisecond latency for auth decisions (no round-trip to origin).
- Pro: centralized — one file covers every route in the app.
- Con: Edge Runtime forbids Node.js built-ins — use
jose, notjsonwebtoken. - Con: No database access from middleware — store all auth state in the token.
Install jose (Edge-Compatible JWT Library)
npm install jose # or pnpm add jose / yarn add jose
# jose uses only Web APIs: fetch, crypto.subtle, TextEncoder
# Works in: Next.js Edge, Cloudflare Workers, Deno, Node.js (18+)Step-by-Step: Minimal JWT Middleware
// src/middleware.ts
import { NextResponse } from 'next/server';
import { jwtVerify, JWTExpired, JWTInvalid } from 'jose';
import type { NextRequest } from 'next/server';
// Encode secret once at module scope (avoids re-encoding per request)
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET);
export async function middleware(req: NextRequest) {
// 1. Skip public routes
const PUBLIC_PREFIXES = ['/login', '/signup', '/_next', '/favicon.ico', '/api/auth'];
if (PUBLIC_PREFIXES.some(p => req.nextUrl.pathname.startsWith(p))) {
return NextResponse.next();
}
// 2. Extract token (cookie-first, Bearer header fallback)
const token =
req.cookies.get('access-token')?.value ??
req.headers.get('authorization')?.replace(/^Bearer /, '');
if (!token) {
return NextResponse.redirect(new URL('/login', req.url));
}
// 3. Verify
try {
const { payload } = await jwtVerify(token, SECRET, {
issuer: 'https://auth.myapp.com', // optional but recommended
audience: 'https://api.myapp.com', // optional but recommended
});
// 4. Forward user identity to Server Components via headers
const res = NextResponse.next();
res.headers.set('x-user-id', String(payload.sub ?? ''));
res.headers.set('x-user-role', String((payload as any).role ?? 'user'));
return res;
} catch (err) {
// Different redirects for expired vs. invalid tokens
if (err instanceof JWTExpired) {
return NextResponse.redirect(new URL('/login?reason=expired', req.url));
}
return NextResponse.redirect(new URL('/login', req.url));
}
}
// Apply middleware only to non-static paths
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};Reading JWT Claims in Server Components
// app/dashboard/page.tsx — Server Component
import { headers } from 'next/headers';
export default function DashboardPage() {
const hdrs = headers();
const userId = hdrs.get('x-user-id');
const role = hdrs.get('x-user-role');
return <div>Welcome user {userId} (role: {role})</div>;
}Role-Based Route Protection
// Add after jwtVerify succeeds:
const ROUTE_ROLES: Record<string, string[]> = {
'/admin': ['admin', 'superadmin'],
'/billing': ['admin', 'finance'],
'/dashboard': ['admin', 'editor', 'viewer'],
};
const { pathname } = req.nextUrl;
for (const [prefix, allowed] of Object.entries(ROUTE_ROLES)) {
if (pathname.startsWith(prefix)) {
const role = (payload as any).role as string;
if (!allowed.includes(role)) {
return NextResponse.redirect(new URL('/403', req.url));
}
}
}Using Refresh Tokens in Next.js
Middleware runs synchronously-ish but cannot call external APIs (no fetch to your own auth server). The recommended pattern:
- Short-lived access token in a cookie (
max-age=900, 15 min). - Long-lived refresh token in a separate
httpOnlycookie (max-age=604800, 7 days). - A dedicated API Route at
/api/auth/refreshthat reads the refresh cookie, validates it with your auth server, and sets a new access-token cookie in the response. - In middleware, when the access token is expired (
JWTExpired), redirect to/api/auth/refresh?redirect=ORIGINAL_PATH. The route handler refreshes transparently and redirects back.
next-auth vs. Custom JWT Middleware
| Consideration | Custom jwt middleware | next-auth (Auth.js) |
|---|---|---|
| Setup complexity | Low (5-20 lines) | Medium (config file + providers) |
| OAuth providers | Manual | Built-in (50+ providers) |
| Session strategy | Stateless JWT only | JWT or database session |
| Edge Runtime | ✅ Full support | ✅ JWT strategy works; database strategy does not |
| Custom claims | Full control | Requires callbacks |
Choose custom JWT middleware for: mobile/SPA auth, API-first architectures, existing auth servers (Keycloak, Auth0, Cognito). Choose next-auth for: social login, rapid prototyping, database-backed sessions.
Related JWT Tools
- JWT Decoder Online — inspect tokens from your Next.js app
- JWT Expiration Checker — confirm
expis set correctly - JWT Decoder in TypeScript — client-side decode pattern
- JWT Express Middleware — the Node.js counterpart
- JWT Secret Key Generator — create the JWT_SECRET env var
- JWT Debugger — decode and verify tokens