Stateless Tokens You Manage Yourself
JWT Sessions
If you'd rather not pull in Auth.js, you can sign a JWT yourself, store it in an httpOnly cookie, and verify it in middleware.
What you'll learn
- Sign tokens with `jose`
- Set them as `httpOnly` cookies on login
- Verify them in middleware and server actions
A JWT-based auth flow is stateless: the server signs a token, the browser sends it back on every request, and the server verifies it without consulting a database.
Sign a Token
// lib/auth.ts
import * as jose from 'jose'
const secret = new TextEncoder().encode(process.env.JWT_SECRET!)
export async function signSession(userId: string) {
return await new jose.SignJWT({ sub: userId })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('1h')
.sign(secret)
} Set the Cookie
// app/actions/login.ts
'use server'
import { cookies } from 'next/headers'
import { signSession } from '@/lib/auth'
export async function login(form: FormData) {
// ...verify credentials...
const token = await signSession(user.id)
const c = await cookies()
c.set('session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60,
path: '/',
})
} Verify in Middleware
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import * as jose from 'jose'
const secret = new TextEncoder().encode(process.env.JWT_SECRET!)
export async function middleware(req: NextRequest) {
const token = req.cookies.get('session')?.value
if (!token) return NextResponse.redirect(new URL('/login', req.url))
try {
await jose.jwtVerify(token, secret)
return NextResponse.next()
} catch {
return NextResponse.redirect(new URL('/login', req.url))
}
}
export const config = { matcher: ['/app/:path*'] } jose works in the Edge Runtime; jsonwebtoken does not. Always set cookies
httpOnly + secure + sameSite=lax and pick a strong secret.