JSON Web Tokens (JWT) Explained

JSON Formatter Hub

JSON Web Tokens (JWT) Explained: Structure, Use Cases, and Security

A JSON Web Token (JWT) is a compact, URL-safe way to transmit claims between two parties. It is used almost universally for API authentication โ€” when a user logs in, the server issues a JWT; the client stores it and sends it with every subsequent request. The server verifies the token's signature without looking anything up in a database. Understanding exactly how JWTs work โ€” and where they go wrong โ€” is essential for any developer building an API.

The Three-Part Structure

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzE1MzM2MDAwLCJleHAiOjE3MTUzMzk2MDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Three base64url-encoded sections, separated by dots: header.payload.signature.

Part 1: Header

Decode the first section and you get a JSON object describing the token type and signing algorithm:

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

Common algorithms: HS256 (HMAC-SHA256, symmetric โ€” one shared secret), RS256 (RSA-SHA256, asymmetric โ€” private key signs, public key verifies).

Part 2: Payload (Claims)

The payload carries the actual data โ€” called claims:

{
  "sub": "1234567890",
  "name": "Alice",
  "email": "alice@example.com",
  "role": "admin",
  "iat": 1715336000,
  "exp": 1715339600,
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com"
}

Standard registered claims:

Part 3: Signature

The signature is computed over the encoded header and payload using the algorithm and secret:

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

The signature cannot be forged without the secret. If the payload is tampered with, the signature will not match and the token is rejected. Note: the payload is encoded, not encrypted โ€” anyone can read it by base64-decoding it.

How to Decode a JWT (Without Verifying)

// JavaScript โ€” decode without verifying signature
function decodeJWT(token) {
  const parts = token.split('.');
  if (parts.length !== 3) throw new Error('Invalid JWT');
  const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
  return JSON.parse(atob(payload));
}

const claims = decodeJWT('eyJhbGci...');
console.log(claims.sub);  // user ID
console.log(new Date(claims.exp * 1000)); // expiry date

Creating and Verifying JWTs

JavaScript (jsonwebtoken library)

const jwt = require('jsonwebtoken');

const SECRET = process.env.JWT_SECRET;

// Sign a token (expires in 1 hour)
const token = jwt.sign(
  { sub: user.id, name: user.name, role: user.role },
  SECRET,
  { expiresIn: '1h', issuer: 'https://auth.example.com' }
);

// Verify and decode
try {
  const payload = jwt.verify(token, SECRET, {
    issuer: 'https://auth.example.com'
  });
  console.log(payload.sub);
} catch (err) {
  console.error('Invalid token:', err.message);
}

Python (PyJWT library)

import jwt
from datetime import datetime, timedelta, timezone

SECRET = 'your-secret-key'

# Sign
payload = {
    'sub': str(user.id),
    'name': user.name,
    'iat': datetime.now(timezone.utc),
    'exp': datetime.now(timezone.utc) + timedelta(hours=1)
}
token = jwt.encode(payload, SECRET, algorithm='HS256')

# Verify
try:
    decoded = jwt.decode(token, SECRET, algorithms=['HS256'])
    print(decoded['sub'])
except jwt.ExpiredSignatureError:
    print('Token expired')
except jwt.InvalidTokenError as e:
    print(f'Invalid token: {e}')

HS256 vs RS256: Which to Use

AlgorithmTypeSigns withVerifies withBest for
HS256SymmetricShared secretSame secretSingle-service apps where server signs and verifies
RS256AsymmetricPrivate keyPublic keyMicroservices โ€” auth server signs, multiple services verify without the private key
ES256Asymmetric (ECDSA)Private keyPublic keySame as RS256 but smaller key size

Security Pitfalls

The "alg: none" attack

// WRONG โ€” accepts any algorithm including "none"
jwt.verify(token, secret);

// CORRECT โ€” whitelist the expected algorithm
jwt.verify(token, secret, { algorithms: ['HS256'] });

localStorage vs httpOnly cookie

Storing a JWT in localStorage makes it readable by any JavaScript on the page, including injected scripts (XSS). Storing it in an httpOnly cookie means JavaScript cannot read it, but you must also set SameSite=Strict or SameSite=Lax to prevent CSRF.

Keep expiry short, use refresh tokens

A stolen JWT cannot be invalidated (JWTs are stateless). Short-lived access tokens (15 minutes to 1 hour) combined with longer-lived refresh tokens minimize the damage window.

Validate iss and aud claims

Always verify iss (issuer) and aud (audience) in addition to the signature and expiry. A token issued by one service should not be accepted by another service even if the signature is valid.

What NOT to store in a JWT payload

The payload is base64-encoded, not encrypted โ€” anyone with the token can read it. Never put passwords, credit card numbers, social security numbers, or other sensitive PII in the payload.

Related Articles