JWT Structure Explained: Header, Payload, and Signature (With a Decoded Example)

A JWT looks like one opaque string, so people treat it like a password: secret, scrambled, safe to trust if it parses. It is none of those. It is three pieces of text joined by dots, two of which are plain JSON that anyone can read in a second, and one of which is the only part that means anything for security.

This guide takes one real token and decodes it segment by segment, so you can see what each part is and what it is not. Two ideas carry most of the weight: the payload is encoded, not encrypted, and reading a token is not the same as verifying it. Get those two straight and most JWT confusion goes away.

You can follow along live by pasting any token into the JWT Decoder. Everything below is checkable against what it shows.

Which RFC actually defines the three parts

People say "RFC 7519" for everything about JWTs. That is half right, and the wrong half causes real misunderstandings.

  • RFC 7519 (JWT) defines the concept of a JSON Web Token and the registered claim names that go in the payload: iss, sub, aud, exp, nbf, iat, jti. It is about meaning, not wire format.
  • RFC 7515 (JWS) defines the structure you actually see: three Base64url segments joined by dots, with a signature computed over header.payload. The dotted shape is a JWS thing, not a JWT thing.
  • RFC 4648 §5 defines Base64url, the specific alphabet each segment uses.

So a JWT is a set of claims; the compact aaa.bbb.ccc string is a JWS that carries those claims. Almost every "JWT" you handle is a JWS. The other option is a JWE (RFC 7516), which is encrypted and has five segments, not three. If you ever see five dot-separated parts, you are looking at a JWE and a plain decode will not show you the payload.

This explains why you can read a JWT without any key (the JWS payload is not encrypted) and why the signature, not the structure, is what stops tampering.

The example token, decoded

Here is the token. It is the canonical example from jwt.io, which makes it ideal: the values are checkable, and the signature actually validates with the secret your-256-bit-secret.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Three segments, two dots. Split on the dots and you get exactly three strings. Decode the first two from Base64url and you get JSON. The third is raw signature bytes that do not decode to anything human-readable, and that is expected.

SegmentBase64url charsDecodes to
Header36{"alg":"HS256","typ":"JWT"}
Payload74{"sub":"1234567890","name":"John Doe","iat":1516239022}
Signature4332 raw bytes (HMAC-SHA256 output)

That is the whole token. No encryption anywhere. If John Doe were a real user, that name and ID would be sitting in plain JSON inside a string you might be logging, putting in a URL, or pasting into a chat. Treat the payload as public, because it is.

The header: how the token is signed, claimed by the token itself

Take the first segment, eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9, decode it from Base64url, and you get:

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

Two fields here:

  • alg names the algorithm used for the signature. HS256 means HMAC with SHA-256, a shared-secret scheme. The same secret signs and verifies.
  • typ is a media type hint, almost always JWT. It is informational.

Real tokens often add a kid (key ID) so the verifier knows which key from a key set to use, and sometimes a jku URL pointing at a JWKS endpoint. Those exist to help the verifier find the right key.

Here is the trap, and it is the root of the most famous JWT attacks: the header is supplied by whoever made the token, which can include an attacker. A verifier that reads alg from the header and trusts it is letting the attacker choose the verification rules. The fix is to decide the expected algorithm out of band and ignore the header's claim about it. The JWT Decoder flags the two header-trust failures (the alg:none trick and RS256-to-HS256 confusion) in its scan mode rather than reproducing them here, but the principle is one line: the header is a hint, not an instruction.

The payload: readable claims, not secrets

The second segment, eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ, decodes to:

{"sub":"1234567890","name":"John Doe","iat":1516239022}

Three claims. sub (subject) is a registered claim from RFC 7519 and usually holds the user or principal ID. name is a custom claim this issuer chose to include. iat (issued at) records when the token was minted. You can put anything JSON-serializable in here.

What you should not do is put anything you would not hand to the bearer. There is no key involved in reading this. Base64url is a transport encoding, the same family as the Base64 you have decoded a thousand times, just with a URL-safe alphabet. Decoding is reversal, not decryption. A common real-world leak is a JWT with an email, a role like "admin":true, or an internal account number in the payload, sitting in browser localStorage or a server log where it is one copy-paste away from being read.

The standard claims are worth knowing because verifiers act on them: exp (expiry) and nbf (not before) bound the validity window, iss and aud say who minted the token and who it is for, iat records when it was issued, and jti gives it a unique ID for revocation lists. Our example carries iat but no exp, so a strict verifier checking expiry would accept it forever. That is a property of this demo token, not a good default. Real tokens should always carry exp.

If you want to see the encoded-versus-readable point with your own eyes, run the payload string through the Base64url converter. No key, no signature, the JSON falls right out. That is the whole argument for why a JWT payload is not a place for secrets.

The signature: the only part that resists tampering

The third segment, SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c, is not JSON and does not decode to text. It is 43 Base64url characters that represent 32 raw bytes. That 32 is not arbitrary: HS256 is HMAC-SHA256, SHA-256 produces a 256-bit digest, 256 bits is 32 bytes, and 32 bytes encode to 43 Base64url characters with the trailing padding stripped. If you ever see an HS256 signature that is not 43 characters, something is wrong with the token.

The signature is computed over the exact ASCII of header.payload, the first two segments including the dot, signed with the secret. To verify, you recompute the HMAC over those same two segments with the same secret and compare. Match means the token has not been altered since it was signed; mismatch means a byte changed somewhere, or the wrong key was used.

