JSON Web Tokens (JWT) Explained: How They Work and When to Use Them

JWTs are signed claims you can verify without a database lookup. The structure, signing, verification, and the production pitfalls.

JSON Web Tokens (JWT) Explained: How They Work and When to Use Them

A JWT (JSON Web Token) is a way to express claims about a subject (typically a user) in a self-contained, signed format. The server signs the token; the bearer presents it; the recipient verifies the signature and trusts the claims without needing to consult a database. This is the foundation of most modern stateless authentication.

This post explains JWTs at a practical level: the structure, the algorithms, how to use them safely, and the production pitfalls that trip up most teams.

The Three Parts

A JWT is three base64url-encoded strings joined by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBbGljZSIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  • Header — Algorithm and token type.
  • Payload — Claims about the user.
  • Signature — Proof the token wasn’t tampered with.

Decoded:

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

// Payload
{ "sub": "12345", "name": "Alice", "iat": 1516239022 }

// Signature (binary, base64url-encoded)
HMAC-SHA256(header_b64 + "." + payload_b64, secret)

The signature binds the header and payload to a secret only the issuer knows. Anyone with the secret can verify; anyone without it cannot forge.

Common Claims

Standard claims (registered in RFC 7519):

  • iss — Issuer. Who issued this token.
  • sub — Subject. The user/entity this token represents.
  • aud — Audience. Who this token is intended for.
  • exp — Expiration. Unix timestamp after which the token is invalid.
  • iat — Issued at.
  • nbf — Not before.
  • jti — JWT ID. Unique identifier for the token (useful for revocation).

You can also add custom claims:

{
    "sub": "12345",
    "name": "Alice",
    "role": "admin",
    "exp": 1700000000
}

The payload is base64-encoded, not encrypted. Anyone can read it. Don’t put secrets in JWTs.

Signing Algorithms

The two common families:

HMAC (HS256, HS384, HS512)

Symmetric — same secret signs and verifies. Simple, fast. Use when the issuer and verifier are the same service (or trusted services sharing a secret).

import jwt from 'jsonwebtoken'
const token = jwt.sign({ sub: 'user-id' }, process.env.JWT_SECRET, { algorithm: 'HS256', expiresIn: '1h' })

RSA / ECDSA (RS256, ES256)

Asymmetric — private key signs; public key verifies. Use when the issuer and verifier are different services. The verifier only needs the public key.

const token = jwt.sign({ sub: 'user-id' }, privateKey, { algorithm: 'RS256', expiresIn: '1h' })
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] })

Modern choice: EdDSA (Ed25519) is fast and small. Use it when your library supports it.

Don’t use: none algorithm. It says “I’m not signed.” Many libraries used to accept unsigned tokens; classic security bug. Modern libraries reject none unless explicitly enabled.

How to Use JWTs

The standard pattern:

  1. User logs in with credentials.
  2. Server validates credentials.
  3. Server signs a JWT with the user’s claims and a reasonable expiration (e.g., 1 hour).
  4. Server sends the JWT to the client.
  5. Client stores it (usually in memory or httpOnly cookie).
  6. Client includes it in subsequent requests: Authorization: Bearer <token>.
  7. Server verifies signature and claims for each request.

The key benefit: step 7 doesn’t require a database lookup. The server just verifies the signature with its secret and reads the claims.

When JWTs Are the Right Tool

  • Distributed services that need to validate user identity without coordinating.
  • API tokens with limited scopes (read-only, specific resources).
  • Service-to-service auth with short-lived tokens.
  • Mobile apps where session state on the server is awkward.
  • Federated identity (OAuth, OIDC) where one service issues, others consume.

When JWTs Are the Wrong Tool

  • User sessions on a traditional web app. Server-side sessions are simpler. JWTs add complexity without clear benefit.
  • When you need easy revocation. JWTs are valid until expiration; you can’t easily invalidate one early without maintaining a blocklist (defeating the stateless point).
  • When you need to update user claims often. Re-issuing tokens on every change is awkward.
  • When you have a single monolith. Session cookies work fine; JWTs are over-engineered.

The biggest mistake teams make is using JWTs because “everyone uses JWTs” when a simple cookie-based session would be simpler.

The Revocation Problem

JWTs are valid until they expire. If you want to revoke one early:

Option 1: Short expirations + refresh tokens

Access tokens expire in 5-15 minutes. Long-lived refresh tokens (stored server-side) exchange for new access tokens. To revoke, mark the refresh token as invalid; access tokens expire quickly.

