JWT Structure Explained: Header, Payload, and Signature Deep Dive
Most JWT tutorials stop at "it has three parts". This one goes deeper. We will walk through every field of a real token: what alg, typ, and kid mean in the header, how registered claims differ from public and private claims in the payload, and what actually happens byte-by-byte when a token is signed with HS256 versus RS256. By the end, a decoded JWT should read like plain documentation to you.
The Big Picture: header.payload.signature
A JSON Web Token (defined in RFC 7519) is a compact, URL-safe way of transmitting signed claims between parties. Here is a complete example token, split for readability:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0yMDI2LTA3In0 <- HEADER . eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyXzQy IiwiYXVkIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20iLCJleHAiOjE3ODM0MjU2 MDAsImlhdCI6MTc4MzQyNDcwMCwicm9sZSI6ImVkaXRvciJ9 <- PAYLOAD . 4lC7ZkFvQ2m8HxNwT1pRaU3sYbE9jKdOgWi5nMfB0cA <- SIGNATURE
Each of the first two parts is just base64url-encoded JSON. The third part is raw signature bytes, also base64url-encoded. Nothing is encrypted — a JWT is signed, not secret. Anyone can read it; only the key holder can produce a valid signature for it.
The Header: alg, typ, and kid
Decoding the header above gives:
{
"alg": "HS256",
"typ": "JWT",
"kid": "key-2026-07"
}- alg — the signing algorithm. Common values: HS256 (HMAC-SHA256, symmetric), RS256 (RSA-SHA256, asymmetric), ES256 (ECDSA, asymmetric but with smaller keys and signatures). The dangerous value is none — a verifier must never accept it. Your server should hard-code the algorithms it accepts, not read them from the token.
- typ — the token type, almost always JWT. Some systems use at+jwt for OAuth access tokens so verifiers can reject ID tokens sent where access tokens belong.
- kid — the Key ID. When an issuer rotates keys, it publishes several public keys (usually at a JWKS URL like /.well-known/jwks.json) and stamps each token with the id of the key that signed it. The verifier picks the matching key by kid. Important: kid is a lookup hint into keys you already trust — never fetch or accept key material that the token itself supplies (that is what makes jku and x5u header injection attacks work).
The Payload: Registered, Public, and Private Claims
The decoded payload of our example:
{
"iss": "https://auth.example.com", // issuer - who created the token
"sub": "user_42", // subject - who the token is about
"aud": "https://api.example.com", // audience - who should accept it
"exp": 1783425600, // expiry (unix seconds)
"iat": 1783424700, // issued-at (unix seconds)
"role": "editor" // private claim - app-specific
}RFC 7519 groups claims into three categories:
- Registered claims — standardized short names with defined meaning: iss, sub, aud, exp, nbf, iat, jti (unique token id, useful for revocation lists). All optional, but exp should be considered mandatory in practice.
- Public claims — names registered in the IANA JSON Web Token Claims registry or namespaced with a URI to avoid collisions, e.g. email, name, or https://myapp.com/claims/tenant. These are meant to be understood across systems.
- Private claims — anything two parties agree on privately, like role or plan above. Fine within your own ecosystem, but keep them small: every claim travels on every request, and an HTTP header over ~8 KB will be rejected by many proxies.
One rule cuts across all three categories: never put secrets in the payload. It is readable by anyone who obtains the token — no key required.
The Signature: HS256 vs RS256 Signing Flows
The signature is computed over the exact base64url text of the first two parts:
signingInput = base64url(header) + "." + base64url(payload) HS256: signature = HMAC-SHA256(secret, signingInput) RS256: signature = RSA-PKCS1-v1.5-Sign(privateKey, SHA256(signingInput))
The practical difference matters more than the math:
- HS256 (symmetric) — one shared secret signs and verifies. Simple and fast, ideal when the same service issues and consumes tokens. Weakness: every service that can verify can also mint tokens, and a short human-chosen secret can be brute-forced offline.
- RS256 (asymmetric) — the auth server keeps a private key; every other service verifies with the public key. Perfect for microservices and third-party integrations: verification ability never implies forging ability. Tokens and signatures are larger (an RS256 signature is 256 bytes for a 2048-bit key vs 32 bytes for HS256).
- ES256 — the modern middle ground: asymmetric like RS256, but with signatures around 64 bytes. Increasingly the default for new systems.
Verification reverses the flow: the verifier rebuilds signingInput from the token it received, computes or checks the signature with its key, and rejects the token if a single byte of header or payload changed. That is the entire integrity guarantee of a JWT.
Walking Through the Example Token Field by Field
- alg: HS256 — verify only with HMAC-SHA256 and the configured secret. If a token arrives claiming RS256 or none, reject it.
- kid: key-2026-07 — the July 2026 signing key; the previous key can still verify old tokens during rotation overlap.
- iss — must equal the issuer you expect. Prevents tokens from a different (even legitimate) auth server being accepted.
- sub: user_42 — the stable user identifier. Use this, not email (emails change), as your foreign key.
- aud — must contain your API's identifier. Prevents a token minted for service A being replayed against service B.
- exp / iat — 1783425600 minus 1783424700 is 900 seconds: a 15-minute token, a healthy lifetime for an access token.
- role: editor — a private claim your API can trust only after signature verification passes.
The fastest way to build this reading skill is repetition: paste tokens from your own apps into a client-side decoder and identify every field. The Dev Brains AI JWT Decoder decodes in your browser and highlights expiry so nothing sensitive leaves your machine.
Frequently Asked Questions
A JWT has a header (JSON metadata such as the signing algorithm), a payload (JSON claims about the user and token), and a signature (cryptographic proof that the header and payload were not modified). The three parts are base64url-encoded and joined with dots.
HS256 is symmetric: the same secret key both signs and verifies the token, so every verifier can also forge tokens. RS256 is asymmetric: a private key signs and a public key verifies, so services can verify tokens without being able to create them. RS256 suits microservices and third-party verification.
kid means Key ID. It tells the verifier which key from a key set (usually a JWKS endpoint) was used to sign the token, which enables key rotation. The verifier must look up the key by kid from a trusted source, never accept a key embedded in the token itself.