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-charsImplementation#
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#
- Use strong secrets - Minimum 32 characters, randomly generated
- Set short expiration - Balance security vs. user experience
- Use httpOnly cookies - Prevent XSS access to tokens
- Enable secure flag - Only send over HTTPS in production
- Implement refresh tokens - For longer sessions, use refresh token rotation
- Handle token errors - Gracefully handle expired/invalid tokens
Related Patterns#
- Session Management - Advanced session handling
- NextAuth.js - NextAuth.js with JWT strategy
- MFA - Multi-factor authentication