What a JSON Web Token actually contains

Inside a JWT, the header, payload and signature explained, the standard claims, and why Base64url decoding a token is not the same as verifying it.

A JSON Web Token (JWT) is three chunks of text joined by full stops. Each chunk is Base64url encoded, and the first two are just JSON. Anyone who has the token can read what is inside it. Nothing in a normal JWT is encrypted.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9      <- header
eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBbGV4In0   <- payload
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFW     <- signature

Joined with full stops, that is the token you paste into a request. Decode the first two parts and you get ordinary JSON:

{ "alg": "HS256", "typ": "JWT" }
{ "sub": "12345", "name": "Alex" }

A real payload usually carries timestamps and an audience as well:

{
  "iss": "https://auth.example.com",
  "sub": "12345",
  "aud": "api.example.com",
  "iat": 1756000000,
  "exp": 1756003600,
  "roles": ["editor"]
}

The header

The header describes how the token is signed. Two fields matter most:

  • alg names the signing algorithm, for example HS256 (HMAC with SHA-256, using a shared secret) or RS256 (RSA signature with SHA-256, signed with a private key and checked with a public one).
  • kid, the key ID, is optional and tells the recipient which of several keys was used, so keys can be rotated without breaking existing tokens.

typ is usually just JWT and carries no security meaning.

The payload and its claims

The payload is a JSON object of claims, which is simply the specification's word for statements about the subject of the token. Some claim names are registered by the standard, and they are all short:

ClaimNameWhat it holds
issIssuerWho created the token, usually a URL or service name
subSubjectWho the token is about, usually a stable user ID
audAudienceWho the token is intended for, so a service can reject tokens meant for a different one
expExpiration timeThe instant after which the token must be rejected
nbfNot beforeThe instant before which the token must be rejected
iatIssued atWhen the token was created
jtiJWT IDA unique identifier, useful for tracking or blocking individual tokens

exp, nbf and iat are all NumericDate values, meaning seconds since 1 January 1970 UTC. They are seconds, not milliseconds. If a timestamp decodes to a date in the year 57,000, you are reading milliseconds and need to divide by a thousand.

Everything else in the payload is whatever the issuer put there: roles, permissions, an email address, a tenant ID. Because a private claim name could one day collide with a registered one, issuers often namespace their own with a URL-like prefix.

The signature

The signature is computed over the exact text of the first two parts joined by a dot, using the algorithm from the header and a key. Change a single character of the header or payload and the signature no longer matches.

With HS256 the same secret both signs and verifies, so anyone who can check a token can also mint one. With RS256 or ES256 a private key signs and a public key verifies, which is what lets an identity provider hand out tokens that many separate services can validate without holding anything secret.

Base64url is not encryption

Base64url is the same idea as Base64, with two differences: it uses - and _ in place of + and / so the result is safe in URLs, and the trailing = padding is usually stripped. It is an encoding, a reversible way of writing bytes as text. It provides no confidentiality whatsoever.

So never put anything sensitive in a JWT payload: no passwords, no card numbers, no internal notes about the user. Assume the person holding the token, and anyone who reads it out of a log file or browser storage, can see every field.

There is a separate format, JWE, that does encrypt the payload, and it has five parts rather than three. If your token has three parts, it is signed and readable.

Decoding is not verifying

This is the point that matters. A decoder shows you what a token says. It does not tell you whether the token is genuine. Verification is a separate step and needs the key.

To actually trust a token, a server has to:

  1. Check the signature against the correct key.
  2. Insist on the algorithm it expects, rather than trusting the alg field in the token. Two classic attacks come from skipping this: setting alg to none and stripping the signature, and taking a service's RSA public key and using it as an HMAC secret so an RS256 verifier is tricked into running HS256.
  3. Check exp and, if present, nbf, allowing a small clock skew.
  4. Check iss and aud match what this service expects.

Only then do the claims mean anything. Until then the payload is an unverified string that arrived over the network.

Practical notes

  • Tokens are usually sent in an HTTP header as Authorization: Bearer <token>, so keep the payload small. Every claim you add is sent on every request.
  • A signed token stays valid until it expires. There is no built-in way to revoke one, which is why access tokens tend to have short lifetimes, often minutes, with a separate refresh token used to get new ones.
  • If a token looks malformed, count the dots first. Two dots and three parts is a normal signed JWT. A truncated token pasted from a terminal is a very common cause of "invalid signature".
  • Decoding a token to read its expiry, its subject or its roles while debugging is completely safe and does not require any secret. That is exactly what the payload is for.