Understanding JWT Tokens: Structure and Security

JSON Web Tokens are everywhere in modern API authentication, but they are frequently misunderstood and misused. This guide explains exactly what a JWT is, how it works, how to decode one, and how to avoid the most dangerous security pitfalls.

JWT Security Authentication API

What Is a JWT?

A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe string that encodes a set of claims — assertions about a user or session — and optionally signs or encrypts them. The defining characteristic of a JWT is that the data it carries can be verified without querying a database, because the server uses a cryptographic signature to prove the token was legitimately issued.

A typical JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJleHAiOjE3MTY5OTUyMDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The three parts separated by dots are: the Header, the Payload, and the Signature. Each part is Base64URL-encoded.

The Three Parts Explained

1. Header

The header specifies the token type and the signing algorithm. Decode the first part and you get:

{
  "alg": "HS256",
  "typ": "JWT"
}

HS256 means HMAC-SHA256 — a symmetric algorithm where the same secret key both signs and verifies. RS256 (RSA) is asymmetric: the server signs with a private key and anyone can verify with the public key.

2. Payload (Claims)

The payload contains claims. Standard registered claims include:

{
  "sub": "user_123",       // Subject (user ID)
  "iss": "auth.example.com", // Issuer
  "aud": "api.example.com",  // Audience
  "exp": 1716995200,         // Expiration (Unix timestamp)
  "iat": 1716908800,         // Issued At
  "email": "user@example.com" // Custom claim
}
⚠️ The payload is Base64URL-encoded, NOT encrypted. Anyone who has the token can read the payload by decoding it. Never put passwords, credit card numbers, or sensitive PII in a JWT payload.

3. Signature

The signature is computed as:

HMACSHA256(
  base64url(header) + "." + base64url(payload),
  secret_key
)

When your server receives a JWT, it recomputes this signature using the secret key. If the signature matches, the token is authentic and unmodified. If it does not match, the token was tampered with and must be rejected.

How to Decode a JWT in the Terminal

You can decode the header and payload without any tools — they are just Base64URL. In bash:

# Decode the payload (second part)
echo "eyJ1c2VySWQiOiIxMjMifQ" | base64 -d 2>/dev/null
# Output: {"userId":"123"}

Using Python (handles padding automatically):

import base64, json

def decode_jwt_part(part):
    # Add padding
    padding = 4 - len(part) % 4
    part += '=' * padding
    return json.loads(base64.urlsafe_b64decode(part))

token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEyMyJ9.xyz"
parts = token.split('.')
print(decode_jwt_part(parts[0]))  # header
print(decode_jwt_part(parts[1]))  # payload

Verifying a JWT in Node.js

const jwt = require('jsonwebtoken');

// Signing a token (on login)
const token = jwt.sign(
  { userId: '123', email: 'user@example.com' },
  process.env.JWT_SECRET,
  { expiresIn: '1h', audience: 'api.example.com' }
);

// Verifying a token (on each protected request)
try {
  const decoded = jwt.verify(token, process.env.JWT_SECRET, {
    audience: 'api.example.com'
  });
  console.log('User ID:', decoded.userId);
} catch (err) {
  // TokenExpiredError, JsonWebTokenError, etc.
  console.error('Invalid token:', err.message);
}

Common JWT Security Pitfalls

Pitfall 1: The "alg: none" Attack

Some early JWT libraries accepted a token with "alg": "none" in the header, skipping signature verification entirely. An attacker could forge any payload and set the algorithm to none. Always use a library that explicitly rejects alg: none and explicitly specify the expected algorithm during verification.

Pitfall 2: Symmetric Keys in Public APIs

If you use HS256 (symmetric), both signing and verifying require the same secret. If you share your JWT with third-party services to verify, they also need the secret — and now it is no longer secret. Use RS256 for public APIs: third parties verify with your public key, while your private key remains protected.

Pitfall 3: Long-Lived Tokens Without Revocation

JWTs are stateless by design — once issued, they are valid until expiry. If you need to revoke access immediately (e.g., on logout or account suspension), a pure JWT approach cannot do this without a server-side blocklist. Keep access token expiry short (15 minutes) and use refresh tokens for long sessions.

Pitfall 4: Storing JWTs in localStorage

Tokens stored in localStorage are accessible to any JavaScript running on your page, making them vulnerable to XSS attacks. Prefer storing JWTs in httpOnly cookies, which cannot be accessed by JavaScript. If you must use localStorage, ensure strict CSP policies and sanitize all user-controlled content.

✅ Best practice summary: short expiry (15 min), RS256 for public APIs, httpOnly cookies for storage, explicit algorithm in verification, and a refresh token rotation strategy.

JWT vs. Session Tokens

Traditional session tokens are random strings stored server-side in a database or cache. The server must do a database lookup on every request. JWTs are self-contained — no database lookup needed. The trade-off: sessions can be revoked instantly, while JWTs cannot be revoked before expiry without extra infrastructure. Neither is universally better; choose based on your scale and security requirements.

Related Tools