JWT & Session Management
Secure user sessions with JSON Web Tokens
🎫 What is JWT?
JSON Web Tokens (JWT) are secure tokens for authentication. They store user information in an encrypted format, allowing stateless authentication without server-side session storage for scalable applications.
// JWT structure: header.payload.signature
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
Key JWT Concepts
Stateless Auth
No server-side session storage needed
Self-Contained
Token contains all user information
Secure
Cryptographically signed and verified
Expiration
Automatic token expiry for security
🔹 Creating JWT Tokens
Generate JWT tokens using the jose library in Next.js. Tokens contain user data and are signed with a secret key to ensure authenticity and prevent tampering.
# Install jose library
npm install jose
🔸 Sign JWT Token
// lib/jwt.js
import { SignJWT } from 'jose'
export async function createToken(payload) {
const secret = new TextEncoder().encode(
process.env.JWT_SECRET
)
const token = await new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('24h')
.sign(secret)
return token
}
Result:
✅ Secure JWT token created
✅ Expires in 24 hours
✅ Contains user payload
🔹 Verifying JWT Tokens
Verify and decode JWT tokens to authenticate requests. This ensures the token is valid, not expired, and hasn't been tampered with before granting access to protected resources.
// lib/jwt.js
import { jwtVerify } from 'jose'
export async function verifyToken(token) {
try {
const secret = new TextEncoder().encode(
process.env.JWT_SECRET
)
const { payload } = await jwtVerify(token, secret)
return payload
} catch (error) {
return null
}
}
Verification checks:
- Token signature is valid
- Token hasn't expired
- Token hasn't been tampered with
- Secret key matches
🔹 Login API Route
Create an API route that authenticates users and returns a JWT token. This endpoint validates credentials and generates a token that clients can use for subsequent authenticated requests.
// app/api/login/route.js
import { createToken } from '@/lib/jwt'
import { NextResponse } from 'next/server'
export async function POST(request) {
const { email, password } = await request.json()
// Validate credentials (check database)
const user = await validateUser(email, password)
if (!user) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
)
}
// Create JWT token
const token = await createToken({
userId: user.id,
email: user.email,
})
return NextResponse.json({ token })
}
Result:
✅ User authenticated
✅ JWT token generated
✅ Token returned to client
🔹 Middleware Authentication
Use middleware to protect routes by verifying JWT tokens before allowing access. This centralizes authentication logic and automatically protects multiple routes without repeating code.
🔸 Create Middleware
// middleware.js
import { NextResponse } from 'next/server'
import { verifyToken } from '@/lib/jwt'
export async function middleware(request) {
const token = request.cookies.get('token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
const payload = await verifyToken(token)
if (!payload) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*'],
}
🔸 Protected Routes
- /dashboard/* - Requires authentication
- /profile/* - Requires authentication
- Other routes - Public access
🔹 Storing Tokens
Store JWT tokens securely in HTTP-only cookies to prevent XSS attacks. Cookies are automatically sent with requests and can't be accessed by JavaScript, providing better security than localStorage.
🔸 Set Cookie
// app/api/login/route.js
import { cookies } from 'next/headers'
export async function POST(request) {
// ... authenticate user ...
const token = await createToken(payload)
cookies().set('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24, // 24 hours
})
return NextResponse.json({ success: true })
}
🔸 Clear Cookie (Logout)
// app/api/logout/route.js
import { cookies } from 'next/headers'
export async function POST() {
cookies().delete('token')
return NextResponse.json({ success: true })
}
🔹 Refresh Tokens
Implement refresh tokens for long-lived sessions. Access tokens expire quickly for security, while refresh tokens allow users to get new access tokens without re-authenticating frequently.
// app/api/refresh/route.js
import { verifyToken, createToken } from '@/lib/jwt'
import { cookies } from 'next/headers'
export async function POST() {
const refreshToken = cookies().get('refreshToken')?.value
if (!refreshToken) {
return NextResponse.json(
{ error: 'No refresh token' },
{ status: 401 }
)
}
const payload = await verifyToken(refreshToken)
if (!payload) {
return NextResponse.json(
{ error: 'Invalid refresh token' },
{ status: 401 }
)
}
// Create new access token
const newToken = await createToken({
userId: payload.userId,
email: payload.email,
})
cookies().set('token', newToken, {
httpOnly: true,
secure: true,
maxAge: 60 * 15, // 15 minutes
})
return NextResponse.json({ success: true })
}
🔹 Client-Side Usage
Access protected API routes from the client using the stored JWT token. The token is automatically included in requests via cookies, making authenticated API calls simple and secure.
'use client'
import { useState } from 'react'
export default function LoginForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleLogin = async (e) => {
e.preventDefault()
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
if (response.ok) {
window.location.href = '/dashboard'
}
}
return (
<form onSubmit={handleLogin}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
)
}