This is the part the whole structure rests on. Change a single character in the payload, say flip the name from John Doe to Jane Doe, and the recomputed signature no longer matches the one in the token. That is the security model: not hiding the data, but detecting changes to it.

One detail people miss: the signature in the token tells you nothing on its own. You cannot look at SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c and know whether it is valid. You can only know by recomputing it with the key.

Decoding is not verifying

A decoder splits the dots and Base64url-decodes the first two segments. That is it. It does not touch the signature, it needs no key, and it will happily show you the contents of a completely forged token. Decoding answers "what does this token claim?" It does not answer "is this token genuine?"

Verifying recomputes the signature with the correct key and checks it, then checks the time claims (exp, nbf) and the audience and issuer. It needs the secret (for HMAC) or the issuer's public key (for RSA, ECDSA, EdDSA). Verifying answers "should I trust what this token claims?"

Why this distinction is not academic: jwt.io-style decoders and debug panes show you a friendly, parsed view of any token. That view feels authoritative. Developers occasionally wire decode-only logic into application code, read the role or user ID straight out of the decoded payload, and skip verification entirely. An attacker who can hand you a token then just edits the payload to "admin":true and re-encodes it. No key needed, because nothing is checking the signature.

The rule for application code: never make a trust decision on a decoded-but-unverified token. Decode for display and debugging; verify before you act. The JWT Decoder keeps these as separate modes on purpose. Decode mode reads. Verify mode takes a secret or PEM public key and gives you a pass/fail with a reason. Paste the example token into Verify mode with the secret your-256-bit-secret and it passes; change one character of the payload first and it fails.

Base64url, not Base64, and why the difference shows

JWT segments use Base64url (RFC 4648 §5), not standard Base64. Two practical differences:

  • The alphabet swaps + for - and / for _, so the string survives being put in a URL or an HTTP header without escaping.
  • Trailing = padding is stripped.

You can spot it in our example: the signature segment contains _ (in JV_adQssw5c) and the whole token has no = anywhere. That single underscore is proof it is Base64url. If you tried to decode these segments with a strict standard-Base64 decoder, the - and _ characters and the missing padding can throw errors or produce garbage. This is a common cause of "my JWT library works but my hand-rolled decode fails."

If a token ever arrives genuinely URL-encoded on top of the Base64url (percent-escaped), unescape it first with a URL decoder before splitting on dots. The dots themselves are never escaped; they are structural.

A quick mental checklist for any token

When you are handed a JWT and need to reason about it fast, run this in your head:

  • Count the dots. Two dots, three parts: a signed JWS, decodable. Four dots, five parts: a JWE, encrypted, you need a key to see anything.
  • Decode the header. Read alg. If your verifier trusts that field instead of pinning the algorithm, that is a bug waiting to happen.
  • Decode the payload. Assume everything in it is public. Check for exp; a token without expiry is a token that never dies.
  • Do not trust the signature segment by looking at it. Its presence proves nothing. Only a verification with the right key proves anything.
  • Decide what you are doing. Inspecting? Decode is fine. Authorizing? Verify, every time.

Paste a token into the JWT Decoder to run the whole sequence at once: it splits, decodes, surfaces the standard claims, and, when you give it a key, tells you whether the signature actually holds. Decode to understand, verify to trust.

Use the tool

Skip the manual work. The companion tool runs this in your browser, with nothing uploaded.

JWT Decoder, Verifier & Generator

Frequently asked questions

Is the payload of a JWT encrypted?

No. The payload is Base64url-encoded JSON, which is trivially reversible without any key. Anyone holding the token can read every claim in it. Encoding is a transport format, not protection. If you need the contents hidden, use a JWE (encrypted) or only ever send the token over TLS, and never put secrets in the payload of a signed JWT.

What is the difference between decoding and verifying a JWT?

Decoding splits the token on its dots and Base64url-decodes the header and payload to readable JSON. It needs no key and works on forged tokens. Verifying recomputes the signature with the correct key and checks the time and audience claims, which tells you whether the token is genuine and current. Decoding answers what the token claims; verifying answers whether you should trust it. Never make an auth decision on a decoded-but-unverified token.

Why does RFC 7519 not define the header.payload.signature structure?

RFC 7519 defines the JWT concept and the registered claim names that live in the payload. The three-segment dotted wire format and the signature over header.payload come from RFC 7515 (JWS). A JWT is a set of claims; the compact string you see is a JWS carrying them. Base64url itself is defined in RFC 4648 section 5.

Why is an HS256 signature always 43 characters?

HS256 is HMAC with SHA-256. SHA-256 outputs a 256-bit digest, which is 32 bytes. Encoding 32 bytes as Base64url with padding stripped gives 43 characters. So an HS256 signature segment is consistently 43 Base64url characters. A different length for a token claiming HS256 is a sign that something is off.

How can I tell a JWS from a JWE?

Count the segments. A JWS (the normal signed JWT) has three Base64url parts separated by two dots. A JWE is encrypted and has five parts separated by four dots. A plain decoder will show you the contents of a JWS but cannot reveal a JWE payload without the recipient's private key. If a token has five segments, you are looking at an encrypted JWE.

Why does my JWT fail to decode with a standard Base64 decoder?

JWT segments use Base64url, which replaces + with - and / with _ and strips the = padding so the token is URL-safe. A strict standard-Base64 decoder can choke on those substituted characters or the missing padding. Use a Base64url decoder, or add the padding back and translate the alphabet before decoding.

Related tools