JWT header, payload, and signature explained
May 29, 2026 · 15 min read
Every JWT is two JSON objects plus a MAC or digital signature. Developers who understand what each segment contains debug auth failures faster - especially when alg mismatches, clock skew breaks exp, or a gateway rewrites claims.
Header fields you will see
The header is small JSON, usually under 100 bytes. alg is required for signed JWTs and must match what the verifier expects - never accept none in production. typ is often JWT; nested tokens may use JWT with a cty (content type) of JWT for JWS inside JWE. kid (key ID) points verifiers at the right public key in a JWKS rotation.
{
"alg": "RS256",
"typ": "JWT",
"kid": "2024-06-rotation-1"
}
Payload: registered, public, and private claims
- Registered -
iss,sub,aud,exp,nbf,iat,jti(RFC 7519). - Public - names registered in IANA to avoid collisions.
- Private - your app-specific keys like
roleortenant_id; agree on meaning out of band.
Keep payloads lean. Large claim sets inflate every request header and may hit proxy limits. Put bulky authorization data in the database keyed by sub, not inside the token.
Base64URL encoding rules
JWT uses Base64URL: standard Base64 with - instead of +, _ instead of /, and padding = stripped. Decoders must accept unpadded input. A common mistake is piping a JWT through standard Base64 tools without URL-safe alphabet conversion, which yields garbage JSON.
function decodeJwtPart(segment) {
const padded = segment.replace(/-/g, "+").replace(/_/g, "/");
const json = atob(padded.padEnd(padded.length + ((4 - (padded.length % 4)) % 4), "="));
return JSON.parse(json);
}
How the signature is built
For HMAC-SHA256 (HS256), the signing input is the ASCII string BASE64URL(header).BASE64URL(payload). HMAC-SHA256 is applied with the shared secret; the result is Base64URL-encoded as the third segment. For RS256, RSA-SHA256 replaces HMAC but the signing input string is identical. Verifiers recompute and compare in constant time.
signing_input = base64url(header_json) + "." + base64url(payload_json)
HS256: signature = base64url( HMAC-SHA256(signing_input, secret) )
RS256: signature = base64url( RSA-SHA256(signing_input, private_key) )
Inspecting real tokens safely
Copy tokens from browser devtools or API logs into a local decoder to see pretty-printed JSON and Unix timestamps for exp and iat. Redact production tokens before screenshots; even without the secret, payloads may contain emails and internal IDs.
FAQ
- What happens if I change one character in the payload?
- The signature no longer matches. Servers must reject the token with 401 Unauthorized unless they never verified signatures - a critical misconfiguration.
- Can the header and payload use pretty-printed JSON?
- No. JWT libraries serialize to compact JSON (no extra whitespace). Different whitespace changes the signature.
- What is the jti claim for?
jti(JWT ID) is a unique token identifier useful for replay detection or one-time use tokens when stored in a cache.- Why does my payload decode but verification fail?
- Decoding needs no key; verification needs the correct secret, public key, algorithm, and sometimes issuer/audience checks.
Related: What is a JWT · Decode JWTs safely