Next.jsIntermediateMay 24, 2024 · 12 min read
Build Authentication in Next.js with Credentials and JWT
Add secure email/password authentication to a Next.js app using credentials and JSON Web Tokens, including protected routes and session handling.
Authentication is one of the first things every real application needs. In this tutorial we wire up an email and password flow in the Next.js App Router, issue a signed JWT, and store it in an http-only cookie.
The plan
- A route handler that verifies credentials and issues a token.
- An http-only cookie so the token never touches client JavaScript.
- A small helper to read the session on the server.
Issuing a token
// app/api/login/route.ts
import { NextResponse } from "next/server";
import { SignJWT } from "jose";
import { verifyUser } from "@/lib/users";
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
export async function POST(request: Request) {
const { email, password } = await request.json();
const user = await verifyUser(email, password);
if (!user) {
return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
}
const token = await new SignJWT({ sub: user.id })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("7d")
.sign(secret);
const res = NextResponse.json({ ok: true });
res.cookies.set("session", token, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
});
return res;
}Never store JWTs in localStorage. An http-only cookie can't be read by
JavaScript, which closes off an entire class of XSS token-theft attacks.
Reading the session
// lib/session.ts
import { cookies } from "next/headers";
import { jwtVerify } from "jose";
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
export async function getSession() {
const token = (await cookies()).get("session")?.value;
if (!token) return null;
try {
const { payload } = await jwtVerify(token, secret);
return { userId: payload.sub as string };
} catch {
return null;
}
}Protecting a page
// app/dashboard/page.tsx
import { redirect } from "next/navigation";
import { getSession } from "@/lib/session";
export default async function Dashboard() {
const session = await getSession();
if (!session) redirect("/login");
return <h1>Welcome back</h1>;
}Signing out
Clearing the cookie is all it takes:
res.cookies.set("session", "", { maxAge: 0, path: "/" });That's a complete, secure credentials flow — no third-party auth provider required.