Skip to content
Home System Design JWT Authentication Flow — The 'alg: none' Attack

JWT Authentication Flow — The 'alg: none' Attack

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Security → Topic 2 of 10
Random users accessed admin endpoints after an attacker set alg:none and removed signature.
⚙️ Intermediate — basic System Design knowledge assumed
In this tutorial, you'll learn
Random users accessed admin endpoints after an attacker set alg:none and removed signature.
  • 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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
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.
🚨 START HERE

JWT Debugging Cheat Sheet

Commands and checks to diagnose JWT issues in Java/Node.js environments
🟡

Token is rejected with 'exp' claim validation

Immediate ActionDecode 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 NowIf token is expired, re-login. If exp is missing, add 'exp' claim with a 15-minute window from iat.
🟡

Invalid signature error

Immediate ActionVerify 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 NowIf 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 ActionCheck 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 NowEnsure all verifying services fetch the same JWKS endpoint or hardcode the public key consistently.
🟡

Token contains 'kid' header but verification fails

Immediate ActionCheck 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 NowIf kid is not in JWKS, the key was rotated. Request a new token or update the JWKS cache.
Production Incident

The Alg None Attack That Compromised 20+ Services

A production authentication service accepted tokens with alg: 'none', bypassing all signature checks.
SymptomSupport tickets reported random users accessing admin endpoints without valid credentials.
AssumptionThe team assumed the JWT library defaulted to rejecting unsigned tokens.
Root causeThe 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.
FixUpgraded 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 Guide

Symptom → Action guide for the most common JWT issues

API returns 401 on every request right after loginCheck token expiry (exp claim). If it's in the past, the token is already expired — regenerate with a longer iat-to-exp window.
Signature mismatch errors (InvalidSignatureError)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.
Token works on local dev but fails in stagingCheck environment variables for the signing secret — often missing or different in staging. Use a secret manager, not hardcoded values.
Intermittent 401 after token refreshClock 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.

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.java · JAVA
1234567891011121314151617181920212223242526272829303132
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.

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.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839
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
Mental Model: The Wax Seal
Think of the private key as a unique seal ring, the public key as a glass display of that seal's impression.
  • 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.java · JAVA
123456789101112131415161718192021222324252627282930
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.java · JAVA
12345678910111213141516171819202122232425262728293031
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.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637
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
Mental Model: The Passport
A JWT is like an international passport — issued by one country's authority, verifiable by any border control that trusts that authority.
  • 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.
🗂 JWT Authentication vs Session-Based Authentication
Key differences and when to choose each
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

  • 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

    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 Questions on This Topic

  • QExplain the structure of a JWT. What are the three parts and how are they composed?JuniorReveal
    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.
  • QWhat is the difference between HS256 and RS256? When would you choose one over the other?Mid-levelReveal
    HS256 (HMAC with SHA-256) is a symmetric algorithm — the same secret key is used for both signing and verification. Fast, simple, but requires sharing the secret between issuer and verifier. RS256 (RSA with SHA-256) is asymmetric — uses a private key to sign and a public key to verify. The public key can be safely distributed to any verifier. Choose HS256 when only one server or a tightly controlled network issues and verifies tokens (e.g., a single monolithic application). Choose RS256 when multiple services need to verify tokens without sharing a secret (e.g., microservices, third-party APIs). RS256 also simplifies key rotation: you can replace the private key without updating every verifier's config.
  • QHow do you handle token revocation in a stateless JWT architecture?SeniorReveal
    You cannot revoke a stateless JWT without introducing state. The standard approach is to use short-lived access tokens (15 minutes) combined with a long-lived refresh token stored on the server (or in a database). When the user logs out, you revoke the refresh token. The access token expires naturally. For immediate revocation needs, you can maintain a blacklist of token IDs (jti) in Redis — but this adds latency and defeats statelessness. Better: accept that you lose immediate revocation and rely on short expiry. Attackers exploit stolen tokens within the window — minimize that window. Also implement refresh token rotation: each time a refresh token is used, issue a new refresh token and invalidate the old one. This limits the impact of a leaked refresh token.
  • QExplain the algorithm confusion vulnerability and how to prevent it.SeniorReveal
    The algorithm confusion vulnerability occurs when a server accepts a token with the algorithm set by the attacker, then uses a public key (suitable for RS256) as the HMAC secret for HS256. Since the public key is public, the attacker can create a valid HS256 token using that public key as the secret. The server verifies it and accepts. Prevention: never base your verification key on the token's header. Always whitelist accepted algorithms in the parser configuration. For example, in jjwt: parserBuilder().requireAlg("RS256"). In Node.js jsonwebtoken: pass { algorithms: ['RS256'] } to the verify call. Also ensure your JWKS endpoint only returns keys of the expected type (e.g., only RSA keys if you use RS256).

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.

🔥
Naren Founder & Author

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.

← PreviousOAuth 2.0 and OpenID ConnectNext →HTTPS and TLS Explained
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged