Senior 7 min · March 06, 2026

JWT Authentication Flow — The 'alg: none' Attack

Random users accessed admin endpoints after an attacker set alg:none and removed signature.

N
Naren Founder & Principal Engineer

20+ years shipping large-scale distributed systems. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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.
✦ Definition~90s read
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.

Imagine you go to a theme park and buy a wristband at the gate.

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.

Plain-English First

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.

JwtExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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);
    }
}
Output
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyLTEyMzQiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3NDQzOTc2MDAsImV4cCI6MTc0NDQwMTIwMH0.abc123signature
Forge Insight:
Type this code yourself rather than copy-pasting. The muscle memory of writing it will help it stick. And never commit secrets — use environment variables or a secret manager like AWS Secrets Manager.
Production Insight
A common production mistake: developers set the signing key as a plain string under 256 bits. HMAC-SHA256 requires keys at least 256 bits long; shorter keys are silently padded, creating a false sense of security.
If you're using RS256, keep the private key off the authentication service's disk — mount it from a secrets volume or fetch from Vault at startup.
Rule: always verify the key length before the first deployment.
Key Takeaway
JWT provides stateless authentication via signed claims.
The signature is the only guarantee of integrity — protect the signing key as a production secret.
Never trust the algorithm from the token header; enforce algorithm in code.
JWT Authentication Flow & 'alg: none' Attack THECODEFORGE.IO JWT Authentication Flow & 'alg: none' Attack Token creation, verification, and critical vulnerability in JWT Token Creation (Signing) Server signs header+payload with secret/private key Token Sent to Client Client stores JWT (localStorage/cookie) Token Verification (Stateless) Server verifies signature using public key/secret 'alg: none' Attack Attacker sets alg to 'none', bypasses signature check Secure Implementation Reject 'none' algorithm, use asymmetric keys ⚠ Never trust 'alg: none' — always validate algorithm Use a whitelist of allowed algorithms (e.g., RS256, HS256) THECODEFORGE.IO
thecodeforge.io
JWT Authentication Flow & 'alg: none' Attack
Jwt Authentication Flow

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.

JwtSigner.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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;
        }
    }
}
Output
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...signature
Mental Model: The Wax Seal
  • 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.
Production Insight
Key rotation is a nightmare if you hardcode HMAC secrets in every service. With RS256, simply publish the new public key to your JWKS endpoint and wait for the old tokens to expire.
If you rotate an HMAC secret, all existing tokens become invalid immediately — users forced to re-login.
Rule: for multi-service architectures, choose asymmetric signing (RS256 or ES256) from day one.
Key Takeaway
Use asymmetric keys (RS256/ES256) when multiple services verify tokens.
Symmetric keys (HS256) are fine for a single server or trusted internal network.
Never sign tokens with a weak algorithm — enforce a minimum key length.

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.

JwtVerifier.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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);
    }
}
Output
user-1234 (subject) from verified token with role=admin
Watch Out: Clock Skew
If the issuing server and verifying server run on different system clocks, the exp claim might be interpreted differently. Set a small clock skew tolerance (e.g., 30 seconds) to avoid rejecting legitimate tokens. But don't set it too high — attackers can replay a stolen token for that window.
Production Insight
Stateless verification fails silently: a missing public key or wrong kid points to a non-existent key. The token is rejected without a clear error, and the developer often blames the token instead of the key source.
Use a dedicated health check endpoint that returns the current JWKS keyset id and verify this matches the token's kid.
Rule: log the kid from the token and the kid from the JWKS response on every verification failure — catch key mismatches immediately.
Key Takeaway
Stateless verification trades revocation granularity for speed and scale.
Always enforce algorithm whitelist and clock skew tolerance.
Log key mismatches explicitly — they are the most common cause of verification failures.

Common JWT Vulnerabilities and How to Avoid Them

JWTs are secure only if implemented correctly. Top vulnerabilities:

  1. 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.
  2. 'none' Algorithm: Some libraries accept 'alg: none' and skip verification. Fix: reject tokens where the algorithm is 'none' or not in the whitelist.
  3. 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.
  4. 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).
  5. 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.
SecureJwtConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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()
        );
    }
}
Output
Token accepted only if algorithm matches RS256 and signature verifies
Critical: Algorithm Confusion Attack
If your verification code uses the public key as the HMAC key (because you pass it as a string to the parser), an attacker changes the header to HS256 and signs the token using that same public key (which is public!). Your server then accepts it. Always set a whitelist of allowed algorithms in the parser configuration.
Production Insight
The 'kty' (key type) in JWKS is often ignored. If your JWKS contains an RSA key (kty:'RSA') but the header says 'alg:HS256', some libraries use the RSA public key bytes as the HMAC secret — still exploitable.
Always validate the kty matches the expected algorithm.
Rule: if you use RS256, ensure your JWKS only contains RSA keys and verify the header's algorithm matches.
Key Takeaway
Whitelist algorithms in the parser — never trust the token's header.
Never store secrets in payload — it's base64, not encrypted.
Short expiry + refresh token rotation is your best defense against token theft.

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.

GatewayFilter.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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);
    }
}
Output
Claims attached to request; downstream services read userId and roles from request attributes
Mental Model: The Passport
  • 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.
Production Insight
Microservice teams often verify the same JWT multiple times (gateway, service A, service B). Each verification costs microseconds, but if you verify at every hop, you add latency. Instead, have the gateway verify the token once, extract the claims, and propagate them via request context (HTTP header) signed by a short-lived internal token.
However, this internal token must be scoped to only the gateway and downstream services, not the user. It's an additional trust boundary.
Rule: if all services run in a trusted network (e.g., Kubernetes cluster with mTLS), you can skip re-verification in downstream services and trust the claims set by the gateway.
Key Takeaway
JWT thrives in microservices because each service verifies independently.
Short expiry + refresh token rotation solves revocation.
Gateway-level verification with claim propagation avoids redundant crypto and reduces latency.

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.

tokenVerifier.jsNODE.JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge
const jwt = require('jsonwebtoken');
const fs = require('fs');

// In production, load from a secure vault, not the filesystem
const PUBLIC_KEY = fs.readFileSync('./keys/public.pem', 'utf8');

function verifyJwt(token) {
  try {
    const payload = jwt.verify(token, PUBLIC_KEY, {
      algorithms: ['RS256']
    });
    return { valid: true, payload };
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return { valid: false, reason: 'expired' };
    }
    if (err.name === 'JsonWebTokenError') {
      return { valid: false, reason: 'malformed_or_invalid_signature' };
    }
    return { valid: false, reason: 'unknown' };
  }
}

// Usage: every microservice calls this on incoming requests
console.log(verifyJwt('some.jwt.here'));
Output
{
"valid": true,
"payload": { "sub": "user_123", "role": "admin", "iat": 1711850000 }
}
Production Trap:
Never use the same symmetric secret for multiple services. If you must use HS256, rotate the secret weekly and store it in a vault. Asymmetric keys eliminate the shared secret problem entirely.
Key Takeaway
Always use asymmetric signing (RS256/ES256) for multi-service architectures. Symmetric signing is a single point of failure.

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.

authMiddleware.jsNODE.JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// io.thecodeforge
const jwt = require('jsonwebtoken');

// PUBLIC_KEY loaded from environment or vault
const PUBLIC_KEY = process.env.JWT_PUBLIC_KEY;

function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or malformed Authorization header' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] });

    // Only trust the subject (user ID) from the token
    req.userId = decoded.sub;

    // Never trust roles from the token if they can change at runtime
    // Look up roles from your database here if needed
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired. Please refresh.' });
    }
    return res.status(403).json({ error: 'Invalid token.' });
  }
}

module.exports = authMiddleware;
Output
200 OK (on success)
401 Unauthorized (missing or expired token)
403 Forbidden (invalid signature)
Production Trap:
Set 'exp' to 15 minutes. Use a refresh token with a longer lifespan saved in an HTTP-only, Secure, SameSite=Strict cookie. Rotate refresh tokens on each use to limit damage if stolen.
Key Takeaway
Keep access tokens short-lived (15 minutes). Never embed mutable authorization data in the payload. Use the 'sub' claim as a stable user identifier.
● Production incidentPOST-MORTEMseverity: high

The Alg None Attack That Compromised 20+ Services

Symptom
Support tickets reported random users accessing admin endpoints without valid credentials.
Assumption
The team assumed the JWT library defaulted to rejecting unsigned tokens.
Root cause
The Node.js jsonwebtoken library (pre-v9) accepted 'none' algorithm when the public key was passed as a string. The attacker modified the JWT header to 'alg: none' and stripped the signature. The server parsed the token and trusted it.
Fix
Upgraded to jsonwebtoken v9 which rejects 'none' by default. Added explicit algorithm whitelist: { algorithms: ['RS256'] } in verification options. Conducted a token replay audit.
Key lesson
  • 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.
