{ }jsonkitOpen the tool

Understanding JWTs: Structure, Claims, and Verification

8 min read · updated 2026-07-12

A JSON Web Token (JWT) is a compact, URL-safe way to carry claims — statements about a user or session — between two parties. You have almost certainly seen one: a long, cryptic string with two dots in it, sitting in an Authorization: Bearer header or a cookie. JWTs power a huge share of modern authentication and API authorization because they are self-contained: the token itself holds the data a server needs, signed so that tampering is detectable.

The catch is that a JWT is trivially readable by anyone. The payload is not encrypted — it is merely base64url-encoded. This single fact is the source of most JWT security bugs: developers confuse *reading* a token with *trusting* it. This guide walks through the three parts of a JWT, the standard claims, how HS256 and RS256 signatures actually work, and why signature verification is the only thing standing between your API and an attacker. Paste a real token into the JWT Decoder as you read to see each concept in practice.

The Anatomy of a JWT: Three Base64url Parts

A JWT is a single string made of three parts separated by dots: header.payload.signature. Each part is independently base64url-encoded — a URL-safe variant of base64 that swaps + and / for - and _ and drops the = padding, so the token survives inside URLs, headers, and cookies without escaping.

A JWT: header.payload.signature (line breaks added for readability)text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSIsImFkbWluIjp0cnVlLCJpYXQiOjE3MjAwMDAwMDB9
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The first two parts are just JSON that has been encoded. Decode the first segment and you get the header, which describes how the token was signed. Decode the second and you get the payload (also called the claims set). The third part is the signature, computed over the first two.

The header

The header is a small JSON object naming the signing algorithm (alg) and the token type (typ). It may also carry a key ID (kid) so the verifier knows which key to use when several are in rotation.

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

The Payload and Registered Claims

The payload is a JSON object whose fields are called claims. The JWT spec (RFC 7519) defines a set of registered claims — short, standardized names with agreed meanings. You are free to add your own private claims too (roles, tenant IDs, feature flags), but the registered ones are what libraries and identity providers understand.

