API Security JSON Web Tokens JWT
JSON Web Tokens — commonly called JWTs (pronounced "jot") — are one of the most widely used credentials in modern APIs. When you log in to an application and receive a token that your app includes with every subsequent API call, there is a good chance that token is a JWT.
JWTs are powerful, portable, and efficient. They are also widely misimplemented. Understanding how JWTs work at a technical level is essential for identifying and preventing the vulnerabilities they can introduce.
What Is a JWT
A JWT is a self-contained, digitally signed piece of data. "Self-contained" means the token itself carries all the information needed to verify it and understand what it represents — the server does not need to look up a database to validate a JWT.
Traditional Session Token vs JWT:
Traditional Session:
User logs in.
Server generates random session ID: "abc123xyz"
Server stores: { "abc123xyz": { user_id: 101, role: "admin" } }
Client stores session ID in cookie.
Every request: server looks up "abc123xyz" in database.
→ Requires database lookup on every single request.
JWT:
User logs in.
Server creates JWT containing: { user_id: 101, role: "admin", expires: 1h }
Server signs JWT with a secret key.
Client stores signed JWT.
Every request: server verifies signature mathematically.
→ No database lookup required. Verified with math alone.
This makes JWTs extremely fast and scalable — ideal for microservices where multiple services need to validate the same token without sharing a session database.
JWT Structure
A JWT consists of three parts, separated by dots. Each part is Base64URL-encoded.
JWT Format: HEADER.PAYLOAD.SIGNATURE Example JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJ1c2VyX2lkIjoxMDEsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcwMDAwMDAwMH0 . SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Part 1: The Header
The header describes the type of token and the algorithm used to sign it.
Header (decoded from Base64URL):
{
"alg": "HS256",
"typ": "JWT"
}
"alg": HS256 means HMAC with SHA-256.
Other common values: RS256 (RSA), ES256 (Elliptic Curve), HS512
Security note: The header is attacker-controlled.
An attacker can modify the alg field before sending the token.
The server MUST NOT use the alg value from the header to decide
which algorithm to verify with — that decision must be hardcoded
on the server side.
Part 2: The Payload (Claims)
The payload contains claims — statements about the user and the token itself.
Payload (decoded from Base64URL):
{
"sub": "101", ← Subject (user ID)
"name": "Neha Joshi", ← Custom claim (user's name)
"role": "admin", ← Custom claim (user's role)
"iat": 1699900000, ← Issued At (Unix timestamp)
"exp": 1699903600, ← Expiration (1 hour after iat)
"iss": "api.company.com", ← Issuer (who created the token)
"aud": "api.company.com" ← Audience (who should accept it)
}
Standard Registered Claims:
sub → Subject (user identifier)
iat → Issued At
exp → Expiration Time
iss → Issuer
aud → Audience
nbf → Not Before (token invalid before this time)
jti → JWT ID (unique token identifier for revocation)
The payload is Base64URL-encoded, not encrypted. Anyone who has the JWT can decode and read the payload without any key. Never store passwords, secrets, or other sensitive data in a JWT payload.
Part 3: The Signature
The signature proves that the token was created by someone who knows the server's secret key, and that neither the header nor the payload has been tampered with since it was issued.
How the Signature Is Created (HS256): signature = HMAC-SHA256( base64url(header) + "." + base64url(payload), SECRET_KEY ) The SECRET_KEY is only known to the server. When the server receives a JWT: 1. Re-compute the signature using the header, payload, and SECRET_KEY 2. Compare computed signature to the signature in the token 3. If they match → token is authentic and unmodified 4. If they do not match → token was tampered with → reject An attacker who changes the payload (e.g., "role": "admin") cannot produce a valid signature without knowing SECRET_KEY. → Token modification is detected and rejected.
Symmetric vs Asymmetric Signing
Symmetric (HS256, HS384, HS512): Uses a single shared secret key. Same key signs AND verifies the token. Diagram: Auth Server API Server [SECRET_KEY] → signs JWT → [SECRET_KEY] → verifies JWT Both sides must know the same secret. Risk: If the API server is compromised, the attacker has the signing key and can forge valid tokens for anyone. Asymmetric (RS256, RS384, RS512, ES256): Uses a key pair: private key and public key. Private key signs the token (kept secret by Auth Server). Public key verifies the token (can be shared freely). Diagram: Auth Server API Server [PRIVATE KEY] → signs JWT → [PUBLIC KEY] → verifies JWT API server only needs the public key — cannot forge tokens. Better for: Microservices, multi-tenant platforms, anywhere the resource server and authorization server are separate systems.
JWT Validation: What Must Be Checked
Receiving a JWT is not enough. Every server that accepts JWTs must validate all of the following before trusting the token's contents.
Complete JWT Validation Checklist:
✓ 1. Signature verification
Re-compute the signature and compare. Non-negotiable.
✓ 2. Algorithm check
Server verifies using ITS EXPECTED algorithm only.
Never trust the "alg" field in the header.
✓ 3. Expiration (exp) check
Current time must be before the exp timestamp.
Reject expired tokens — even by one second.
✓ 4. Issued At (iat) check
Token should not have been issued in the future.
Helps detect clock skew attacks.
✓ 5. Not Before (nbf) check
If present, current time must be after nbf.
✓ 6. Issuer (iss) check
Must match the expected issuer (e.g., "auth.company.com").
Prevents tokens from one service being used with another.
✓ 7. Audience (aud) check
Must include the current API's identifier.
Prevents tokens meant for one API from being used with another.
Skipping any of these checks creates a vulnerability.
Critical JWT Vulnerabilities
Vulnerability 1: Algorithm None Attack
The "none" algorithm attack:
A JWT with alg set to "none" has no signature.
Some vulnerable libraries accept "none" as valid,
skipping signature verification entirely.
Attacker takes their token:
{ "alg": "HS256", "typ": "JWT" }
{ "user_id": 101, "role": "user" }
Modifies payload:
{ "user_id": 101, "role": "admin" }
Changes header:
{ "alg": "none", "typ": "JWT" }
Re-encodes without signature.
If the server accepts "none": attacker is now admin.
Fix: Never accept "none" as a valid algorithm.
Hardcode the expected algorithm on the server.
Vulnerability 2: RS256 to HS256 Confusion Attack
When a server normally uses RS256 (public/private keys): Public key is available at a well-known URL. Server verifies with public key. Attacker's trick: Download server's public key (it's public, after all). Create a new JWT claiming alg: HS256. Sign it with the public key used as an HMAC secret. If the server trusts the "alg" header and switches to HS256 mode, it uses the public key as the HMAC secret for verification. The attacker signed with the same value → verification passes! Fix: Server must ALWAYS use the algorithm it expects, not what the token claims. If the server uses RS256, it must refuse HS256 tokens.
Vulnerability 3: Weak Secret Keys
JWT HMAC signing secret should be: - At least 256 bits (32 bytes) of random data - Cryptographically random (from a CSPRNG) - Never a dictionary word, username, or short string Weak secrets that have been found in real-world JWTs: "secret" "password" "jwt_secret" "12345" The app's domain name Attackers use offline brute-force tools to try common secrets. If the secret is guessable, they can sign their own tokens. Strong secret: 3mK9pL#xQ7nRv$bZ2wHy8cDtA5sE6fJu
Vulnerability 4: Missing Expiration
JWT without exp claim:
{ "user_id": 101, "role": "admin" }
→ Valid forever
If an attacker steals this token:
→ They have permanent admin access
→ Only way to revoke is to change the signing secret
→ That invalidates ALL tokens for ALL users
Always set exp. Typical values:
Access tokens: 15 minutes to 1 hour
Refresh tokens: 7 days to 30 days
Vulnerability 5: Sensitive Data in Payload
JWT payload is Base64URL encoded — NOT encrypted. Anyone who has the token can read the payload. Never put in JWT payload: Passwords or password hashes Credit card numbers SSN or Aadhaar numbers Medical records Any secret or sensitive business data Safe to put in payload: User ID Role or permission list Email (with consideration) Expiration time Issuer and audience
JWT Revocation Problem
The biggest operational challenge with JWTs is that they cannot be revoked before expiration by default. Once issued, a valid JWT remains valid until its exp time arrives — even if the user logs out, changes their password, or is banned.
Revocation Strategies: Strategy 1: Short expiration + refresh tokens Access token expires in 15 minutes. If revocation needed, user just waits 15 minutes. Refresh token stored in server-side database — can be deleted. Trade-off: Small window where revoked access token still works. Strategy 2: Token blocklist (jti claim) Every JWT includes unique jti (JWT ID) claim. Server maintains blocklist of revoked jti values. On every request: check if jti is in blocklist. Trade-off: Requires database lookup, reduces statelessness benefit. Strategy 3: Token version in user record User record has a "token_version" number (e.g., 5). JWT contains the version at issuance. On request: compare JWT version to current user version. To invalidate all tokens: increment user's token_version. Trade-off: Database lookup per request.
Where to Store JWTs Safely
Storage Location Comparison: localStorage (JavaScript-accessible): Pro: Easy to implement Con: XSS can steal it. Any script on the page reads it. Verdict: Avoid for high-security tokens sessionStorage (cleared on tab close): Pro: Auto-cleared Con: Still vulnerable to XSS Verdict: Slightly better than localStorage, still vulnerable Secure HttpOnly Cookie: Pro: JavaScript cannot read it (HttpOnly flag) Pro: Sent automatically with same-origin requests Con: Requires CSRF protection (SameSite=Strict or token) Verdict: Best option for web applications Memory (JavaScript variable): Pro: Not accessible after page reload, not in storage Con: Lost on page refresh Verdict: Good for short-lived tokens in SPAs Mobile app secure storage: iOS: Keychain Android: EncryptedSharedPreferences or Keystore Verdict: Required for mobile apps
Key Points
- A JWT has three parts: header, payload, and signature — each Base64URL-encoded and separated by dots.
- The signature proves the token was issued by the correct server and has not been tampered with.
- Always validate: signature, algorithm, expiration, issuer, and audience.
- Never trust the "alg" field from the token header — hardcode the expected algorithm on the server.
- The algorithm-none attack and RS256-to-HS256 confusion are the two most dangerous JWT vulnerabilities.
- JWT payloads are readable by anyone — never store sensitive data in them.
- JWTs cannot be revoked by default. Use short expiration times and implement a blocklist or version check for revocation needs.
