Polyatic

JWT Structure Explained

A JSON Web Token looks like a wall of random characters, but it is not random and it is not secret. It is three small pieces of JSON, lightly encoded and joined by dots: header.payload.signature. Once you can see those three parts and read what is inside them, JWTs stop being mysterious — and, more importantly, you stop making the two mistakes that cause real security incidents: believing a token is encrypted, and trusting its contents before checking the signature. This guide takes a token apart from first principles, shows a worked payload with the standard claims, and states the honest rules for handling one.

The anatomy: three dot-separated parts

A compact JWT is exactly three strings separated by two dots. Split on those dots and you get the header, the payload, and the signature, in that order:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSJ9.Sf1KxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c └────────── header ──────────┘ └──────────── payload ────────────┘ └──────── signature ────────┘

The first two parts — header and payload — are just JSON objects that have been Base64URL-encoded. Base64URL is the URL-safe flavour of Base64 (it swaps + and / for - and _ and drops the = padding) so the token survives inside URLs and HTTP headers. The crucial fact: this encoding has no key. Anyone who has the token can decode the header and payload back into readable JSON. The third part, the signature, is the only piece that involves a secret, and it is what makes the token trustworthy — if you check it.

Part 1 — the header

Base64URL-decode the first segment above and you get a tiny JSON object:

{ "alg": "HS256", "typ": "JWT" }

The header describes how the token is signed, not what it contains. alg names the signing algorithm — here HS256, meaning HMAC with SHA-256, a symmetric scheme where one shared secret both signs and verifies. typ is almost always "JWT". You may also see kid (a key ID that tells the verifier which of several keys to use). The header matters for security because the recipient uses alg to decide how to check the signature — and, as we will see, blindly trusting that field is a classic vulnerability.

Part 2 — the payload and its claims

The middle segment is where the data lives. Each key/value pair is called a claim — a statement the issuer is asserting. Here is a fuller worked example payload, decoded from Base64URL:

{ "iss": "polyatic.com", "sub": "1234567890", "aud": "polyatic-api", "name": "Ada Lovelace", "role": "member", "iat": 1516239022, "nbf": 1516239022, "exp": 4102444800 }

Some of those keys are custom — name and role are whatever the issuer chose to put in. The short three-letter keys are registered claims, standardised in RFC 7519 so every library agrees on their meaning:

ClaimNameMeaning
issIssuerWho created and signed the token (polyatic.com).
subSubjectWho the token is about — usually a user ID (1234567890).
audAudienceThe service the token is meant for; a recipient should reject a token addressed to someone else.
expExpirationThe instant after which the token must be rejected.
iatIssued AtWhen the token was created.
nbfNot BeforeThe instant before which the token is not yet valid.

The three time claims — exp, iat and nbf — are NumericDates: plain integer seconds since 1 January 1970 UTC (the Unix epoch), never a formatted date string and always UTC. In the example, iat and nbf are both 1516239022, which is 18 January 2018, 01:30:22 UTC — the token was issued and became valid at that instant. exp is 4102444800, which is 1 January 2100, 00:00:00 UTC, so this particular token is (deliberately) valid for the rest of the century. A verifier compares these numbers to the current time: reject if now is past exp, or before nbf.

Part 3 — the signature

The third segment is not JSON and does not decode to anything readable — it is raw signature bytes, Base64URL-encoded. It is produced by feeding the exact string base64url(header) + "." + base64url(payload) into the algorithm named in alg, together with a key. For HS256 that is:

signature = HMAC-SHA256( base64url(header) + "." + base64url(payload), secret )

Because the signature covers the header and payload, changing even one character of either makes the recomputed signature no longer match. That is the whole security model: the signature does not hide the data, it seals it. A recipient who holds the secret (for HS*) or the issuer's public key (for RS*, ES*, EdDSA) recomputes the signature and compares. If it matches, the token is authentic and untampered; if not, it is rejected outright.

Signed (JWS) is not encrypted — the biggest myth

This is the misconception worth burning into memory: a normal JWT is signed, not encrypted. The technical name for the signed form is JWS (JSON Web Signature). Signing guarantees integrity and origin — "this came from the issuer and nobody changed it" — but it provides zero confidentiality. The payload is Base64URL, which, as shown above, anyone can decode in a second. There is an encrypted variant called JWE (JSON Web Encryption), which hides the payload, but it is comparatively rare and looks different (five parts, not three). Unless you have deliberately built JWE, assume every claim in your token is public. The practical rule: never put a password, credit-card number, API key, or any secret in a JWT payload — you are effectively printing it in plain text on anything that carries the token.

The alg: none footgun

The JWT spec allows an unsecured token whose header says { "alg": "none" } and whose signature segment is empty. It exists for cases where integrity is guaranteed by other means, but it is a notorious source of vulnerabilities. If a server accepts alg: none, an attacker can take any token, set the algorithm to none, rewrite the payload to say "role": "admin", drop the signature entirely, and the token still parses cleanly. A related attack flips an RS256 token to HS256 and signs it using the server's public key as if it were the HMAC secret. Both are defeated the same way: the server must pin the algorithm(s) it expects and reject anything else — never let the token's own alg header choose how it is verified.

The honest security rule: decode is not verify

Everything above collapses into one discipline. Decoding a JWT tells you what it claims; it does not tell you whether those claims are true. Reading the payload is trivial and keyless — that is by design. Acting on it safely requires three server-side checks every time: (1) recompute and match the signature with the correct, algorithm-pinned key; (2) confirm the current time is between nbf and exp; and (3) check iss and aud are the ones you expect. Skip any of these and you are trusting attacker-controllable data. A decoded payload on its own is an unverified assertion, never proof of identity or permission.

Take a real token apart right now

Paste any JWT and see the header, payload and claims decoded live, with each NumericDate turned into a human date — and because it runs entirely in your browser, the token never leaves your device.

Try the JWT decoder →

Putting it together

A JWT is three parts joined by dots: a header that names the signing algorithm, a payload of claims that anyone can read because it is only Base64URL-encoded, and a signature that seals the first two so tampering is detectable. The registered claims — iss, sub, aud, exp, iat, nbf — give a token its identity and its lifetime, expressed as UTC Unix seconds. Signing is not encryption, so keep secrets out of the payload; alg: none and algorithm confusion are real attacks, so pin the algorithm; and above all, decoding is not verifying — validate the signature and expiry on the server before you believe a single claim.

Open the JWT decoder → to inspect a token field by field offline, or browse all Polyatic tools.