This is the OAuth pattern. Most production systems use it.

Option 2: Revocation list

Maintain a server-side list of revoked token IDs. Check against it on each request. Loses the “no database lookup” benefit but adds revocation.

Option 3: Versioning

Store a “session version” per user. Include the version in the JWT. To revoke, increment the user’s version; old tokens fail verification.

Practical recommendation: short access tokens + refresh tokens. The 5-15 minute access token expiration is fast enough that revocation usually just means “don’t issue a new one.”

Storage on the Client

A perennial debate:

LocalStorage

  • Pros: Accessible from JavaScript; survives page reloads.
  • Cons: Vulnerable to XSS. Any JavaScript on the page (including injected scripts) can steal the token.
  • Pros: JavaScript can’t access. Resistant to XSS.
  • Cons: Vulnerable to CSRF if not protected with SameSite or CSRF tokens.

Memory only

  • Pros: Most secure (token doesn’t persist).
  • Cons: Doesn’t survive page reloads; needs re-auth on every refresh.

For most production apps in 2026: httpOnly + SameSite=Strict cookies are the safest default. They mitigate XSS and CSRF together.

Production Pitfalls

alg: none acceptance

Some old libraries verified tokens with alg: none (no signature). Fixed in modern libraries but still a vulnerability to check for.

Algorithm confusion

A library that accepts both HS256 and RS256 might let attackers send alg: HS256 with the public RSA key as the HMAC secret. Always specify the expected algorithm explicitly:

jwt.verify(token, publicKey, { algorithms: ['RS256'] })  // not ['HS256', 'RS256']

Long expirations

A 1-year JWT that gets leaked is exposed for a year. Keep expirations short.

Putting too much in the payload

JWTs are sent on every request. Adding lots of claims bloats every request. Keep them minimal.

Trusting unsigned tokens

Always verify the signature before trusting the claims. Some libraries have a “decode” function that doesn’t verify — don’t confuse them.

Clock skew

exp and iat checks fail if servers’ clocks are out of sync. Allow a small grace period (clockTolerance: 30 seconds typical).

Logging tokens

Don’t log the full token. The signature might be safe, but the payload is auditable PII.

Public Key Distribution

For asymmetric signing, the verifier needs the issuer’s public key. Common patterns:

JWKS endpoint

The issuer publishes public keys at a well-known URL (/.well-known/jwks.json). Verifiers fetch keys from there.

GET https://issuer.example.com/.well-known/jwks.json
{
    "keys": [
        { "kid": "key-1", "kty": "RSA", "n": "...", "e": "AQAB" }
    ]
}

The JWT includes a kid (key ID) header so the verifier knows which key to use.

This supports key rotation: the issuer adds a new key, signs new tokens with it, and the old key remains valid for verification until all old tokens expire.

Static configuration

Verifier has the public key hardcoded. Simpler but doesn’t rotate easily.

For internal services, static is often fine. For federated systems (OAuth, OIDC), JWKS is the standard.

JWT vs Session Cookies

A direct comparison:

FeatureSession cookieJWT
Stateless serverNoYes
Easy revocationYesNo (without help)
Database lookups per requestYes (cheap)No
Mobile app friendlyOKBetter
Cross-domainHardEasy
Token sizeSmall (cookie ID)Larger (encoded claims)
Setup complexityLowMedium

If you’re a single backend serving a single frontend, session cookies are simpler. If you have many services, mobile apps, third-party integrations — JWTs (or OAuth, which uses JWTs) make sense.

TL;DR

  • JWT = signed claims in a compact format.
  • Three parts: header, payload, signature, base64url-joined by dots.
  • Payload is readable, not encrypted. Don’t put secrets in it.
  • HS256 for shared-secret signing; RS256/ES256 for asymmetric.
  • Use short expirations + refresh tokens for revocation.
  • Store in httpOnly SameSite cookies for web apps.
  • Always specify the expected algorithm during verification.
  • Don’t use JWTs by default for simple monoliths. Server-side sessions are simpler.

JWTs are a tool for specific problems (stateless, distributed, federated). When those problems apply, they work well. When they don’t, they add complexity. For the broader auth flow patterns, see OAuth 2.0 flow explained; for the transport security underneath, TLS handshake.

Get Started

Convert IPs into accurate location data in milliseconds.

Sign up today and get 1,000 free monthly stored conversions, and discover why developers trust us for fast, reliable, and affordable IP conversions.