JWT Explained: Header, Payload, and Signature Decoded
A JSON Web Token is three Base64URL-encoded strings joined by dots: header.payload.signature. The compact format makes JWTs easy to embed in HTTP headers, URL query strings, and cookies. But that compactness hides a lot of nuance — especially around security.
The header
The header is a JSON object describing the token type and the signing algorithm. Decoded, it typically looks like:
{
"alg": "HS256",
"typ": "JWT"
}The alg field is critical. Common values:
HS256— HMAC-SHA256. Symmetric: the same secret is used to sign and verify. Simple but the secret must be shared between services.RS256— RSA-SHA256. Asymmetric: sign with a private key, verify with a public key. Better for microservices — only the auth server needs the private key.ES256— ECDSA with P-256. Like RS256 but smaller key sizes and faster operations.none— No signature. Never accept this in production. Attackers can forge tokens with"alg": "none"if your library doesn't explicitly reject it.
The payload
The payload contains claims — statements about the token subject. Standard registered claims:
sub— subject: the user ID or entity this token representsiss— issuer: the service that issued the token (e.g.https://auth.example.com)aud— audience: the intended recipient service(s)exp— expiry: Unix timestamp after which the token is invalidiat— issued at: when the token was creatednbf— not before: token is invalid before this timejti— JWT ID: unique identifier for this token (enables revocation)
Important: The payload is Base64URL-encoded, not encrypted. Anyone with the token can read the payload — never put passwords, credit card numbers, or other secrets in a JWT payload.
The signature
The signature prevents tampering. For HS256, it is computed as:
HMAC-SHA256( base64url(header) + "." + base64url(payload), secret )
If an attacker changes any bit of the header or payload, the signature won't verify. This is what makes JWTs tamper-evident — not confidential.
Common JWT pitfalls
- Algorithm confusion attack — if your library accepts both HS256 and RS256, an attacker can take an RS256 public key (which is public), sign a token with it using HS256, and the library may accept it if it doesn't check the expected algorithm explicitly.
- Not validating expiry — always check
exp. A valid signature doesn't mean a token is still valid. - Not validating audience — a token issued for Service A should be rejected by Service B. Check
aud. - Weak secrets for HS256 — a short or guessable HMAC secret can be brute-forced. Use at least 256 bits of random entropy.
- Storing in localStorage — accessible to any JavaScript on the page. Prefer httpOnly cookies for tokens that authorize actions.
JWT vs sessions
JWTs are stateless — the server doesn't store anything. Sessions are stateful — the server stores session data and gives the client an opaque ID. JWTs scale horizontally without a shared session store, but can't be individually revoked (you have to wait for exp or implement a token blocklist — which adds state back anyway). For most applications, server-side sessions with a fast store (Redis) are simpler and more secure.
Try it yourself
Use the free browser-based JWT Debugger on DevBench — no signup, runs entirely in your browser.
Open JWT Debugger