A JSON Web Token (JWT) is a compact, URL-safe string that carries claims about a user or session, signed by the issuer so the receiver can verify it wasn’t tampered with. A JWT has three parts separated by dots: header, payload, signature.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3MzAwMDAwMDB9.5RZ3...
└── header ────────┘ └── payload ─────────────────────────┘ └ sig ┘
The three segments
Header. A small JSON object that names the signing algorithm (alg) and token type (typ), then base64url-encoded. A typical header decodes to:
{ "alg": "HS256", "typ": "JWT" }
Payload. A JSON object of claims — facts the issuer wants to assert. Standard claims include sub (subject, usually a user ID), iat (issued-at timestamp), exp (expiration timestamp), aud (intended audience), and iss (issuer). Custom claims sit alongside the standard ones. The payload is base64url-encoded, not encrypted.
Signature. A keyed hash of base64url(header) + "." + base64url(payload), computed with the algorithm named in the header. With HS256 the key is a shared secret; with RS256 it’s the issuer’s private RSA key, and verifiers use the matching public key. The signature is what makes the token tamper-evident: change a single character of the payload and the signature won’t match anymore.
What a JWT isn’t
It isn’t encrypted. Base64url-encoded is not encrypted. Anyone with the token can decode it and read every claim in the payload — including, if you put it there, an email address, an internal user ID, a permission set, or anything else. Treat the payload as readable.
It isn’t a session. A session lives on the server and can be revoked by deleting it. A JWT is self-contained: once issued, it’s valid until exp. Revoking a leaked JWT before its expiration requires either a denylist (which gives back the server-state you avoided by using JWTs) or short expirations with refresh-token rotation.
It isn’t proof the user did anything. A valid signature proves the issuer minted the token. It does not prove the human at the keyboard authenticated recently — only that they presented a token the issuer signed at some earlier point.
Pitfalls
- Storing JWTs in
localStorageexposes them to any XSS on the origin. Cookies withHttpOnly; Secure; SameSite=Strictare the lower-risk default for browser sessions. - Long expirations. A 30-day
expmeans a stolen token is valid for 30 days. Short-lived access tokens (5–15 minutes) plus refresh tokens are the standard mitigation. - Trusting the
algheader. Some libraries historically acceptedalg: nonefrom the header itself, allowing forged tokens. Always pin the expected algorithm in the verifier; never let the token tell you how to verify itself. - Putting secrets in claims. Anything in the payload is readable. PII, internal infrastructure identifiers, and permission scopes are common over-shares.
- Skipping signature verification. Decoding a JWT (which is just base64url) is not the same as verifying it. A debug logger that calls
jwt.decode()instead ofjwt.verify()can be the source of a “we trusted an unsigned token” incident.
Use the JWT decoder above to inspect a token’s contents — the decoder runs locally in your browser, so the token never leaves your machine. To generate signed test tokens for development, see the JWT generator.