All posts
authjwt

JWT Authentication: A Practical Guide for Full-Stack Developers

A practical guide to JWT authentication — structure, signing, expiry strategy, revocation problems, and common security mistakes.

SR

Suhail Roushan

August 6, 2026

·
5 min read
·
0 views

JWT authentication gets misused constantly because the format looks simple — three base64 segments — while the actual security guarantees it provides are easy to get subtly wrong.

A JWT (JSON Web Token) is a compact, self-contained token consisting of a header, payload, and signature, used to represent claims (like a user ID) that a server can verify without a database lookup, because the signature proves the payload hasn't been tampered with since it was issued. That "no database lookup" property is JWT's core value proposition — and also the source of its most common misuse: forgetting that a valid signature doesn't mean a session is still supposed to be active.

Why JWT Matters (and When to Skip It)

JWTs are the right tool when you need stateless verification — microservices that need to validate a token without calling back to an auth service, or APIs where a database round-trip per request is a real cost you're trying to avoid. The signature alone proves authenticity without touching a session store.

Skip JWTs (in favor of opaque session tokens + a database lookup) when instant revocation matters more than statelessness — a banned user, a compromised session, a logged-out device all need to actually lose access immediately, which a stateless JWT can't do until it naturally expires.

Getting Started with JWT Authentication

Signing and verifying with a library like jsonwebtoken:

import jwt from "jsonwebtoken";

const token = jwt.sign(
  { sub: user.id, role: user.role },
  process.env.JWT_SECRET!,
  { expiresIn: "15m" }
);

try {
  const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; role: string };
} catch (err) {
  // signature invalid or token expired
}

A JWT's three segments, decoded, look like this:

// header
{ "alg": "HS256", "typ": "JWT" }

// payload
{ "sub": "user_123", "role": "admin", "iat": 1735689600, "exp": 1735690500 }

// signature: HMACSHA256(base64(header) + "." + base64(payload), secret)

Core JWT Concepts Every Developer Should Know

The payload is readable by anyone, signed but not encrypted. Base64 is not encryption — anyone with the token can decode and read the payload, they just can't forge a valid signature for a modified one. Never put secrets (passwords, raw credit card numbers) in a JWT payload.

Short-lived access tokens plus refresh tokens solve the revocation problem partially. A 15-minute access token limits the damage window if compromised; a longer-lived refresh token (stored server-side or as an httpOnly cookie, checkable against a revocation list) handles renewal:

// access token: short-lived, stateless, used on every request
// refresh token: longer-lived, stored server-side, checked against a revocation list on renewal
async function refreshAccessToken(refreshToken: string) {
  const stored = await db.refreshTokens.findValid(refreshToken);
  if (!stored) throw new Error("Invalid or revoked refresh token");
  return jwt.sign({ sub: stored.userId }, process.env.JWT_SECRET!, { expiresIn: "15m" });
}

Algorithm confusion is a real, documented attack class. Some JWT libraries historically accepted alg: none or let an attacker switch from asymmetric (RS256) to symmetric (HS256) verification, tricking the server into verifying a forged token against a public key treated as a shared secret. Fix: always explicitly specify allowed algorithms when verifying, never trust the alg header from the token itself.

jwt.verify(token, secret, { algorithms: ["HS256"] }); // explicit allowlist

exp (expiry) should always be set and always be checked. A JWT without expiry is a permanent, unrevokable credential — treat a missing exp claim as a configuration bug, not a convenience.

Common JWT Mistakes and How to Fix Them

Mistake 1: storing JWTs in localStorage. This exposes tokens to any XSS vulnerability on the page, since JavaScript can read localStorage freely. Fix: store tokens in httpOnly cookies, which JavaScript can't access, meaningfully reducing XSS token theft risk.

Mistake 2: treating JWTs as inherently revocable. A signed, unexpired JWT remains valid until it expires, full stop — there's no way to "delete" it server-side unless you maintain a blocklist, which reintroduces the statefulness JWTs were meant to avoid. Fix: keep access token lifetimes short and put real revocation logic on the refresh token layer instead.

Mistake 3: putting too much or sensitive data in the payload. A bloated payload adds size to every request; a payload including sensitive fields the client shouldn't see (since it's just base64, not encrypted) is a real data exposure. Fix: keep the payload minimal — user ID and role are usually enough; fetch anything else from your database when needed.

When Should You Use JWTs Instead of Opaque Session Tokens?

Use JWTs for stateless service-to-service auth, or when your access token lifetime is short enough that the revocation gap is acceptable. Use opaque session tokens with a database/cache lookup when instant revocation is a hard requirement — banking apps, admin panels, anything security-sensitive enough that "wait up to 15 minutes for a ban to take effect" isn't acceptable.

JWT Authentication in Production

Rotate your signing secret periodically and support verifying against both the old and new secret during a transition window, rather than invalidating every active session on rotation. Also log and monitor for repeated invalid-signature verification failures — that pattern is a signal of active token forgery attempts, not just expired tokens from normal use.

Before reaching for JWTs by default, ask whether you actually need statelessness — if you don't, an opaque session token with a fast cache lookup (Redis) sidesteps the entire revocation problem for a negligible performance cost.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch