A step-by-step guide to building JWT authentication in Next.js with App Router, middleware, and secure cookie storage.
npm install jose
We use jose — a modern, edge-compatible JWT library that works in Next.js middleware.
# .env.local
JWT_SECRET=your-256-bit-random-secret
// lib/jwt.ts
import { SignJWT, jwtVerify } from "jose";
const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
export async function signToken(payload: Record<string, unknown>) {
return new SignJWT(payload)
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("15m")
.sign(secret);
}
export async function verifyToken(token: string) {
const { payload } = await jwtVerify(token, secret);
return payload;
}
// app/api/auth/login/route.ts
import { signToken } from "@/lib/jwt";
import { cookies } from "next/headers";
export async function POST(req: Request) {
const { email, password } = await req.json();
const user = await verifyCredentials(email, password);
if (!user) return Response.json({ error: "Invalid credentials" }, { status: 401 });
const token = await signToken({ sub: user.id, email: user.email });
(await cookies()).set('token', token, { httpOnly: true, secure: true, maxAge: 900 });
return Response.json({ success: true });
}
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { verifyToken } from "@/lib/jwt";
export async function middleware(req: NextRequest) {
const token = req.cookies.get('token')?.value;
if (!token) return NextResponse.redirect(new URL("/login", req.url));
try {
await verifyToken(token);
return NextResponse.next();
} catch {
return NextResponse.redirect(new URL("/login", req.url));
}
}
export const config = { matcher: ["/dashboard/:path*"] };
// app/api/auth/logout/route.ts
import { cookies } from "next/headers";
export async function POST() {
(await cookies()).delete('token');
return Response.json({ success: true });
}