JSON Web Tokens

Signed Tokens — Stateless Auth at Scale

JSON Web Tokens

JWTs are signed tokens carrying the user identity. Stateless, scalable, easy to get wrong.

4 min read Level 3/5 #express#jwt#auth
What you'll learn
  • Issue a JWT on login
  • Verify it on every request
  • Choose the right algorithm

A JWT (JSON Web Token) is a signed string that carries a JSON payload. The signature proves we issued it; the payload tells us who the user is.

Install

npm install jsonwebtoken

Issue on Login

import jwt from "jsonwebtoken";

const SECRET = process.env.JWT_SECRET;   // a 32+ byte random string

function issueToken(user) {
  return jwt.sign(
    { sub: user.id, email: user.email },
    SECRET,
    { expiresIn: "15m" }
  );
}

app.post("/auth/login", validate({ body: LoginSchema }), async (req, res) => {
  const { email, password } = req.validBody;
  const user = await db.users.findByEmail(email);
  if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
    return res.status(401).json({ error: { code: "invalid_credentials" } });
  }
  const token = issueToken(user);
  res.json({ data: { token, user: { id: user.id, email: user.email } } });
});

Verify on Every Request

export function requireAuth(req, res, next) {
  const header = req.headers.authorization;
  const token = header?.startsWith("Bearer ") ? header.slice(7) : null;
  if (!token) {
    return res.status(401).json({ error: { code: "missing_token" } });
  }
  try {
    req.user = jwt.verify(token, SECRET);
    next();
  } catch {
    res.status(401).json({ error: { code: "invalid_token" } });
  }
}

app.get("/me", requireAuth, (req, res) => {
  res.json({ data: { id: req.user.sub, email: req.user.email } });
});

Algorithms — Pick Wisely

jwt.sign defaults to HS256 (HMAC + shared secret). For most apps that’s fine. For microservices where one service issues and others verify:

  • HS256 — symmetric (shared secret). Simple, fast.
  • RS256 / ES256 — asymmetric (private key signs, public key verifies). Better when verifiers don’t need the issuing power.

Never accept alg: none — that’s a known JWT exploit. Use jose (more modern lib) and pin the algorithm explicitly.

Token Lifetime

Short-lived tokens (15 min) plus a refresh token (longer-lived, stored as a session in your DB) is the common pattern. Refresh tokens enable revocation; access tokens stay stateless.

Storing Tokens on the Client

Two options:

WhereProsCons
localStorageSimpleVulnerable to XSS — JS can read it
HttpOnly cookieSafe from XSSNeeds CSRF protection

HttpOnly cookie is the safer default, even though most JWT tutorials show localStorage. The latter is fine for very low-risk apps; not for anything user-data-sensitive.

Where JWT Shines

  • Microservices — one issuer, many verifiers
  • Mobile + web sharing an API — token works the same everywhere
  • Anywhere you can tolerate “logout doesn’t immediately invalidate”

Where Sessions Win

  • You need instant revocation
  • You’re already running Redis / DB
  • Cookies + same-domain is enough

What NOT To Put In A JWT

A JWT is not encrypted by default — it’s signed. Anyone can decode it (the payload is base64). Don’t put secrets, passwords, or sensitive PII in the payload.

Passport.js →