Skip to content
Utilerio

How JWT Authentication Actually Works (and How to Debug a Token)

A JSON Web Token is signed, not encrypted — the single fact that explains most of what confuses people about it, plus a practical workflow for debugging one.

· 3 min read

A JWT shows up in almost every modern API as an Authorization: Bearer <token> header, and it's usually treated as an opaque blob — copy it, paste it, hope it works. It's worth five minutes to actually understand the three parts, because the single most common misunderstanding about JWTs — that they're encrypted — is also the one most likely to cause a real security mistake.

Three parts, joined by dots

A JWT is header.payload.signature, where the header and payload are JSON objects, each Base64url-encoded:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dGhpc2lzbm90YXJlYWxzaWduYXR1cmU
└──────────── header ────────────┘ └────────── payload ──────────┘ └────── signature ──────┘

The header names the signing algorithm (alg) and token type (typ). The payload carries claims — arbitrary key-value data, plus some standard fields:

ClaimMeaning
subSubject — usually the user id
issIssuer — who created the token
audAudience — who the token is intended for
iatIssued at, Unix timestamp
expExpiry, Unix timestamp
nbfNot valid before, Unix timestamp

The part everyone gets wrong: it's signed, not encrypted

Base64url is an encoding, not encryption — reversible by anyone, with no key required. Anyone holding a JWT can read its entire payload without ever touching the signature. JWT Decoder demonstrates this directly: it decodes header and payload with no secret at all, because there's nothing to decrypt.

What the signature actually does is let the server detect tampering: it's an HMAC (or RSA/ECDSA signature) computed over the header and payload using a secret only the server knows. Change one character of the payload and the signature no longer matches — the server rejects it. But it never hides the payload from the person holding the token. This means:

  • Never put a password, an API key, or anything sensitive directly in a JWT payload — anyone with the token can read it.
  • A JWT stored in localStorage is readable by any JavaScript that runs on the page, which makes a successful XSS attack a full account takeover. An httpOnly cookie is the safer default for browser sessions specifically because client-side JavaScript can't read it.

HS256 vs RS256, briefly

HS256 (HMAC-SHA256) uses one shared secret to both sign and verify — simple, but that secret has to live on every service that verifies the token. RS256 uses an RSA key pair: the private key signs, the public key verifies, so a token issued by one service can be verified by many others without ever sharing a secret. Hash Generator is a useful companion here if you want to see what a raw SHA-256 hash actually looks like, since HS256's signature is exactly that hash function applied with a key.

A practical debugging workflow

  1. Decode it first. Paste the token into JWT Decoder and check exp against the current time — an expired token is the single most common "invalid token" cause, and it's instantly visible without touching any code.
  2. Check the claims match what your server expects — a mismatched aud or missing sub fails validation even with a perfectly valid signature.
  3. Reproduce it on your side. If your backend rejects a token you believe should be valid, use JWT Encoder to build one with the exact same header and payload, signed with the secret your server is configured with — HS256, HS384 or HS512 — and see whether the server accepts your version. If it does, the original token's secret or algorithm doesn't match what the server expects.

Both tools run entirely with the Web Crypto API in your browser — a secret typed into the encoder is used locally to compute the signature and is never sent anywhere, which matters for a value you'd rather not paste into a random website.

One more base — Base64 itself

Since the header and payload segments are Base64url under the hood, Base64 Encoder / Decoder is a quick way to manually decode a single segment if you ever need to check exactly what's being encoded without a JWT-aware tool at hand.