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.

Leave a Comment