JWT Authentication Flow — The 'alg: none' Attack
Random users accessed admin endpoints after an attacker set alg:none and removed signature.
20+ years shipping large-scale distributed systems. Notes here come from systems that actually shipped.
- 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.
Imagine you go to a theme park and buy a wristband at the gate. Every ride operator can look at your wristband and immediately know you've paid — they don't need to call the front gate to check. JWTs work exactly like that wristband: the server hands you a token when you log in, and every future request you carry contains that token so the server can trust you instantly, without looking you up in a database each time. The token itself contains your identity, and it's tamper-proof because it's been cryptographically signed by the server that issued it.
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.
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.
- 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.
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.
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.
- 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.
Session Tokens vs. JWT: Why You Reach for Asymmetric Keys
You've heard the debate. Sessions live on the server. JWTs live on the client. The real difference is failure mode. Session tokens require a database lookup every request. That lookup adds latency and couples your auth to a centralized store. For a single web app, that's fine. For a microservice mesh, it's a nightmare. JWT flips the model: the server signs a token with a private key, and any service can verify it with a public key. No shared database, no synchronous lookups. The price? You can't revoke a JWT without a deny list. That's why you choose asymmetric signing (RS256 or ES256). Symmetric signing (HS256) forces every service to hold the same secret—one leak and your entire system is compromised. Asymmetric keys let you distribute trust. Your auth service holds the private key. Every other service only needs the public key. If one service gets pwned, the attacker still can't mint new tokens. This is not academic. I've cleaned up after teams that used HS256 across six services. Don't repeat that mistake.
Implementation in Node.js with Express: Where Most Teams Bleed
The code is the easy part. The mistakes are in the flow. Here's what a production JWT authentication middleware looks like. Notice what is missing: no database calls, no session stores. You check the token, verify the signature, and extract the user ID from the 'sub' claim. That's it. But here's the trap: you must set an 'exp' claim with a short expiration (15 minutes for access tokens, 7 days for refresh tokens). Do not set 'exp' to a year. I've seen tokens with a 5-year expiration in a SaaS product. The engineer thought it was 'convenient' for users. The attacker who stole that token thought it was 'convenient' too. The second trap: never trust the token's 'role' or 'isAdmin' claim without also verifying it against a backend store if the role changes at runtime. Otherwise, a user can change their role in the JWT if they forge the signature. Use 'sub' and look up roles per request if you need real-time authorization. The code below shows a secure, minimal Express middleware that follows this pattern. One job. One validation. No surprises.
The Alg None Attack That Compromised 20+ Services
- Never trust the JWT header to specify which algorithm to use — your code must enforce it.
- The simplest attacks exploit what the spec allows but your security posture prohibits.
- Third-party library defaults are not security promises; read the changelog for every major version.
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)")Key takeaways
Common mistakes to avoid
5 patternsUsing a weak HMAC secret (less than 256 bits)
openssl rand -base64 32). Store in environment variable or secret manager.Accepting the 'none' algorithm
Jwts.parserBuilder().requireAlg("RS256").... In jsonwebtoken (Node.js): { algorithms: ['RS256'] }.Storing sensitive data in the JWT payload
Not verifying the token's issuer (iss) or audience (aud)
iss and aud claims match your expected values. In jjwt: parserBuilder().requireIssuer("auth.myapp.com").requireAudience("api").Using JWT as a session replacement without thinking about revocation
Interview Questions on This Topic
Explain the structure of a JWT. What are the three parts and how are they composed?
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjkr5pem9Bp8NVNw9gC.Frequently Asked Questions
20+ years shipping large-scale distributed systems. Notes here come from systems that actually shipped.
That's Security. Mark it forged?
7 min read · try the examples if you haven't