Skip to main content

JWT Structure Explained: Header, Payload, Signature, and How to Decode Them Safely

A practical breakdown of the three parts inside every JWT token — what each section contains, how the signature ties them together, and how to decode claims without accidentally trusting an unverified token.

Published By Lei Li
#jwt #authentication #developer #security #base64url

JWT Structure Explained: Header, Payload, Signature, and How to Decode Them Safely

Every JWT you have ever seen looks the same at a glance: three chunks of text joined by dots. But those three chunks encode very different information, and conflating them is one of the most common authentication mistakes I've seen in code review.

This article walks through exactly what each part contains, how the cryptographic signature binds the first two together, and what "safe decoding" actually means in practice. Every claim is backed by a real token you can try yourself in Toolora's JWT Decoder and Claims Inspector.

The Three-Part Structure

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1MTIzIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzQ5NDE3NjAwLCJleHAiOjE3NDk0MjEyMDB9.Kq2iSsLFzT3e4rPtmHvNQWx8bJcYAoD6uXpGfRlEhMk

Split on the . character and you get three Base64URL-encoded segments:

  1. HeadereyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  2. PayloadeyJzdWIiOiJ1MTIzIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzQ5NDE3NjAwLCJleHAiOjE3NDk0MjEyMDB9
  3. SignatureKq2iSsLFzT3e4rPtmHvNQWx8bJcYAoD6uXpGfRlEhMk

Each segment uses Base64URL encoding (RFC 4648 §5), not standard Base64. The key difference: + and / are replaced by - and _, and padding = characters are stripped. That makes the token safe to drop directly into a URL query parameter or an HTTP Authorization header without percent-encoding.

Part 1: The Header

Decode the first segment and you get a small JSON object:

Input: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Decoded output:

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

The header describes the token format and the signing algorithm. The alg claim is critical: it tells the verifying server which algorithm was used to generate the signature in part three. Common values include:

| alg value | Algorithm | Key type | |---|---|---| | HS256 | HMAC-SHA256 | Shared secret | | RS256 | RSA-SHA256 | Public/private key pair | | ES256 | ECDSA P-256 | Public/private key pair | | none | No signature | None (dangerous, reject it) |

The alg: none case is historically significant. In 2015, Tim McLean published research showing that many JWT libraries at the time accepted tokens with "alg": "none" as valid, even though an unsigned token provides zero authentication. Per the analysis, libraries from Auth0, python-jwt, php-jwt, and others were affected. Modern libraries reject none by default, but older codebases and misconfigured setups still appear in the wild.

Part 2: The Payload (Claims)

Decode the second segment:

Input: eyJzdWIiOiJ1MTIzIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzQ5NDE3NjAwLCJleHAiOjE3NDk0MjEyMDB9

Decoded output:

{
  "sub": "u123",
  "name": "Alice",
  "iat": 1749417600,
  "exp": 1749421200
}

The payload holds the actual claims — assertions about the token subject and metadata about the token itself. RFC 7519 defines three categories:

Registered claims — standardized names with specific semantics:

  • sub (subject): who the token is about, here user ID u123
  • iat (issued at): Unix timestamp when the token was created
  • exp (expiration): Unix timestamp after which the token must be rejected
  • iss (issuer): which server minted the token
  • aud (audience): which server(s) should accept the token

Public claims — names registered with IANA to avoid collisions, like email or name.

Private claims — application-specific fields agreed upon between parties, like role: "admin" or org_id: "acme".

The exp and iat values in the example above (1749417600 and 1749421200) differ by 3600 seconds — a one-hour window. I tested this token in the Toolora decoder and the tool correctly flagged the token as expired and showed the exact UTC time both timestamps resolve to, which is far faster than running a manual date -d @1749421200 in a shell.

Important: the payload is encoded, not encrypted. Anyone who holds the token can read every claim inside it. Do not store passwords, credit card numbers, or any sensitive personal data in a JWT payload unless you are using JWE (JSON Web Encryption) rather than JWS (JSON Web Signature).

Part 3: The Signature

The signature is how the token proves it hasn't been tampered with. For HS256, the server computes:

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

The result is then Base64URL-encoded and appended as the third segment. Because the header and payload are hashed together with a secret the client doesn't know, any modification to either part — changing "role":"user" to "role":"admin", adjusting the exp timestamp, swapping the algorithm — produces a signature that won't match what the server recomputes on its side.

For RS256, the flow is the same but uses a private key to sign and a public key to verify. This lets you publish the public key openly (many providers expose a JWKS endpoint at /.well-known/jwks.json) while keeping the signing key private. A token signed by the server can be verified by any party that fetches the public key.

The Base64URL encoding of the raw HMAC or RSA output bytes is what appears in the third segment. You can encode and decode those raw bytes with Toolora's Base64URL Encoder/Decoder for JWT-safe strings if you want to inspect the signature bytes directly.

Safe Decoding vs. Trusting a Token

This distinction matters more than most tutorials acknowledge. "Decoding" a JWT means base64url-decoding the header and payload to read the JSON. "Verifying" a JWT means recomputing the signature against the header and payload and confirming it matches.

You can decode a JWT without verifying it. Dev tools do this all the time — the JWT debugger at jwt.io, the Toolora inspector, curl + base64 on the command line. That's useful for debugging. But decoded ≠ trusted.

Safe decoding for production code means:

  1. Verify the signature first, using a library that defaults to strict algorithm checking.
  2. Check exp: reject any token where exp < now.
  3. Check iss and aud if your deployment uses multiple issuers or services.
  4. Reject alg: none explicitly, don't rely on library defaults.
  5. Never decode on the client side and treat claims as authoritative without a server-side re-verification.

I've seen codebases that decoded the payload in a browser, read the role field, and used it to control UI visibility. That's fine for cosmetics. It becomes a security issue when the server-side code skips verification and trusts the same decoded role to authorize API calls.

The OWASP API Security Top 10 (2023 edition) lists "Broken Authentication" as API2, and JWT misconfiguration — specifically skipping signature verification or accepting weak algorithms — is one of the canonical examples. The fix is simple: always verify with a battle-tested library (jsonwebtoken in Node, python-jose in Python, java-jwt in Java) and never write your own signature logic.

When the Three Parts Don't All Show Up

A compact JWE (encrypted JWT) has five parts, not three. The structure is:

header.encrypted_key.iv.ciphertext.tag

If you paste a five-part token into a standard JWT decoder, it will appear to fail or show garbage in the payload segment — because the payload is ciphertext, not a Base64URL-encoded JSON object. Watch for this when debugging tokens from providers that use JWE (Azure AD, for example, can issue encrypted tokens for certain resource types).

A quick way to tell: if the payload segment contains characters from the full Base64URL alphabet but decodes to binary noise rather than { starting a JSON object, you likely have a JWE, not a JWS.


Made by Toolora · Updated 2026-06-09