Guide

JWT explained: structure, signing, and security

After you log in, something has to prove who you are on the next hundred API calls. A JSON Web Token (JWT) is one of the most common answers: a compact, signed string that carries claims — user ID, roles, expiry — so each service can verify identity without hitting the database every time. This guide walks through how JWTs are built, how to verify them correctly, and the security mistakes that turn a convenient format into an open door.

What a JWT is (and what it is not)

A JWT is not encryption. Anyone who intercepts the string can Base64-decode the middle section and read the JSON inside. What JWTs provide is integrity: if a single character changes, the signature no longer matches and verification fails.

Think of a JWT as a tamper-evident envelope. The sender writes claims on a card, seals the envelope with a secret or private key, and hands it to the client. The client presents the envelope on later requests; the server checks the seal before trusting what is written inside.

JWTs excel in stateless APIs and microservice meshes where many backends need the same identity context. They are a poor fit when you need instant revocation of every active session (a stolen token remains valid until it expires unless you maintain a blocklist). For high-assurance flows — banking, admin panels, wallet key access — pair short-lived JWTs with server-side session control or refresh-token rotation.

The three-part structure: header.payload.signature

A JWT is three Base64url-encoded segments joined by dots: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header

Declares the token type (typ: "JWT") and signing algorithm (alg). Common values: HS256 (HMAC with SHA-256) or RS256 (RSA signature). The header is signed along with the payload — never trust an alg value without pinning allowed algorithms in your verifier.

Payload (claims)

A JSON object of statements about the subject. Standard registered claims include:

  • sub — subject (usually user ID)
  • iss — issuer (who minted the token)
  • aud — audience (which API should accept it)
  • exp — expiration time (Unix seconds)
  • iat — issued-at time
  • nbf — not-before time (optional)

You can add custom claims — role, tenant_id, scope — but keep payloads small. JWTs ride on every request in the Authorization header; bloated claims waste bandwidth and leak more data if captured.

Signature

Computed from Base64url(header) + "." + Base64url(payload) using the chosen algorithm and a key. Verifiers recompute the signature with the same key material. Mismatch means reject — do not parse claims first and check the signature as an afterthought.

HS256 vs RS256: symmetric vs asymmetric signing

HS256 uses a single shared secret. Issuer and every verifier must know that secret. Simple for monoliths; painful when ten microservices all need the same key — one leak compromises every service.

RS256 (and ES256 with elliptic curves) uses an RSA or EC private key to sign and a matching public key to verify. Only the auth service holds the private key; APIs fetch the public key from a JWKS endpoint (/.well-known/jwks.json) and verify locally. This is the pattern behind most OAuth 2.0 providers and OpenID Connect identity tokens.

Rule of thumb: HS256 inside one trusted process boundary; RS256/ES256 when multiple services verify tokens independently. Rotate keys periodically and support overlapping key IDs (kid in the header) so rotation does not force a hard logout for every user.

Access tokens, refresh tokens, and lifetimes

Production systems rarely issue one long-lived JWT and call it done. The usual split:

  • Access token — short TTL (5–15 minutes), sent on API calls as Authorization: Bearer <token>. Small blast radius if stolen.
  • Refresh token — longer TTL (days to weeks), stored more carefully (HTTP-only cookie or secure server storage), used only to obtain new access tokens at a dedicated /token endpoint.

When an access token expires, the client silently exchanges the refresh token for a fresh pair. If the refresh token is compromised, you revoke it server-side (store refresh token IDs in a database) without invalidating every access token globally. This hybrid gives you stateless verification for hot paths and revocation where it matters.

Set exp aggressively and validate it with a small clock-skew tolerance (30–60 seconds). Reject tokens with nbf in the future and, when using OIDC, confirm iss and aud match your expected issuer and client ID.

JWT vs session cookies

Server-side sessions store state in Redis or a database; the browser holds only an opaque session ID in an HTTP-only cookie. Revocation is instant (delete the session row). The trade-off is a datastore lookup on every request and stickier scaling across regions.

JWTs push identity into the token itself. Verification is a CPU-bound signature check — fast and horizontally scalable — but revocation requires extra machinery (short TTLs, refresh rotation, deny lists).

Many apps combine both: session cookie for the web UI, JWT access tokens for mobile apps and third-party API consumers. Neither replaces OAuth 2.0 and OpenID Connect when users sign in through Google or GitHub — OAuth handles authorization; the resulting access token is often a JWT.

Validation checklist every API should implement

Libraries like jsonwebtoken (Node), PyJWT (Python), and golang-jwt handle the cryptography — but you must configure them correctly:

  1. Pin allowed algorithms. Reject alg: "none" and unexpected algorithms — a classic JWT attack vector.
  2. Verify signature before trusting claims. Use the correct key (shared secret or JWKS public key matched by kid).
  3. Check exp, nbf, and optionally iat. Reject expired tokens; do not accept tokens from the future.
  4. Validate iss and aud when your tokens come from an identity provider.
  5. Do not put secrets in the payload. No passwords, API keys, or PII you would not log in plain text.
  6. Rate-limit auth endpoints. Brute-force against login and refresh routes still happens — see our API rate limiting guide.

On the client, avoid storing access tokens in localStorage if XSS is possible — any injected script can exfiltrate them. Prefer HTTP-only, Secure, SameSite cookies for browser apps, and keep CORS rules tight so random origins cannot call your API with stolen tokens.

Common mistakes and how to avoid them

Treating the payload as trusted input

Even after signature verification, treat claims as hints, not authorization. A role: "admin" claim should come from your auth service at login time — but sensitive actions still deserve a server-side permission check or a fresh lookup if roles can change mid-session.

Long-lived tokens without rotation

A JWT valid for 30 days is effectively a password that cannot be changed without a deny list. Prefer short access tokens plus refresh rotation; detect refresh token reuse (if the same refresh token is presented twice, revoke the whole family).

Logging tokens

Access logs, error trackers, and browser devtools will capture Authorization headers if you are not careful. Redact bearer tokens in logs and never pass them in URL query strings — they end up in referrer headers and CDN logs.

Confusing JWT with wallet signatures

Blockchain wallets sign transaction messages with ed25519/secp256k1 keys tied to on-chain addresses. That is a different trust model from JWT bearer tokens issued by your API. dApps often use wallet signatures for "Sign in with Solana" flows and JWTs for subsequent API access — keep the two paths separate and document which key material protects what.

When to reach for JWTs

Use JWTs when you need portable, verifiable identity across services without central session storage on every hop — internal microservices, mobile backends, machine-to-machine APIs, and OIDC identity tokens. Skip them when you need immediate global logout, when payloads would grow large, or when a simple HTTP-only session cookie solves the problem with less attack surface.

The format is standardized (RFC 7519); the security is entirely in how you sign, store, validate, and expire the tokens. Get those four right and JWTs are a durable building block. Get them wrong and you have a fancy encoding of user IDs that attackers can forge or replay.

Related reading