Notes
The three parts of a JWT, and what verification checks
Decoding the header and payload does not mean the token can be trusted. This note separates Base64URL, the signature, and claim checks, and explains why the algorithm and the key have to be chosen together.
By Brook/Updated 2026-10-09/12 min read
It is three pieces of text joined by dots
A JWT looks like header.payload.signature. All three parts are text. There is no encrypted envelope. Anyone who has the string can read the first two parts without a key. The key exists so the third part can be checked against the first two: change one character and the signature no longer matches.
Decoding and trusting are different jobs. Decoding is useful while debugging, because you can see which claims were issued. Accepting a token in production because it decoded is the same as letting anyone write their own header and payload. A decoder in the browser is for inspection, not for authorization.
Header and payload are Base64URL, not ciphertext
- 01SplitThree parts on dotsFewer or more parts is not a complete JWT.
- 02HeaderBase64URL decodeJSON with alg and typ.
- 03PayloadBase64URL decodeClaims: sub, exp, and anything custom.
- 04SignatureDo not parse as JSONIt is a digest over the first two parts.
Base64URL replaces + with -, / with _, and often drops the = padding. A standard Base64 decoder will fail on those two characters or on the missing padding. Swap the characters back and restore = by length, and you have the same bytes. The decoded header and payload should be JSON objects, not arbitrary text.
The field that matters in the header is alg. It names the algorithm. HS256 is a symmetric HMAC: the same key signs and verifies. RS256 and ES256 are asymmetric: a private key signs, a public key verifies. typ is usually JWT. It is a hint, not a substitute for checking the algorithm.
What the signature actually proves
The signed input is the Base64URL header, a dot, and the Base64URL payload: the exact characters before the second dot. It is not "decode the JSON and stringify it again". A verifier has to hash the original string it received. Re-serializing changes key order or whitespace, the digest changes, and a valid token fails.
HMAC feeds that string and the key into a hash. An asymmetric algorithm signs a digest with a private key, and verification uses the public key. In both cases the signature says "someone holding the key accepted this header and payload". It does not make the claims true, and it does not hide them. A role written into the payload is still just a claim the issuer made.
Verification looks at these steps, in order
- 01ClaimsCheck exp, nbf, iss, audA valid signature on an expired token is still a rejection. Clock skew stays small.
- 02SignatureVerify part three with the agreed keyCompare in constant time. A short-circuit string compare leaks where the bytes differ.
- 03Algorithmalg must be on an allow-listDo not pick the key from whatever algorithm the token names. The server decides.
- 04ShapeThree parts, Base64URL, JSONIf the shape is wrong, skip the cryptography.
exp is the expiry and nbf is the not-before time, both in Unix seconds. Checking the signature and ignoring the clock accepts a ticket that has already been withdrawn. iss and aud confirm the token was meant for this service. Custom claims are fine. Do not redefine the standard ones.
The algorithm and the key are one decision
A known failure mode is to read alg from the token and, if it says none, skip the signature. An attacker then writes a header and payload and leaves the signature empty. The server should decide "I only accept HS256" or "I only accept RS256" first. Anything else is rejected. none belongs in an explicit test config, not in production.
Another failure mode mixes algorithms. The server uses the PEM text of an RSA public key as an HMAC secret, and it also lets the token choose HS256. An attacker signs an HS256 token with that public key, which was public already, and verification succeeds. An allow-list has to say which key is legal for which algorithm. Public keys and symmetric secrets do not belong in one lookup table.
Decode in the browser, verify on the server
Decoding in the page answers "what did the issuer write?". The key should not live in the frontend for the decision that actually grants access. A symmetric key in the browser is public. A public-key check in the frontend can be skipped by editing the page. The frontend can warn about a bad shape or an obvious expiry. The check that lets a request through belongs on the server that holds the key.
The JWT tool on this site only decodes: split the three parts, restore the header and payload with Base64URL, and show the claims. It does not keep a key, and it does not send the token to a server. Use it to inspect what you issued. Do not use it as the authorization step.
Tools mentioned here
Keep reading
- How JSON parsing works, from characters to a valueFormat, minify, and tree view look like three buttons. Underneath they share one pipeline: split the text into tokens, then fold those tokens into a value. This note walks that path and shows where line numbers come from.
- What UTF-8 actually encodesCharacter counts, code points, and bytes are three different rulers. This note walks one code point through UTF-8 and explains why URL encoding and JSON escapes look nothing like each other.
- How an SSE stream reassembles a model reply from deltasModel APIs often split one reply across many SSE events. This note covers the frame format, how deltas merge by path, and why tool-call arguments are concatenated strings rather than JSON on every frame.