JWT Authentication

intermediate
14 min

JWT Authentication

API keys identify applications, but products with human users need per-user sessions that carry identity, expire automatically, and scale across servers. JSON Web Tokens (JWTs) are the dominant answer for REST APIs. In this lesson we dissect the token format, build the login flow, and cover the refresh-token pattern and the pitfalls that regularly cause real-world breaches.

Anatomy of a JWT

A JWT is three base64url-encoded parts joined by dots: header.payload.signature.

text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 <- header: {"alg":"HS256","typ":"JWT"} .eyJzdWIiOiJ1XzQyIiwicm9sZSI6ImVkaXRvciIsImV4cCI6MTc4NDAwMDAwMH0 .p3k1Zx9v... <- signature

The payload holds claims: standard ones like sub (subject, the user ID), exp (expiry timestamp), iat (issued at), and custom ones like role. The signature is computed over the first two parts with a secret key (HS256) or a private key (RS256). Anyone can decode and read a JWT; only holders of the key can create a valid signature. JWTs provide integrity and authenticity, not secrecy, so never put passwords or sensitive data in the payload.

The Login Flow

  1. The client POSTs credentials to /auth/login.
  2. The server verifies the password against its stored hash (bcrypt or argon2).
  3. The server signs a JWT containing sub, role, and a short exp, and returns it.
  4. The client stores the token and sends it on every request: Authorization: Bearer <token>.
  5. Middleware verifies the signature and expiry on each request, then attaches the decoded claims to the request object. No database lookup is needed, which is the stateless scaling win: any server with the secret can validate any token.

Access and Refresh Tokens

Statelessness has a dark side: a signed token stays valid until exp, and you cannot easily un-sign it. The standard mitigation is a pair of tokens. The access token is short-lived, around 15 minutes, and is what accompanies API calls. The refresh token is long-lived, days or weeks, stored server-side in a database so it can be revoked, and is used only against /auth/refresh to mint new access tokens.

When a user logs out or is compromised, you revoke the refresh token; the attacker's window shrinks to whatever remains of the 15-minute access token. Rotating refresh tokens on each use, and invalidating the family if an old one is replayed, closes the window further.

Where Should Browsers Store Tokens?

For browser clients, localStorage is readable by any script, so one XSS vulnerability leaks every user's token. The safer pattern is an httpOnly, Secure, SameSite cookie for the refresh token, with the access token held only in memory. Mobile and server-side clients can use their platform's secure storage. Whatever you choose, transmit tokens exclusively over HTTPS.

Classic JWT Pitfalls

  • alg: none attacks: old libraries accepted unsigned tokens. Always pin the expected algorithm when verifying.
  • Weak secrets: HS256 with a guessable secret can be brute-forced offline. Use 32+ random bytes.
  • No expiry: tokens without exp are forever. Always set one.
  • Trusting decode without verify: decoding is not verification; anyone can forge a decoded payload. Verify the signature on every request.
  • Fat tokens: stuffing permissions lists into the payload bloats every request and goes stale; keep claims minimal.

Key Takeaways

  • A JWT is readable-by-anyone claims plus a signature only the server can forge.
  • Verify signature and expiry in middleware; the payload then provides user identity statelessly.
  • Pair short-lived access tokens with revocable, rotating refresh tokens.
  • Pin algorithms, use strong secrets, always set exp, and keep payloads lean.

Next up: API Versioning Strategies, evolving your API without breaking its clients.

JWT Authentication - REST APIs | CodeYourCraft | CodeYourCraft