JWT Authentication

Patterns for JWT (JSON Web Token) handling and validation in Next.js.

Overview#

JWT provides stateless authentication with:

  • Self-contained tokens with claims
  • No server-side session storage required
  • Edge-compatible verification
  • Cryptographic signature validation

Prerequisites:

  • Understanding of token-based auth
  • Secure secret key management

Installation#

npm install jose
# .env.local JWT_SECRET=your-secure-secret-key-min-32-chars

Implementation#

JWT Session Creation#

1// lib/session.ts 2import { SignJWT, jwtVerify } from 'jose' 3import { cookies } from 'next/headers' 4 5const secret = new TextEncoder().encode(process.env.JWT_SECRET) 6const COOKIE_NAME = 'session' 7 8export async function createSession(userId: string) { 9 const token = await new SignJWT({ userId }) 10 .setProtectedHeader({ alg: 'HS256' }) 11 .setIssuedAt() 12 .setExpirationTime('7d') 13 .sign(secret) 14 15 cookies().set(COOKIE_NAME, token, { 16 httpOnly: true, 17 secure: process.env.NODE_ENV === 'production', 18 sameSite: 'lax', 19 maxAge: 60 * 60 * 24 * 7, // 7 days 20 path: '/' 21 }) 22 23 return token 24}

Session Verification#

1// lib/session.ts 2export async function getSession() { 3 const token = cookies().get(COOKIE_NAME)?.value 4 5 if (!token) return null 6 7 try { 8 const { payload } = await jwtVerify(token, secret) 9 return payload as { userId: string } 10 } catch { 11 return null 12 } 13} 14 15export async function requireSession() { 16 const session = await getSession() 17 18 if (!session) { 19 throw new Error('Unauthorized') 20 } 21 22 return session 23}

Session Refresh#

1// lib/session.ts 2export async function refreshSession() { 3 const session = await getSession() 4 5 if (!session) return null 6 7 // Create new token with extended expiry 8 return createSession(session.userId) 9}

Session Destruction#

// lib/session.ts export async function destroySession() { cookies().delete(COOKIE_NAME) }

JWT Middleware#

1// middleware.ts 2import { NextResponse } from 'next/server' 3import type { NextRequest } from 'next/server' 4import { jwtVerify } from 'jose' 5 6const secret = new TextEncoder().encode(process.env.JWT_SECRET) 7 8export async function middleware(request: NextRequest) { 9 const token = request.cookies.get('session')?.value 10 11 if (!token) { 12 return NextResponse.redirect(new URL('/login', request.url)) 13 } 14 15 try { 16 await jwtVerify(token, secret) 17 return NextResponse.next() 18 } catch { 19 return NextResponse.redirect(new URL('/login', request.url)) 20 } 21} 22 23export const config = { 24 matcher: ['/dashboard/:path*', '/settings/:path*'] 25}

API Token Authentication#

1// lib/api-auth.ts 2import { jwtVerify, SignJWT } from 'jose' 3 4const secret = new TextEncoder().encode(process.env.JWT_SECRET) 5 6export async function verifyApiToken(token: string) { 7 try { 8 const { payload } = await jwtVerify(token, secret) 9 return payload 10 } catch { 11 return null 12 } 13} 14 15export async function createApiToken(payload: Record<string, unknown>) { 16 return new SignJWT(payload) 17 .setProtectedHeader({ alg: 'HS256' }) 18 .setIssuedAt() 19 .setExpirationTime('30d') 20 .sign(secret) 21}

API Route with JWT Auth#

1// app/api/protected/route.ts 2import { NextResponse } from 'next/server' 3import { verifyApiToken } from '@/lib/api-auth' 4 5export async function GET(request: Request) { 6 const authHeader = request.headers.get('authorization') 7 const token = authHeader?.replace('Bearer ', '') 8 9 if (!token) { 10 return NextResponse.json( 11 { error: 'Missing authorization token' }, 12 { status: 401 } 13 ) 14 } 15 16 const payload = await verifyApiToken(token) 17 18 if (!payload) { 19 return NextResponse.json( 20 { error: 'Invalid or expired token' }, 21 { status: 401 } 22 ) 23 } 24 25 return NextResponse.json({ data: 'Protected data', user: payload }) 26}

Usage Examples#

Login with JWT#

1// app/api/login/route.ts 2import { NextResponse } from 'next/server' 3import { createSession } from '@/lib/session' 4import { prisma } from '@/lib/db' 5import bcrypt from 'bcryptjs' 6 7export async function POST(request: Request) { 8 const { email, password } = await request.json() 9 10 const user = await prisma.user.findUnique({ 11 where: { email } 12 }) 13 14 if (!user || !await bcrypt.compare(password, user.password)) { 15 return NextResponse.json( 16 { error: 'Invalid credentials' }, 17 { status: 401 } 18 ) 19 } 20 21 await createSession(user.id) 22 23 return NextResponse.json({ success: true }) 24}

Protected Server Action#

1// actions/user.ts 2'use server' 3 4import { requireSession } from '@/lib/session' 5import { prisma } from '@/lib/db' 6 7export async function getUserProfile() { 8 const session = await requireSession() 9 10 return prisma.user.findUnique({ 11 where: { id: session.userId } 12 }) 13}

Best Practices#

  1. Use strong secrets - Minimum 32 characters, randomly generated
  2. Set short expiration - Balance security vs. user experience
  3. Use httpOnly cookies - Prevent XSS access to tokens
  4. Enable secure flag - Only send over HTTPS in production
  5. Implement refresh tokens - For longer sessions, use refresh token rotation
  6. Handle token errors - Gracefully handle expired/invalid tokens