ClaimNameMeaning
issIssuerWho issued the token (e.g. your auth server's URL).
subSubjectWho the token is about — typically a user ID.
audAudienceWho the token is intended for — the API that should accept it.
expExpiration TimeUnix timestamp after which the token must be rejected.
nbfNot BeforeUnix timestamp before which the token is not yet valid.
iatIssued AtUnix timestamp when the token was created.
jtiJWT IDA unique identifier, useful for revocation or replay detection.

The time-based claims (exp, nbf, iat) are NumericDate values — seconds since the Unix epoch (1970-01-01 UTC), not milliseconds. A common bug is passing JavaScript's Date.now() (milliseconds) directly, which produces timestamps thousands of years in the future.

A realistic decoded payloadjson
{
  "iss": "https://auth.example.com",
  "sub": "user_8f3a21",
  "aud": "https://api.example.com",
  "iat": 1720000000,
  "exp": 1720003600,
  "roles": ["editor"],
  "jti": "a1b2c3d4"
}

The Signature: How HS256 and RS256 Work

The signature is what makes a JWT trustworthy. It is computed over the encoded header and payload — literally the string base64url(header) + "." + base64url(payload) — using the algorithm named in alg. Change a single byte of the header or payload and the signature no longer matches, so verification fails. Two families of algorithms dominate.

HS256 — symmetric (HMAC + SHA-256)

HS256 uses a single shared secret. The same secret both signs and verifies the token, so every party that can verify a token can also forge one. That is fine when the issuer and verifier are the same service (or trust each other completely), and it is fast and simple.

HS256 signature computationtext
signature = HMAC-SHA256(
  key   = your_shared_secret,
  data  = base64url(header) + "." + base64url(payload)
)

RS256 — asymmetric (RSA + SHA-256)

RS256 uses a key pair. The issuer signs with a private key it never shares; anyone can verify with the corresponding public key. This is the right choice when many independent services need to verify tokens but must not be able to mint them — for example, third parties validating tokens from an identity provider. The public key is often published at a well-known JWKS endpoint.

HS256RS256
Key typeOne shared secretPrivate + public key pair
Who can verifyAnyone with the secretAnyone with the public key
Who can signAnyone with the secretOnly the private-key holder
Best forSingle trusted serviceDistributed verifiers / third parties
SpeedFasterSlower to sign, fast to verify

Decoding Is Not Verifying

This is the most important idea in the whole guide. Decoding a JWT means base64url-decoding the header and payload to read the JSON inside — no key required, no trust established. Verifying means recomputing the signature with a key, confirming it matches, and checking the claims. A decoded-but-unverified payload is attacker-controlled input. Treat it exactly as you would treat a raw query string a user typed.

You can decode a JWT with nothing but the standard library, which makes the point concrete: this code reads the payload without ever seeing a key.

Decoding a JWT in Node.js — reads claims, proves NOTHING about authenticityjs
function decodeJwt(token) {
  const [headerB64, payloadB64] = token.split(".");
  const decode = (part) =>
    JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
  return { header: decode(headerB64), payload: decode(payloadB64) };
}

const { header, payload } = decodeJwt(token);
console.log(header.alg);   // "HS256"
console.log(payload.sub);  // "user_8f3a21" — but is it real? Unknown.

Because decoding is unauthenticated, it is safe for non-security uses: inspecting a token while debugging, reading a non-sensitive exp to decide when to refresh on the client, or logging a jti. Use the JWT Decoder for exactly this kind of inspection — but never let a decoded claim make an authorization decision on the server.

JWT DecoderOpen the tool

How Verification Actually Works, Step by Step

Proper verification is a sequence of checks, and skipping any one of them can open a hole. A compliant library does all of this for you, which is the strongest reason not to hand-roll JWT verification.

  1. Split and decode the header to learn the claimed alg and any kid.
  2. Select the key — but only from an allow-list of algorithms you expect. If the header names an alg you did not configure, reject immediately.
  3. Recompute the signature over header.payload with the selected key and compare it, using a constant-time comparison, to the token's signature.
  4. Validate the claims: reject if exp is in the past or nbf is in the future, and confirm iss and aud match what you expect.
  5. Only now may you trust the payload and act on its claims.
Verifying a JWT with the jsonwebtoken libraryjs
const jwt = require("jsonwebtoken");

try {
  // Pin the algorithm(s) and audience/issuer explicitly.
  const payload = jwt.verify(token, SHARED_SECRET, {
    algorithms: ["HS256"],        // reject anything else, incl. "none"
    audience: "https://api.example.com",
    issuer: "https://auth.example.com",
    clockTolerance: 5,            // seconds, for minor clock skew
  });
  // Reaching here means the signature AND exp/nbf/aud/iss all passed.
  console.log("Authenticated subject:", payload.sub);
} catch (err) {
  // TokenExpiredError, JsonWebTokenError, NotBeforeError, etc.
  console.error("Rejected:", err.message);
}

Security Best Practices and the alg:none Attack

Most JWT vulnerabilities are not breaks in the cryptography — they are verification steps that were skipped. A few disciplined habits eliminate the common ones.

  • Always verify the signature. Never make an authorization decision from a decoded-but-unverified payload.
  • Always check `exp`. A token without expiration validation is a permanent credential if it ever leaks.
  • Pin the algorithm. Configure the exact alg you accept; do not let the token's header choose.
  • Keep secrets out of the payload. It is readable by anyone holding the token.
  • Use a strong secret or key. For HS256, a short or guessable secret can be brute-forced offline against a captured token.
  • Validate `aud` and `iss` so a token minted for one service cannot be replayed against another.

The alg:none attack

The JWT spec defines an "alg": "none" value meaning "unsecured — no signature." It exists for cases where integrity is guaranteed by other means, but it is a notorious footgun. In the classic attack, an adversary takes a valid token, edits the payload (say, flipping "admin": false to true), sets the header algorithm to none, and deletes the signature entirely — leaving a trailing dot.

A forged alg:none token — header says none, signature is emptytext
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ1c2VyIiwiYWRtaW4iOnRydWV9.

A verifier that naively trusts the header's alg sees none, performs no signature check, and accepts the forged claims. The defense is the same allow-list from the previous section: because your code only accepts HS256 (or whatever you configured), a token claiming none is rejected before any payload is read. Modern libraries reject none by default, but pinning the algorithm makes the guarantee explicit instead of relying on a default you did not set.

Frequently asked questions

Is a JWT encrypted?

No. A standard JWT (JWS) is signed, not encrypted. The header and payload are only base64url-encoded, so anyone with the token can read every claim. If you need the contents to be confidential, use JWE (JSON Web Encryption) or simply keep sensitive data out of the token.

What is the difference between decoding and verifying a JWT?

Decoding just base64url-decodes the header and payload to read the JSON — no key and no trust involved. Verifying recomputes the signature with a key, confirms it matches, and checks claims like exp, aud, and iss. Only a verified token should drive authorization decisions.

Should I use HS256 or RS256?

Use HS256 (a single shared secret) when the same service issues and verifies tokens — it is simpler and faster. Use RS256 (a private/public key pair) when many independent services need to verify tokens but must not be able to create them, since verifiers only ever hold the public key.

What is the alg:none attack?

An attacker edits a token's payload, sets the header algorithm to "none", and removes the signature. A verifier that trusts the header's alg performs no signature check and accepts the forgery. Prevent it by pinning an explicit list of accepted algorithms so any token claiming none is rejected.

Why is my JWT always expired or never expiring?

The exp, iat, and nbf claims are in seconds since the Unix epoch, not milliseconds. Passing JavaScript's Date.now() (milliseconds) makes tokens appear to expire thousands of years in the future; dividing by 1000 fixes it. An always-expired token usually means server clock skew — allow a few seconds of tolerance.

Can I trust the data in a JWT payload on the client?

You can read it for non-security purposes like showing a username or deciding when to refresh. You must not make security decisions from it client-side, because the client can never safely hold the verification key and any value could be forged. Authorization must be verified on the server.