JWT Authentication Explained —
How the Full Flow Works
From login to token issuance, request verification, refresh tokens, and the failure modes most tutorials skip.
JWT authentication explained: a JWT replaces the server's need to look up a session in a database on every request — the token itself contains the user's identity and permissions, signed by the server so it can't be tampered with, which is why understanding the full issuance and verification flow is essential before trusting any library's "just works" abstraction.
The Full JWT Authentication Flow
The complete flow has eight steps. Each one matters — skipping the mental model of any step is how subtle security bugs sneak in.
POST /auth/login with { "email": "...", "password": "..." } in the request body.{ sub, email, role, iat, exp }. This is the only step that touches the user database.httpOnly cookie (safest against XSS) or in memory (safest against CSRF). Storing tokens in localStorage is common but exposes them to any JavaScript running on the page.Authorization header: Authorization: Bearer <token>.exp claim (a Unix timestamp). If the current time is past exp, the request is rejected with 401 Unauthorized regardless of a valid signature.sub to identify the user, role for access control — to decide what the request is allowed to do and fulfils it accordingly.What's Inside the JWT
Every JWT is three Base64Url-encoded segments separated by dots. The middle segment — the payload — contains all the claims the server issued. It is not encrypted: anyone who holds the token can read its contents.
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "user_123",
"email": "alice@example.com",
"role": "admin",
"iat": 1716239022,
"exp": 1716242622
}
Important: the payload above is Base64Url-encoded in the wire format — not encrypted. Anyone who intercepts the token can read email, role, and every other claim without the signing secret. Never store passwords, credit card numbers, or any secret value in the payload.
HMAC vs RSA Signing: HS256 vs RS256
The signing algorithm determines how many parties can issue and verify tokens — which matters enormously in distributed systems.
| Property | HS256 (HMAC-SHA256) | RS256 (RSA-SHA256) |
|---|---|---|
| Key type | Single shared secret | Private key (sign) + public key (verify) |
| Who can verify? | Only parties that hold the secret | Anyone with the public key |
| Secret distribution risk | Every verifier needs the secret | Public key is safe to distribute |
| Complexity | Simple — one key to manage | Key pair generation and rotation needed |
| Best for | Single applications, monoliths | Microservices, multi-team systems |
Use HS256 when a single application both issues and verifies tokens — the shared secret never leaves one system. Use RS256 when multiple services need to verify tokens independently: each service holds only the public key, while the private signing key stays exclusively with the authentication service.
Access Tokens + Refresh Tokens — The Standard Pattern
Issuing one long-lived token creates an obvious problem: a stolen token is valid until it expires. The access token / refresh token pattern is the standard solution.
- Lifetime: short — typically 15 minutes
- Sent on: every API request
- Storage: memory or httpOnly cookie
- Purpose: proves identity to the API
- Lifetime: long — 7 to 30 days
- Sent on: only to
POST /auth/refresh - Storage: httpOnly cookie only
- Purpose: obtain a new access token
The refresh flow: when the access token expires, the client silently sends the refresh token to /auth/refresh. If the refresh token is valid and not revoked, the server issues a new access token — and optionally rotates the refresh token too. The user never sees a logout screen.
The security benefit is about blast radius. A stolen access token is valid for at most 15 minutes. A stolen refresh token is serious — but refresh tokens can be stored in httpOnly cookies, making them inaccessible to JavaScript and therefore to XSS attacks that target localStorage.
What Can Go Wrong — Security Failure Modes
The JWT spec allows an algorithm value of none, meaning no signature. Some early libraries accepted this and verified any token as valid. Always explicitly specify the allowed algorithm(s) when calling your verify function — never accept whatever the token header claims.
HMAC-SHA256 can be brute-forced if the secret is short or guessable. An attacker who obtains a token can mount an offline dictionary attack against the signature. Use a randomly generated secret of at least 256 bits.
Setting exp to 24 hours or longer on access tokens eliminates the main security benefit of short-lived tokens. A leaked access token remains valid for a full day. Keep access token lifetimes at 15 minutes and use refresh tokens for persistence.
The JWT payload is Base64Url-encoded, not encrypted. Anyone who intercepts the token — or reads it from localStorage, a log file, or a URL parameter — can read every claim. Never put passwords, raw PII beyond what is absolutely necessary, or secrets of any kind in the payload.
Some JWT libraries have configuration modes or historical bugs that skip signature verification under certain conditions (e.g. when the token has no kid header). Always pin the allowed algorithm explicitly and test that tampered tokens are rejected.
Verification Code Examples
Both examples explicitly specify the allowed algorithm. This is the single most important implementation detail — never let the token header dictate the algorithm.
const jwt = require('jsonwebtoken'); // Always specify algorithms — never omit this option function verifyToken(token) { try { const payload = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'], // explicit allowlist }); return { ok: true, payload }; } catch (err) { return { ok: false, error: err.message }; } } // Usage in Express middleware function authMiddleware(req, res, next) { const header = req.headers['authorization'] ?? ''; const token = header.replace(/^Bearer\s+/i, ''); const { ok, payload, error } = verifyToken(token); if (!ok) return res.status(401).json({ error }); req.user = payload; next(); }
import jwt import os from datetime import datetime, timezone SECRET = os.environ["JWT_SECRET"] def verify_token(token: str) -> dict: # Always pass algorithms= explicitly — PyJWT will raise # DecodeError if the token uses a different algorithm payload = jwt.decode( token, SECRET, algorithms=["HS256"], # explicit allowlist ) return payload # raises on invalid sig or expired exp # Example — FastAPI dependency from fastapi import Depends, HTTPException, Header def current_user(authorization: str = Header(...)): scheme, _, token = authorization.partition(" ") if scheme.lower() != "bearer": raise HTTPException(status_code=401) try: return verify_token(token) except jwt.PyJWTError as exc: raise HTTPException(status_code=401, detail=str(exc))
Inspect a JWT Token Instantly
Paste any JWT into the free decoder to see the header, payload claims, and expiry time — fully client-side, no server requests, no third-party scripts.
Open Free JWT Decoder →FAQ
What is JWT authentication? +
JWT authentication is a stateless mechanism where the server issues a signed token containing the user's identity and permissions after login. On every subsequent request the client sends that token and the server verifies the signature cryptographically — no database session lookup required. The server trusts the claims in the token because only it could have produced a valid signature with the signing secret.
What is the difference between an access token and a refresh token? +
An access token is short-lived (typically 15 minutes) and sent with every API request. A refresh token is long-lived (7–30 days), stored securely, and used only to obtain new access tokens when the current one expires. This limits the damage from a stolen access token — it expires quickly — while keeping the user logged in through the longer-lived refresh token.
What is the difference between HS256 and RS256? +
HS256 uses a single shared secret to both sign and verify tokens — every service that needs to verify must hold that secret. RS256 uses a private key to sign and a public key to verify — microservices can verify tokens with only the public key, never needing access to the private signing key. Use HS256 for single applications and RS256 for distributed or microservice architectures.
How do you revoke a JWT? +
JWTs are stateless by design, so there is no built-in revocation. Common strategies: keep access tokens short-lived (15 minutes) so they expire quickly on their own; maintain a server-side blocklist of revoked token JTI (JWT ID) values for immediate revocation when needed; rotate the signing secret to invalidate all outstanding tokens at once; and invalidate refresh tokens to prevent new access tokens from being issued to a compromised session.