Skip to content
yutils

What is JWT? Token Structure and Secure Use

JSON Web Token (JWT) explained from header/payload/signature to verification, secret rotation, and common security pitfalls.

~9 min read

A JSON Web Token (JWT) is a compact string that two systems pass around to share information. Think of the short token your server returns right after login. This guide walks through what a JWT looks like, how to verify one correctly, and the security pitfalls that trip up most teams. RFC 7519 is long; we'll focus on the parts you actually use.

What problem JWT solves

Traditional session auth stores a session ID on the server and looks it up in a database or cache on every request. As traffic grows, the session store becomes a bottleneck, and scaling across multiple servers requires sticky sessions or a shared cache.

JWT packs the user information into the token itself and signs it. The server only verifies the signature — no lookup needed. The same token flows through microservices, carrying identity along. The trade-off: once issued, a JWT cannot be revoked easily before its expiry — this is the central tension in any JWT design.

Token structure — three pieces

A JWT is three Base64URL-encoded segments separated by dots (.):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0In0.SflKxw...
  • Header — Base64URL-encoded JSON describing the algorithm (alg) and type (typ). Example: {"alg":"HS256","typ":"JWT"}
  • Payload — Base64URL-encoded JSON holding claims: user id, expiry, permissions, and so on.
  • Signature — header + payload signed with a secret or private key. Detects tampering.

Base64URL is almost the same as standard Base64 but URL-safe: + becomes -, / becomes _, and trailing = padding is dropped. Crucially this is encoding, not encryption — anyone can decode the payload with Base64 Encode / Decode. Never put secrets in the payload.

Standard claims

RFC 7519 defines short keys for common claims to keep tokens small.

KeyNameMeaning
issIssuerWho issued the token (auth server URL)
subSubjectWho the token is about (usually a user id)
audAudienceWho is allowed to consume it (API name)
expExpirationExpiry time (Unix epoch seconds)
nbfNot BeforeInvalid before this time
iatIssued AtWhen the token was issued
jtiJWT IDUnique token id (for revocation lists)

You can add custom claims (role, email, …) freely. Watch the size though — payloads ride along on every request. Keep them under 200–500 bytes if possible.

Algorithms — HS256 vs RS256

The alg header decides the signing method. In practice you will see one of two:

  • HS256 (HMAC-SHA256) — A shared secret signs and verifies. Fast and simple for a single backend. Leak the secret and anyone can forge tokens. This is plain HMAC Generator under the hood.
  • RS256 (RSA-SHA256) — Asymmetric. The issuer signs with a private key, verifiers use a public key. Public keys can be distributed safely, so multiple services can verify without sharing a secret. Default for OAuth/OIDC providers like Auth0 or Cognito.

ES256 (ECDSA) is also asymmetric but with smaller keys and signatures — a fit for mobile or constrained environments.

Verification — the mandatory checks

Tools like JWT Decoder show the payload instantly. But decoding is not verifying. Before accepting a token, your server must:

  1. Verify the signature with the declared algorithm and your key.
  2. Enforce an algorithm allowlist. Never trust the alg in the header alone — this is the entry point for the famous alg: none and HS/RS confusion attacks.
  3. Check exp against the current time, usually with ±30s leeway for clock skew.
  4. Validate iss and aud — the token came from the right issuer and is intended for your service.

Most libraries (Node's jsonwebtoken, Python's PyJWT, Go's golang-jwt) handle these checks when you pass verify(token, secret, {algorithms: ["HS256"]}). Passing an algorithm list is not optional — defaults have historically been unsafe.

Common pitfalls

1. Storing secrets in the payload

The payload is plain encoding. Passwords, national IDs, API keys — never. Keep the user id and roles, look up sensitive data on the server.

2. Weak signing key

An HS256 secret like "changeme" is brute-forceable in seconds. Use at least 256 bits (32 bytes) of random data — openssl rand -base64 32 or yutils' Password Generator. Never commit secrets — a git search will find them.

3. No revocation path

JWT's stateless nature breaks "log out immediately on all devices". Two common fixes:

  • Short-lived access + refresh tokens — access expires in 5–15 minutes, refresh tokens live in the database and can be revoked.
  • Revocation list — store jti or user ids to deny. You lose statelessness, but you regain control.

4. The alg: none attack

Old JWT libraries would skip signature verification if alg was none. An attacker rewrites the header to {"alg":"none"} and forges the payload. Most libraries patched this between 2015–2019, but legacy services are still out there.

5. HS256 ↔ RS256 confusion

A server expects RS256, but accepts whatever the header says. An attacker switches the header to HS256 and uses the public key as the HMAC secret. Public keys are, well, public — so anyone can sign. The only defense is an explicit algorithm allowlist.

6. Clock skew

Distributed servers drift by a few seconds. A token issued a moment ago can be rejected on exp or nbf. Use NTP and a ±30s leeway.

Where to store JWTs

In the browser you have three options:

  • localStorage — Accessible from JS. A single XSS steals every token. Simple but risky.
  • HttpOnly + Secure cookies — Not accessible from JS, so XSS can't read them, but you need CSRF protection — SameSite plus a CSRF token.
  • In-memory variable — Lost on reload, but safest. Common pattern: short-lived access token in memory, refresh token in an HttpOnly cookie.

Try it

The fastest way to understand JWT is to encode and decode one.

  • JWT Encoder (HMAC) — build a token from a payload, secret, and algorithm (HS256/384/512).
  • JWT Decoder — paste a token, see its header and payload. All processing happens in the browser, so you never ship secrets to a third-party site.
  • HMAC Generator — run HMAC-SHA256 manually on the header.payload string and watch the signature match the third segment.

Recap

  • JWT = Base64URL(header).Base64URL(payload).signature
  • The payload is encoded, not encrypted — never store secrets in it.
  • On verify, enforce an algorithm allowlist and check exp, iss, aud.
  • Use a 256-bit random secret stored outside the codebase.
  • Short-lived access tokens plus refresh tokens give you both statelessness and a revocation path.
Back to guides