Dilip Singh logo
All posts
Web DevelopmentIntermediate2026-03-12·10 min read

JWT Authentication in Next.js 15 App Router: A Complete Guide

The right way to do JWT authentication in Next.js 15 with App Router — middleware, refresh tokens, secure cookies, role-based access, and server components that know who you are.

Why JWT for App Router?

Next.js 15 App Router is server-first. Server Components can call your database directly. JWT auth — signed, short-lived, and cookie-stored — gives you stateless authentication that works in both Server Components and Middleware.

Token Strategy

TokenLifetimeStorage
Access15 minutesHttpOnly Secure Cookie
Refresh7 daysHttpOnly Secure Cookie (different path)

Never put JWTs in localStorage. Ever.

Signing and Verifying with `jose`

typescript
// lib/auth.ts
import { SignJWT, jwtVerify } from 'jose'

const secret = new TextEncoder().encode(process.env.JWT_SECRET!)

export async function signAccess(payload: { sub: string; role: string }) { return new SignJWT(payload) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('15m') .sign(secret) }

export async function verifyAccess(token: string) { const { payload } = await jwtVerify(token, secret) return payload as { sub: string; role: string } } ```

Login Server Action

typescript
'use server'
import { cookies } from 'next/headers'

export async function login(email: string, password: string) { const user = await authenticate(email, password) if (!user) return { error: 'Invalid credentials' }

const access = await signAccess({ sub: user.id, role: user.role }) const refresh = await signRefresh({ sub: user.id })

const cookieStore = await cookies() cookieStore.set('access', access, { httpOnly: true, secure: true, sameSite: 'lax', path: '/', maxAge: 60 * 15, }) cookieStore.set('refresh', refresh, { httpOnly: true, secure: true, sameSite: 'lax', path: '/api/refresh', maxAge: 60 60 24 * 7, }) return { ok: true } } ```

Middleware-Based Protection

typescript
// middleware.ts
import { NextResponse, type NextRequest } from 'next/server'
import { verifyAccess } from '@/lib/auth'

const PUBLIC = ['/', '/login', '/about', '/blog']

export async function middleware(req: NextRequest) { const { pathname } = req.nextUrl if (PUBLIC.includes(pathname) || pathname.startsWith('/_next')) return NextResponse.next()

const token = req.cookies.get('access')?.value if (!token) return NextResponse.redirect(new URL('/login', req.url))

try { const payload = await verifyAccess(token) const res = NextResponse.next() res.headers.set('x-user-id', payload.sub) res.headers.set('x-user-role', payload.role) return res } catch { return NextResponse.redirect(new URL('/login', req.url)) } }

export const config = { matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico).*)'] } ```

Reading User in a Server Component

typescript
import { headers } from 'next/headers'

export default async function DashboardPage() { const h = await headers() const userId = h.get('x-user-id')! const userRole = h.get('x-user-role')! const orders = await db.order.findMany({ where: { userId } }) return } ```

Refresh Flow

typescript
// app/api/refresh/route.ts
export async function POST(req: Request) {
  const refresh = req.headers.get('cookie')?.match(/refresh=([^;]+)/)?.[1]
  if (!refresh) return new Response('no refresh', { status: 401 })

try { const { payload } = await jwtVerify(refresh, refreshSecret) const user = await db.user.findUnique({ where: { id: payload.sub as string } }) if (!user || user.refreshTokenVersion !== payload.v) throw new Error('revoked')

const access = await signAccess({ sub: user.id, role: user.role }) return new Response(null, { status: 204, headers: { 'Set-Cookie': access=${access}; HttpOnly; Secure; Path=/; Max-Age=900 } }) } catch { return new Response('invalid', { status: 401 }) } } ```

Revocation: The Refresh Version Pattern

To force-logout a user, increment their refreshTokenVersion in the DB. All refresh attempts fail until they log back in. Access tokens expire naturally in 15 minutes — short-lived by design.

DS
Dilip Singh
Lead Software Architect · Hureka Technologies

14+ years building enterprise software and AI systems. Architecting multi-agent AI platforms, RAG pipelines, voice AI, and high-performance SaaS for global clients.