JWT Sessions

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.

5 min read Level 3/5 #nextjs#jwt#auth
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)
}
// 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.

Database With Prisma →