Production debug guideSymptom → Action guide for the most common JWT issues4 entries
Symptom · 01
API returns 401 on every request right after login
Fix
Check token expiry (exp claim). If it's in the past, the token is already expired — regenerate with a longer iat-to-exp window.
Symptom · 02
Signature mismatch errors (InvalidSignatureError)
Fix
Verify the signing key: HS256 uses the same secret on issuer and consumer; RS256 uses private key to sign and public key to verify. A trailing newline or wrong encoding breaks it.
Symptom · 03
Token works on local dev but fails in staging
Fix
Check environment variables for the signing secret — often missing or different in staging. Use a secret manager, not hardcoded values.
Symptom · 04
Intermittent 401 after token refresh
Fix
Clock skew between servers. JWT libraries have a clockTolerance option (default 30s). If multiple services verify the same token with different system clocks, allow 60s tolerance.
★ JWT Debugging Cheat SheetCommands and checks to diagnose JWT issues in Java/Node.js environments
Token is rejected with 'exp' claim validation
Immediate action
Decode token (base64url) and check the exp timestamp against current server time.
Commands
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)")
Fix now
If token is expired, re-login. If exp is missing, add 'exp' claim with a 15-minute window from iat.
Invalid signature error+
Immediate action
Verify you're using the correct key (HS256 secret or RS256 public key).
Commands
jwt-cli decode --json payload token | grep alg
jwt-cli verify token --key public.pem --alg RS256
Fix now
If alg is HS256 but you only have RSA keys, change algorithm. If key mismatch, re-export the correct secret via environment.
Token works in one service but not another+
Immediate action
Check that both services use the same jwks_uri or share the same signing key.
Commands
curl -s http://auth-service/.well-known/jwks.json | python3 -m json.tool
openssl x509 -pubkey -noout -in cert.pem | openssl rsa -pubin -outform DER | sha256sum
Fix now
Ensure all verifying services fetch the same JWKS endpoint or hardcode the public key consistently.
Token contains 'kid' header but verification fails+
Immediate action
Check if the kid refers to an active key in the JWKS set — may be missing or rotated.
Commands
python3 -c "import jwt; print(jwt.get_unverified_header(token)['kid'])"
curl -s http://auth-service/.well-known/jwks.json | jq '.keys[].kid'
Fix now
If kid is not in JWKS, the key was rotated. Request a new token or update the JWKS cache.
JWT Authentication vs Session-Based Authentication
AspectJWT (Stateless)Session (Stateful)
StorageClient stores token (localStorage / cookie)Server stores session in database / Redis
VerificationCryptographic signature (microseconds)Database lookup on every request (1–10ms)
RevocationMust wait for token expiry or maintain a blacklistImmediate: delete session from store
ScalabilityNo shared session store; each server verifies independentlyRequires shared session store (Redis) or sticky sessions
SecurityCompromised token valid until expiry; payload visible (not encrypted)Session ID is opaque; no payload exposure; immediate revocation
Best forMicroservices, mobile APIs, cross-domain authMonolith, server-rendered web apps, low-latency tolerance

Key takeaways

1
JWT enables stateless authentication
the server verifies a signed token without a database lookup.
2
The signature is the only guarantee of integrity
protect the signing key at all costs.
3
Short-lived access tokens (15 min) plus refresh token rotation solve the revocation gap.
4
Always whitelist algorithms in the parser
never trust the token's header.
5
Never store secrets in the JWT payload
it's base64, not encrypted.
6
Use asymmetric signing (RS256) in multi-service architectures for safe key distribution and rotation.

Common mistakes to avoid

5 patterns
×

Using a weak HMAC secret (less than 256 bits)

Symptom
Random users appear to have valid tokens; a brute-force attack recovers the secret.
Fix
Generate a cryptographically random key of at least 256 bits (e.g., using openssl rand -base64 32). Store in environment variable or secret manager.
×

Accepting the 'none' algorithm

Symptom
An attacker sends a JWT with alg: 'none' and empty signature — server grants access.
Fix
Explicitly whitelist allowed algorithms in the parser. In jjwt: Jwts.parserBuilder().requireAlg("RS256").... In jsonwebtoken (Node.js): { algorithms: ['RS256'] }.
×

Storing sensitive data in the JWT payload

Symptom
A data breach results in exposure of user PII or secrets because the payload is base64, not encrypted.
Fix
Never put passwords, credit card numbers, or other secrets in the JWT. If needed, use JWE (encrypted JWT) or store a reference ID in the payload and look up the data server-side.
×

Not verifying the token's issuer (iss) or audience (aud)

Symptom
A token from a different auth service (e.g., a web login token) is accepted by an API that expects mobile-only tokens.
Fix
Validate the 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

Symptom
After user logout, the token remains valid for hours — attacker with stolen token can impersonate.
Fix
Use short-lived access tokens (15 min) with a refresh token that can be revoked server-side. Implement refresh token rotation: issue a new refresh token on each refresh, invalidating the old one.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Explain the structure of a JWT. What are the three parts and how are the...
Q02SENIOR
What is the difference between HS256 and RS256? When would you choose on...
Q03SENIOR
How do you handle token revocation in a stateless JWT architecture?
Q04SENIOR
Explain the algorithm confusion vulnerability and how to prevent it.
Q01 of 04JUNIOR

Explain the structure of a JWT. What are the three parts and how are they composed?

ANSWER
A JWT consists of three base64url-encoded parts separated by dots: header, payload, signature. Header contains the signing algorithm (e.g. HS256, RS256) and token type (JWT). Payload contains registered claims (iss, sub, exp, iat, jti) and custom claims (e.g., roles, permissions). Signature is computed by combining the header and payload with a secret (HMAC) or private key (RSA/ECDSA) using the algorithm specified in the header. The signature ensures the token has not been tampered with. Example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjkr5pem9Bp8NVNw9gC.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is a JWT and how does it work?
02
Is JWT secure?
03
What is the difference between JWT and OAuth2?
04
How do I invalidate a JWT before it expires?
05
Should I store JWT in localStorage or cookies?
N
Naren Founder & Principal Engineer

20+ years shipping large-scale distributed systems. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's Security. Mark it forged?

7 min read · try the examples if you haven't

Previous
OAuth 2.0 and OpenID Connect
2 / 10 · Security
Next
HTTPS and TLS Explained