JWT Authentication Flow — The 'alg: none' Attack
- JWT enables stateless authentication: the server verifies a signed token without a database lookup.
- The signature is the only guarantee of integrity — protect the signing key at all costs.
- Short-lived access tokens (15 min) plus refresh token rotation solve the revocation gap.
- JWT is a signed token containing user claims, issued after login.
- Stateless: server verifies without a session database call.
- Structure: header (alg, typ), payload (claims), signature (HMAC or RSA).
- Verification cost: ~5–20 microseconds per token — database roundtrips are 1000x slower.
- Biggest mistake: accepting "none" algorithm — grants full access with no signature.
- Production insight: revocation requires a blacklist or short expiry — no server-side invalidation by default.
JWT Debugging Cheat Sheet
Token is rejected with 'exp' claim validation
echo 'header.payload' | cut -d. -f2 | base64 -d 2>/dev/null || python3 -c "import sys, json, base64; print(json.loads(base64.urlsafe_b64decode(sys.argv[1]+'==')))"date -d @$(python3 -c "print(exp_value)")Invalid signature error
jwt-cli decode --json payload token | grep algjwt-cli verify token --key public.pem --alg RS256Token works in one service but not another
curl -s http://auth-service/.well-known/jwks.json | python3 -m json.toolopenssl x509 -pubkey -noout -in cert.pem | openssl rsa -pubin -outform DER | sha256sumToken contains 'kid' header but verification fails
python3 -c "import jwt; print(jwt.get_unverified_header(token)['kid'])"curl -s http://auth-service/.well-known/jwks.json | jq '.keys[].kid'Production Incident
Production Debug GuideSymptom → Action guide for the most common JWT issues
Every modern web application needs to answer one question on every single request: 'Do I know this person, and are they allowed to do this?' The naive answer is to store a session in a database and look it up on every request. That works fine for a single server handling a few hundred users — but the moment you scale horizontally, add microservices, or need a mobile app talking to multiple APIs, that session-database approach becomes a bottleneck and an architectural headache.
JWT — JSON Web Token — was designed to solve exactly this problem. Instead of storing state on the server, you encode the user's identity and permissions directly into a signed token and hand it to the client. The client sends it back with every request, and the server can verify it cryptographically in microseconds without touching a database. The server went from being a stateful gatekeeper to a stateless verifier. That shift has enormous implications for scalability, microservices architecture, and cross-domain authentication.
By the end of this article you'll understand exactly how a JWT is structured, how the full login-to-protected-request flow works under the hood, why the signature makes it tamper-proof, how to implement it correctly in Java, and — critically — the mistakes that create real security vulnerabilities even when the basic flow looks right. Whether you're building your first authenticated API or preparing for a system design interview, you'll walk away with a complete mental model of JWT authentication.
What is JWT Authentication Flow?
JWT (JSON Web Token) authentication is a stateless authentication mechanism. When a user logs in, the server creates a signed JSON token containing the user's identity and permissions. The client stores it (typically in localStorage or an HTTP-only cookie) and sends it with every subsequent request via the Authorization header. The server verifies the signature — no database lookup needed. The flow is: Login → Server issues JWT → Client stores JWT → Client sends JWT in request header → Server verifies JWT → Server grants or denies access.
JWTs are base64url-encoded and signed, not encrypted by default. That means anyone with the token can read the payload — so you never put secrets like passwords inside. What makes it trustable is the cryptographic signature appended to the payload. If the payload is tampered with, the signature verification fails.
package io.thecodeforge.auth; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import javax.crypto.SecretKey; import java.util.Date; public class JwtExample { // Never hardcode secrets in production; use environment variables or a vault private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor( "my-super-secret-key-at-least-256-bits-long-for-hs256".getBytes() ); public static String createToken(String userId, String role, long ttlMillis) { Date now = new Date(); Date expiration = new Date(now.getTime() + ttlMillis); return Jwts.builder() .setSubject(userId) .claim("role", role) .setIssuedAt(now) .setExpiration(expiration) .signWith(SECRET_KEY, SignatureAlgorithm.HS256) .compact(); } public static void main(String[] args) { String token = createToken("user-1234", "admin", 3600000); System.out.println(token); } }
Token Creation: Signing the Right Way
Creating a JWT securely involves three artifacts: the header, the payload, and the signature. The header typically contains the algorithm (HS256, RS256) and token type (JWT). The payload contains registered claims (iss, sub, exp, iat, jti) and custom claims like roles. The signature is computed by combining the base64url-encoded header and payload with a secret (HMAC) or private key (RSA/ECDSA).
Two broad signing categories: symmetric (HS256) and asymmetric (RS256, ES256). Symmetric uses the same key for signing and verification — fast but requires sharing the secret between issuer and verifier. Asymmetric uses a private key to sign and a public key to verify — you can safely distribute the public key to any verifying service. In production, asymmetric is preferred because you can rotate the private key without touching every verifier.
The most common mistake: using HS256 when the verifying party should not have the signing secret. For example, a mobile app that verifies its own JWT should never hold the HMAC secret — attackers reverse-engineer the app.
package io.thecodeforge.auth; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Date; public class JwtSigner { // Generate a keypair for RS256 — do this once and reuse private static final KeyPair KP = Keys.keyPairFor(SignatureAlgorithm.RS256); private static final PrivateKey PRIVATE = KP.getPrivate(); private static final PublicKey PUBLIC = KP.getPublic(); public static String createToken(String userId, long ttl) { return Jwts.builder() .setSubject(userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + ttl)) .setIssuer("auth.io.thecodeforge.com") .signWith(PRIVATE, SignatureAlgorithm.RS256) .compact(); } public static boolean verifyToken(String token) { try { Jwts.parserBuilder() .setSigningKey(PUBLIC) .build() .parseClaimsJws(token); return true; } catch (Exception e) { return false; } } }
- The ring (private key) stamps an impression (signature) onto the document (token content).
- Anyone with the glass display (public key) can verify the impression matches the ring — they can't forge a new document using only the display.
- HS256 is like sharing the seal ring itself — you must trust everyone who holds it not to impersonate you.
- In production microservice environments, asymmetric keys let you revoke an issuer's ring (private key) without re-distributing secrets to verifiers.
How Verification Works without State
Stateless verification is the killer feature of JWT. The verifying server does the following: parse the token, decode the header, check the algorithm against a whitelist, decode the payload (but don't trust it yet), recompute the signature using the expected key, and compare with the provided signature. If they match, you trust the payload claims. If not, reject.
Stateless means no database call for each request. That's a massive win for latency and scalability. However, you lose the ability to force-logout a user — a compromised token remains valid until expiry. To mitigate, use short-lived tokens (15 minutes) combined with a refresh token mechanism. The refresh token is stored server-side (or in a database) and can be revoked.
Stateless verification also requires the verifying service to possess the correct key at all times. With RS256 and JWKS endpoint, you can fetch the public key on startup and cache it. If the key rotates, the token signed with the old key stays valid until its expiry — no interruption.
package io.thecodeforge.auth; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Claims; import java.security.PublicKey; public class JwtVerifier { private final PublicKey publicKey; public JwtVerifier(PublicKey publicKey) { this.publicKey = publicKey; } public Claims verify(String token) { return Jwts.parserBuilder() .setSigningKey(publicKey) .build() .parseClaimsJws(token) .getBody(); } public String extractUserId(String token) { return verify(token).getSubject(); } public boolean hasRole(String token, String role) { return verify(token).get("role", String.class).equals(role); } }
Common JWT Vulnerabilities and How to Avoid Them
JWTs are secure only if implemented correctly. Top vulnerabilities:
- Algorithm Confusion: The attacker changes 'alg' from 'RS256' to 'HS256'. If your server uses the public key as the HMAC secret (which is a string), it will verify the token using the public key as the HMAC key — the attacker can sign tokens with the public key. Fix: never derive the verification key from the token; always hardcode the expected algorithm.
- 'none' Algorithm: Some libraries accept 'alg: none' and skip verification. Fix: reject tokens where the algorithm is 'none' or not in the whitelist.
- Weak Secret: HS256 with a short or predictable secret. Fix: use a key of at least 256 bits generated from a cryptographically secure random source.
- Token Replay: A stolen token can be used until expiry. Fix: use short-lived access tokens (15 min) and implement refresh token rotation (issue a new refresh token each time, invalidate the old one).
- Payload Secrets: Developers put passwords or credit card numbers in the payload. The payload is base64 encoded, not encrypted. Fix: never store sensitive data in JWT payload. If needed, use JWE (JSON Web Encryption) to encrypt the payload.
package io.thecodeforge.auth; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import java.security.Key; public class SecureJwtConfig { // Whitelist algorithms explicitly private static final String ALLOWED_ALGORITHM = "RS256"; private static final Key PUBLIC_KEY = loadPublicKey(); public boolean verifyTokenSecurely(String token) { try { Jwts.parserBuilder() .requireAlg(ALLOWED_ALGORITHM) // enforce algorithm .setSigningKey(PUBLIC_KEY) .build() .parseClaimsJws(token); return true; } catch (Exception e) { return false; } } private static Key loadPublicKey() { // Load from secure store — never from token header return Keys.hmacShaKeyFor( System.getenv("JWT_SIGNING_SECRET").getBytes() ); } }
JWT in Microservices: Token Propagation and Revocation
In a microservices architecture, JWT shines because each service can independently verify the token without contacting a central auth store. The authentication service issues a token scoped to a user. Downstream services extract the token from the incoming request (often via a gateway) and verify it.
Challenge: revocation. Stateless tokens cannot be invalidated server-side without a blacklist. Best practice: short access token TTL (15 minutes) plus a long-lived refresh token stored in the auth service's database. When the user logs out, you invalidate the refresh token. The access token will expire soon. For immediate revocation, you can maintain a distributed blacklist (Redis) of jti claims, but this reintroduces state.
Another challenge: token size. With multiple claims and signatures, tokens can grow beyond 2KB. In HTTP headers, that's fine. But if you pass tokens via URL query parameters (bad practice), you'll hit length limits.
Token propagation across service boundaries should use the same signed token — never create a new token per service, as that requires each service to trust the other. Use the existing JWT and let each service verify it with the same public key.
package io.thecodeforge.gateway; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import java.security.PublicKey; public class JwtGatewayFilter implements Filter { private final PublicKey publicKey; public JwtGatewayFilter(PublicKey publicKey) { this.publicKey = publicKey; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { HttpServletRequest httpReq = (HttpServletRequest) request; String authHeader = httpReq.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { throw new SecurityException("Missing or invalid Authorization header"); } String token = authHeader.substring(7); Claims claims = Jwts.parserBuilder() .setSigningKey(publicKey) .build() .parseClaimsJws(token) .getBody(); // Attach claims to request context for downstream services httpReq.setAttribute("claims", claims); chain.doFilter(request, response); } }
- The passport contains your identity and photo (claims).
- The passport's anti-forgery features (watermarks, holograms) are like the cryptographic signature.
- Each border control (microservice) verifies the passport without calling the issuing embassy (auth service) — that's stateless verification.
- If the passport is stolen, you must cancel it (revoke the refresh token) — the passport itself cannot be invalidated until expiry.
- You don't put your bank PIN in the passport — similarly, don't put secrets in the JWT payload.
| Aspect | JWT (Stateless) | Session (Stateful) |
|---|---|---|
| Storage | Client stores token (localStorage / cookie) | Server stores session in database / Redis |
| Verification | Cryptographic signature (microseconds) | Database lookup on every request (1–10ms) |
| Revocation | Must wait for token expiry or maintain a blacklist | Immediate: delete session from store |
| Scalability | No shared session store; each server verifies independently | Requires shared session store (Redis) or sticky sessions |
| Security | Compromised token valid until expiry; payload visible (not encrypted) | Session ID is opaque; no payload exposure; immediate revocation |
| Best for | Microservices, mobile APIs, cross-domain auth | Monolith, server-rendered web apps, low-latency tolerance |
🎯 Key Takeaways
- JWT enables stateless authentication: the server verifies a signed token without a database lookup.
- The signature is the only guarantee of integrity — protect the signing key at all costs.
- Short-lived access tokens (15 min) plus refresh token rotation solve the revocation gap.
- Always whitelist algorithms in the parser — never trust the token's header.
- Never store secrets in the JWT payload — it's base64, not encrypted.
- Use asymmetric signing (RS256) in multi-service architectures for safe key distribution and rotation.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QExplain the structure of a JWT. What are the three parts and how are they composed?JuniorReveal
- QWhat is the difference between HS256 and RS256? When would you choose one over the other?Mid-levelReveal
- QHow do you handle token revocation in a stateless JWT architecture?SeniorReveal
- QExplain the algorithm confusion vulnerability and how to prevent it.SeniorReveal
Frequently Asked Questions
What is a JWT and how does it work?
A JSON Web Token (JWT) is a compact, URL-safe token that encodes claims (user identity, permissions, metadata) in a JSON object. It is digitally signed so the receiving party can verify its authenticity. The flow: user logs in → server creates a JWT signed with a secret (or private key) → client stores the JWT → client sends it in the Authorization header with every request → server verifies the signature and extracts claims to authorize the request.
Is JWT secure?
JWT is secure when implemented correctly. The signature prevents tampering. However, the payload is only base64-encoded, not encrypted — anyone with the token can read it. Never put sensitive data (passwords, credit card numbers) in the payload. Also, a compromised signing key or accepting the 'none' algorithm breaks security. Use strong keys, whitelist algorithms, and short-lived tokens.
What is the difference between JWT and OAuth2?
JWT is a token format; OAuth2 is an authorization framework. OAuth2 specifies how to obtain and use tokens for delegated access, often using JWTs as the token format. JWT can be used standalone for authentication (login), while OAuth2 is about scoped access to resources. In practice, many OAuth2 implementations issue JWTs as access tokens.
How do I invalidate a JWT before it expires?
JWTs are stateless — you cannot invalidate them server-side without maintaining a blacklist. Best practice: set short expiry (15 minutes) and use a refresh token stored on the server. Revoke the refresh token on logout. For immediate revocation, maintain a Redis blacklist of token IDs (jti) but this adds state. Accept short expiry as your primary revocation mechanism.
Should I store JWT in localStorage or cookies?
Neither is perfect. localStorage is accessible via JavaScript (XSS vulnerability). Cookies with HttpOnly and Secure flags are safer against XSS but vulnerable to CSRF. If using cookies, ensure CSRF protection. A common production pattern: store the access token in memory (not persistent) and the refresh token in an HttpOnly cookie. That way, XSS cannot steal the access token (since it's in memory not localStorage), and CSRF protects the refresh endpoint.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.