Reading a JWT: header, payload, and signature explained
If you work with JWT (JSON Web Token), you have seen long opaque strings like eyJhbGciOiJIUzI1NiIs.... A decoder will reveal the contents instantly, but understanding why this format exists matters when you implement token verification yourself. This article walks through the structure of a JWT from the implementer’s perspective.
Overall structure: three Base64URL strings joined by dots
A JWT is, structurally, very simple:
<base64url(header)>.<base64url(payload)>.<base64url(signature)> Splitting an actual token by . always gives exactly three parts. split('.').length === 3 is enough to use as the first sanity check.
The format is defined in RFC 7519. Signature mechanics live in RFC 7515 (JWS), and encryption in RFC 7516 (JWE). What is commonly called “a JWT” in the wild is almost always a JWS — a signed JWT.
Header: a tiny JSON declaring the signing algorithm
The first segment is the header, a JSON object such as:
{
"alg": "HS256",
"typ": "JWT"
} alg— the signing algorithm. HMAC-based (HS256/HS384/HS512) and asymmetric (RS256,ES256, …) are the most common.typ— the format declaration;"JWT"for a JWT.
The alg: "none" trap
The JWT spec includes alg: "none" for unsigned tokens. Historically, many libraries accepted this incorrectly, allowing attackers to flip the header to none and bypass signature verification. When you write verification code, hold an explicit allowlist of accepted algorithms and reject anything else.
Payload: a JSON of claims
The second segment is the payload, which carries authentication data and arbitrary fields:
{
"sub": "user-1234",
"name": "ibukish",
"iat": 1700000000,
"exp": 1700003600
} Each key is called a claim, and they fall into three categories:
- Registered claims — short, reserved names defined by the spec:
iss(issuer),sub(subject),aud(audience),exp(expiry),iat(issued-at),nbf(not-before),jti(unique ID). - Public claims — names registered with IANA or qualified by URI to avoid collisions.
- Private claims — application-specific keys.
A common pitfall is the unit of time fields. exp and iat are Unix seconds, not the milliseconds that JavaScript’s Date.now() returns. If you forget to divide by 1000, the expiry lands 38 years in the future, and the only sign of trouble may be the verifier rejecting the token as “too far in the future”.
Signature: how tampering is detected
The third segment is what turns “Base64-encoded JSON” into a token you can trust.
The signed input is the Base64URL-encoded header and payload joined by a dot — not the signature itself:
signing_input = base64url(header) + "." + base64url(payload)
signature = HMAC_SHA256(signing_input, secret) // when alg = HS256 Verifiers split the incoming token, concatenate the first two segments, recompute the signature with the same algorithm, and compare against the third segment.
Why Base64URL instead of standard Base64
JWTs end up in URLs and HTTP headers, where + and / cause trouble. Base64URL substitutes:
+→-/→_- and drops the trailing
=padding.
A custom decoder needs to convert - back to +, _ back to /, pad with = to a length that is a multiple of four, then feed it into a standard Base64 decoder.
“If the payload is just Base64, isn’t this insecure?”
A common reaction the first time you see a JWT. The misunderstanding is about the goal.
JWT guarantees integrity, not confidentiality. The contents are designed to be readable; the signature ensures the data arrived as the server signed it. Putting secrets in a JWT payload is wrong — use JWE (encrypted JWT) for confidentiality, or keep secrets server-side in a session store.
Implementation tips worth keeping in mind
- Allowlist for
alg— fix the set of accepted algorithms; rejectnoneand anything unexpected. - Time claims are seconds — remember to divide
Date.now()by 1000. - HMAC secrets must be long enough — at least 256 bits (32 bytes) of randomness for HS256.
- Cache JWKS keys — when fetching public keys remotely, do not refetch on every request.
- Account for clock skew — give
expchecks a few seconds of leeway.
When you only need to peek at what is inside a token, the decoder on this site is the fastest path. Signature verification is skipped, and only the header and payload are decoded in your browser, so the token never leaves your device.