JSON Web Tokens (JWTs) are a common way to represent an authenticated session without a server-side lookup on every request. This lesson explains how JWTs work and how to use them safely in a Next.js app.
What a JWT Actually Is
A JWT is a compact, signed string encoding a JSON payload — typically the user's ID and some claims like an expiration time — split into three base64-encoded parts (header, payload, signature) separated by periods. The signature, generated with a secret key only your server knows, lets you verify the token hasn't been tampered with, without needing to look anything up in a database.
Because the payload is only base64-encoded, not encrypted, anyone can decode and read a JWT's contents — never put sensitive data (passwords, secrets) inside one. The security guarantee a JWT provides is integrity (it hasn't been altered), not confidentiality.
import jwt from "jsonwebtoken";
// Signing a token after successful login
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET!,
{ expiresIn: "7d" }
);
// Verifying a token on a protected request
const payload = jwt.verify(token, process.env.JWT_SECRET!);
// payload.userId is now trusted, since the signature checked out
jwt.verify throws if the signature doesn't match or the token has expired, which you should catch and handle explicitly.
JWT Structure
header.payload.signature
// Example (structure, not a real token):
eyJhbGc... . eyJ1c2VySWQ... . SflKxwRJ...
The header describes the signing algorithm; the payload carries your claims; the signature verifies integrity.
Anyone can decode and read the header and payload — they are not encrypted.
Only someone with the signing secret can produce a signature that verifies successfully.
Always set an expiresIn claim so stolen tokens don't remain valid indefinitely.
JWT Cheat Sheet
Key facts and best practices.
Fact / Practice
Detail
Encoding
Base64 (readable), not encrypted
Integrity check
Cryptographic signature, verified with a secret key
Never store
Passwords, secrets, or highly sensitive data in the payload
Always set
An expiration claim (expiresIn)
Storage location
httpOnly cookie (preferred) over localStorage
Storing JWTs Safely
Storing a JWT in localStorage makes it readable (and stealable) by any JavaScript running on the page, including from a successful XSS attack. Storing it in an httpOnly cookie instead means client-side JavaScript can never read it directly, significantly reducing that risk, while the browser still sends it automatically with every request.
// Setting a JWT as an httpOnly cookie (Server Action / Route Handler)
import { cookies } from "next/headers";
cookies().set("token", jwtToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7, // 7 days
});
secure: true ensures the cookie is only sent over HTTPS; sameSite: "lax" provides baseline CSRF protection.
The Revocation Trade-Off
Because a JWT is self-contained and verified purely by its signature, there's no built-in way to "log out" a specific token before it naturally expires — the server doesn't track which tokens are still valid. Some systems mitigate this with a short expiration plus a refresh token, or by maintaining a server-side blocklist for revoked tokens, trading away some of the stateless simplicity for revocability.
Common Mistakes
Storing a JWT in localStorage instead of an httpOnly cookie, increasing XSS risk.
Putting sensitive data directly in the JWT payload, forgetting it's readable by anyone with the token.
Not setting an expiration claim, leaving stolen tokens valid indefinitely.
Hardcoding the signing secret in source code instead of an environment variable.
Key Takeaways
A JWT is a signed, base64-encoded token proving a claim (like a user ID) without a database lookup.
JWTs are readable by anyone, but only verifiable (and forgeable) by someone with the signing secret.
Always set an expiration and never store sensitive data directly in the payload.
Prefer httpOnly cookies over localStorage for storing JWTs.
Revocation before expiration requires extra infrastructure, unlike traditional server-side sessions.
Pro Tip
If your app needs the ability to immediately revoke a compromised session (e.g. "log out everywhere"), lean toward short-lived JWTs paired with a server-tracked refresh token, or a traditional database-backed session — pure long-lived JWTs make immediate revocation structurally difficult.
You now understand JWTs. Next, learn how to build protected routes that require authentication before granting access.