JWT Vulnerabilities in Next.js
Next.js applications commonly use JWTs for authentication across Server Components, API routes, and Middleware. Vulnerabilities arise from storing tokens in localStorage (exposing them to XSS), not validating the algorithm claim, accepting expired tokens, and using weak secrets. NextAuth.js uses JWTs by default, but custom JWT implementations in Next.js are especially prone to security mistakes.
Scan Your Next.js AppHow JWT Vulnerabilities Manifests in Next.js
JWT vulnerabilities in Next.js commonly appear in these patterns: Storing JWTs in localStorage makes them accessible to any JavaScript running on the page. A single XSS vulnerability leaks the token. Client Components that read tokens from localStorage for API calls expose the token to the entire client-side codebase. Custom JWT implementations in API routes that use jwt.verify() without specifying the algorithm are vulnerable to algorithm confusion attacks, where an attacker changes the header to "alg": "none" or switches from RS256 to HS256 using the public key as the HMAC secret. Next.js Middleware that validates JWTs but does not check token expiration allows replayed tokens. Middleware running on the edge may also use different JWT libraries than the Node.js runtime, leading to inconsistent validation. NextAuth.js encrypts its JWT by default, but when developers configure custom JWT encoding or store additional claims, they sometimes weaken the security by using symmetric encryption with short secrets.
Real-World Impact
A Next.js application stored JWTs in localStorage for client-side API authentication. An XSS vulnerability in a user profile page allowed an attacker to inject a script that read localStorage.getItem('token') and sent it to an external server. The attacker used the stolen tokens to impersonate users and access their private data. Another Next.js app used a custom JWT implementation with jwt.verify(token, secret) without specifying algorithms. An attacker modified the JWT header to use "alg": "HS256" with the server's RSA public key as the HMAC secret, forging valid tokens that the server accepted.
Step-by-Step Fix
Store JWTs in httpOnly cookies instead of localStorage
Set tokens as httpOnly cookies from API routes so JavaScript cannot access them.
// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { SignJWT } from 'jose';
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
export async function POST(req: NextRequest) {
const { email, password } = await req.json();
// ... validate credentials ...
const token = await new SignJWT({ sub: user.id, email: user.email })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('1h')
.setIssuedAt()
.setIssuer('your-app')
.sign(secret);
const response = NextResponse.json({ success: true });
response.cookies.set('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 3600,
path: '/',
});
return response;
}Validate JWTs with strict algorithm checking
Use the jose library with explicit algorithm specification to prevent algorithm confusion attacks.
// lib/auth.ts
import { jwtVerify } from 'jose';
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
export async function verifyToken(token: string) {
try {
const { payload } = await jwtVerify(token, secret, {
algorithms: ['HS256'], // Explicitly specify allowed algorithms
issuer: 'your-app', // Validate issuer
maxTokenAge: '1h', // Reject expired tokens
});
return payload;
} catch (error) {
return null; // Token invalid, expired, or tampered
}
}
// Usage in API route
export async function GET(req: NextRequest) {
const token = req.cookies.get('token')?.value;
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const payload = await verifyToken(token);
if (!payload) return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
// ... proceed with authenticated request
}Secure JWT validation in Middleware
Validate JWTs in Next.js Middleware using the jose library which is Edge-compatible.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const protectedPaths = ['/dashboard', '/settings', '/api/user'];
export async function middleware(req: NextRequest) {
const isProtected = protectedPaths.some(p => req.nextUrl.pathname.startsWith(p));
if (!isProtected) return NextResponse.next();
const token = req.cookies.get('token')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', req.url));
}
try {
await jwtVerify(token, secret, { algorithms: ['HS256'] });
return NextResponse.next();
} catch {
return NextResponse.redirect(new URL('/login', req.url));
}
}Prevention Best Practices
1. Store JWTs in httpOnly cookies, not localStorage or sessionStorage. 2. Always specify the expected algorithm when verifying JWTs: jwt.verify(token, secret, { algorithms: ['HS256'] }). 3. Validate token expiration, issuer, and audience claims. 4. Use a strong, random secret of at least 256 bits for HMAC-based JWTs. 5. Prefer NextAuth.js or similar libraries that handle JWT security correctly. 6. Implement token rotation and short expiration times.
How to Test
1. Check if JWTs are stored in localStorage or sessionStorage (browser DevTools > Application > Storage). 2. Decode your JWT at jwt.io and check if it uses "alg": "none" or an unexpected algorithm. 3. Modify the JWT payload (change user ID), re-encode without signing, and test if the server accepts it. 4. Submit an expired token and verify it is rejected. 5. Use Vibe App Scanner to detect JWT configuration issues in your Next.js application.
Frequently Asked Questions
Should I use localStorage or cookies for JWTs in Next.js?
Always use httpOnly cookies. localStorage is accessible to any JavaScript on the page, so a single XSS vulnerability exposes the token. httpOnly cookies cannot be read by JavaScript and are automatically sent with requests. This is especially important in Next.js where API routes can read cookies directly.
Does NextAuth.js handle JWT security automatically?
NextAuth.js encrypts its JWTs using the NEXTAUTH_SECRET and validates them properly by default. However, if you override the jwt or session callbacks, use custom encode/decode functions, or configure a custom JWT secret that is too short, you can weaken the security. Stick to the defaults unless you have a specific reason to customize.
Which JWT library should I use in Next.js?
Use the jose library for Next.js. It is Edge-compatible (works in Middleware), supports all standard JWT algorithms, and provides a clean API for signing and verification. The jsonwebtoken package is popular but does not work in Edge Runtime. jose is the library NextAuth.js uses internally.
Related Security Resources
Is Your Next.js App Vulnerable to JWT Vulnerabilities?
VAS automatically scans for jwt vulnerabilities vulnerabilities in Next.js applications and provides step-by-step remediation guidance with code examples.
Scans from $5, results in minutes. Get actionable fixes tailored to your Next.js stack.