Engineering·Authentication

What is actually inside a JWT

A JSON Web Token is three base64url segments, and only the third one protects anything. Here is what each part holds, what it does not hide, and the mistakes that follow from getting that backwards.

Level
Beginner
Read
9 min
Updated
2026-09-08

Before you start

  • Comfort reading JSON
  • A token you are allowed to inspect, ideally from a test environment

Almost every argument about JSON Web Tokens comes from one misunderstanding: people assume a signed token is a private one. It is not. A JWT is readable by anyone holding it, and the signature protects only against modification, never against reading.

Get that one fact straight and most of the rest follows.

The three segments #

Paste a token anywhere and you will see something like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJIZW5yeSIsImV4cCI6MTc2NzIyNTYwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36P

Three chunks, separated by dots. In order: the header, the payload, and the signature.

The first two are base64url encoded JSON. Encoded, not encrypted. Anyone can reverse them with a single function call:

decode-a-segment.js
const [header, payload] = token
  .split('.')
  .slice(0, 2)
  .map((segment) =>
    JSON.parse(Buffer.from(segment, 'base64url').toString('utf8'))
  );

console.log(header);  // { alg: 'HS256', typ: 'JWT' }
console.log(payload); // { sub: '12345', name: 'Henry', exp: 1767225600 }

Watch out

That is all it takes. If you have put an internal user role, an email address, or anything else you would not paste into a public channel inside a payload, treat it as published.

The header #

The header is small and boring, which is appropriate. It says how the token was signed:

header.json
{
  "alg": "HS256",
  "typ": "JWT"
}

alg is the only field that matters day to day. HS256 means HMAC with SHA-256: one shared secret both signs and verifies. RS256 means RSA: a private key signs, and a public key verifies, which is what you want when the verifier should not be able to mint tokens.

There is a historically nasty value here too: none. A token with "alg": "none" and an empty signature is, by the letter of the spec, a valid unsigned JWT. Libraries used to accept those by default, which meant an attacker could strip the signature and be believed. Any current library rejects it, but if you are writing verification by hand, reject it explicitly rather than assuming.

The payload #

The payload holds the claims. Seven are registered by the spec, and you will meet these four constantly:

Claim Means Notes
sub Subject Who the token is about, usually a user id
iss Issuer Who minted it
exp Expiry Seconds since 1970, not milliseconds
iat Issued at Seconds since 1970

exp and iat are Unix timestamps in seconds. This is the second most common JWT bug after assuming privacy: JavaScript's Date.now() returns milliseconds, so signing a token with it produces an expiry roughly 50,000 years in the future.

expiry.js
// Wrong: Date.now() is milliseconds, exp is seconds.
const exp = Date.now() + 3600_000;

// Right.
const exp = Math.floor(Date.now() / 1000) + 3600;

If a token never seems to expire, check this before anything else.

The signature #

The third segment is the only part doing security work. It is a keyed hash over the first two segments, joined by the dot that is already in the token:

sign.js
import { createHmac } from 'node:crypto';

const signingInput = `${encodedHeader}.${encodedPayload}`;
const signature = createHmac('sha256', secret)
  .update(signingInput)
  .digest('base64url');

Because the signature covers the header and the payload, changing a single character of either invalidates it. That is the whole guarantee: the contents are authentic, not confidential.

Important

Compare signatures in constant time. A comparison that returns as soon as two bytes differ leaks information about the expected value through how long it took. In Node, use crypto.timingSafeEqual rather than ===.

What verification actually has to check #

A surprising number of hand-rolled verifiers check the signature and stop. That is not enough. Verification means all of:

  1. The signature is valid for the algorithm named, using a key you chose rather than one the token named.
  2. alg matches what you expect. Do not let the token pick.
  3. exp is in the future, allowing a little clock skew.
  4. nbf, if present, is in the past.
  5. iss and aud are values you accept.

Point 1 is the subtle one. If you read alg from the header and then select a verification strategy from it, a token claiming HS256 can be verified against your RSA public key as if that key were an HMAC secret. Your public key is public. That is the algorithm-confusion attack, and it comes from trusting the token to describe how to check itself.

When not to use a JWT #

JWTs are good at one thing: carrying a claim that a receiver can verify without calling anyone. That is genuinely useful across service boundaries.

They are a poor fit for ordinary session management, and people reach for them there constantly. A session cookie backed by server-side state can be revoked instantly. A JWT cannot: it is valid until it expires, because the whole point is that nobody has to look it up. Adding a revocation list to fix that gives you a database lookup on every request, which is exactly what the JWT was supposed to avoid. At that point a session is simpler and better.

Short answer: use JWTs between services, and sessions for logging users into your own app.

Inspecting a token safely #

You cannot debug what you cannot read, but pasting a live credential into a random site posts it to that site's server. Rotate anything you paste into a tool you do not control.

The JWT decoder here runs entirely in your browser tab. It makes no network request at all, so the token stays on your machine. It will decode the header and payload, explain the standard claims, and tell you whether the token has already expired.

It will not verify the signature, deliberately, because verifying needs your signing secret and a web page is the wrong place to type one. That part belongs in Toolbelt, the Mac app, where the secret never leaves your machine.

Guides land here first.

New guides and tools get posted as they go up. One email when the Mac app ships, nothing else.