Skip to main content
JWT Guide

JWT Authentication Explained —
How the Full Flow Works

From login to token issuance, request verification, refresh tokens, and the failure modes most tutorials skip.

10 min read·Updated May 2026

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.

1
User logs in
The client sends credentials to the server — typically POST /auth/login with { "email": "...", "password": "..." } in the request body.
2
Server validates credentials
The server looks up the user record, verifies the password hash, and — if valid — assembles the JWT payload: { sub, email, role, iat, exp }. This is the only step that touches the user database.
3
Server signs the token
The server encodes the header and payload as Base64Url strings, concatenates them, and signs the result with a secret (HMAC-SHA256 / HS256) or a private key (RS256). The three parts — header, payload, signature — are joined with dots to produce the final JWT string.
4
Client receives and stores the JWT
The server returns the JWT in the response. The client stores it — in an 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.
5
Client sends the JWT on every request
For each subsequent API call the client attaches the token in the Authorization header: Authorization: Bearer <token>.
6
Server verifies the signature
The server splits the token, recomputes the expected signature from the header and payload, and compares it to the received signature. If they match, the payload has not been tampered with. No database lookup — the verification is purely cryptographic.
7
Server checks the expiry claim
After verifying the signature, the server reads the exp claim (a Unix timestamp). If the current time is past exp, the request is rejected with 401 Unauthorized regardless of a valid signature.
8
Server processes the request using claims
The server uses the claims in the payload — 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.

Header (decoded)
{
  "alg": "HS256",
  "typ": "JWT"
}
Payload (decoded)
{
  "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.

Access Token
  • Lifetime: short — typically 15 minutes
  • Sent on: every API request
  • Storage: memory or httpOnly cookie
  • Purpose: proves identity to the API
Refresh Token
  • 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

Critical alg:none attack

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.

High risk Weak HS256 secret

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.

High risk Long expiry on access tokens

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.

Medium risk Sensitive data in the payload

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.

High risk Libraries that skip signature verification

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.

Node.js — jsonwebtoken server.js
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();
}
Python — PyJWT auth.py
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.

Related