← All guides

JWTs explained: what's inside a token and how to debug it

Open the developer tools on almost any modern web app and you'll find one: a long, opaque-looking string starting with eyJ, sitting in a cookie or riding along in an Authorization header. That's a JSON Web Token, and it's far less mysterious than it looks — three chunks of encoded text joined by dots, two of which anyone can read in seconds. This guide takes a token apart: what each part does, what the standard claims mean, what the signature does and doesn't prove, and how to work through the failures you'll actually meet — without handing a live credential to a stranger's server along the way.

One string, three parts

A JWT is a compact way for one system to hand another a set of signed statements — claims — such as “this is user 42, and this token is good until midnight.” Structurally, every JWT is the same three parts, separated by two dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBZGEiLCJleHAiOjE3NTIxOTIwMDB9.wDDioQfRZhPBcSQf3x_RBv_erAAK5mxoUUPb-kmeXhU

The reason nearly every JWT starts with eyJ is charmingly mundane: that's what {" — the opening of a JSON object — looks like after Base64 encoding.

Base64URL is encoding, not encryption

The header and payload are Base64URL-encoded: the standard Base64 alphabet with two characters swapped (- for +, _ for /) and the trailing = padding dropped, so a token survives being pasted into URLs and HTTP headers. Decoding it requires no key and no secret — it's a reversible transformation any browser can undo in a microsecond.

This is the single most important thing to understand about JWTs: anyone who obtains a token can read everything in it. The signature stops people from changing the payload; it does nothing to hide it. Two rules follow. First, never put secrets in a token — no passwords, no API keys, and no more personal data than the receiving service genuinely needs. Second, a token pasted into a bug report or a screenshot leaks both its contents and a working credential. The payload itself is ordinary JSON, the same format your APIs already speak.

See it yourself: paste a token into the JWT decoder — it splits and decodes the header and payload in your browser and flags whether the token has expired. Nothing you paste leaves the page.

The standard claims, translated

The payload can carry any JSON you like, but RFC 7519 registers a handful of claims with fixed, three-letter names. Six of them do most of the work:

ClaimStands forWhat it tells you
issIssuerWho created and signed the token — typically your auth server's URL.
subSubjectWho the token is about — the user or account identifier.
audAudienceWho the token is for — the API that should accept it (and others shouldn't).
expExpiration timeThe moment the token stops being valid.
iatIssued atWhen the token was created.
nbfNot beforeThe moment the token starts being valid.

The three time claims — exp, iat, nbf — are Unix timestamps: a count of seconds since January 1, 1970 UTC. That's why an expiry looks like 1752192000 rather than a date. Paste one into the timestamp converter to see it as a human date in UTC and your local time, or read our guide to Unix timestamps for how those numbers behave.

What the signature actually proves

The signature is computed over the exact bytes of header.payload. With HS256 — the usual choice when one app issues tokens to itself — it's an HMAC-SHA-256: the issuer feeds the first two parts plus a shared secret into a keyed hash function. Verifying means repeating that computation with the same secret and comparing results. A match proves exactly two things: the token was produced by something that knows the secret, and not one character of it has changed since.

Notice what “shared” implies: any service that can verify an HS256 token can also mint one, because verification and signing use the same key. That's fine inside a single application; it gets uncomfortable once several services need to check tokens. That's when asymmetric algorithms like RS256 or ES256 earn their keep — a private key signs, and a public key, which you can hand out freely, verifies.

Equally important is what a valid signature does not prove. It doesn't hide anything, and it doesn't make the token acceptable: an expired token verifies perfectly. Checking the signature is step one; checking exp, aud and iss against what your service expects is the rest of the job, and skipping it is how “valid” tokens end up authorizing the wrong things.

The classic failures, and how to read them

One failure class earned itself a history lesson. The header's alg field announces how the token is signed — and early JWT libraries trusted it. The spec even defines "alg":"none" for contexts where integrity is guaranteed some other way, so an attacker could strip a token's signature, set the algorithm to none, and some verifiers would wave the forgery through. A related trick told the verifier to treat an RSA public key as an HMAC secret by swapping RS256 for HS256. Modern libraries have closed both doors, and the official guidance in RFC 8725 distills the lesson: the verifier decides which algorithms it accepts — never the token.

Never debug with a stranger's server

A JWT is a bearer token: whoever bears it, is — as far as the API can tell — the subject. Until exp passes, a leaked token is a working login that bypasses the password and any two-factor prompt. So pasting a production token into some random “JWT debugger” website is a real risk: if the site decodes on its server, or merely logs what visitors type, you've just mailed someone a live credential.

What makes a client-side decoder different is the data path. Toolkit's JWT decoder does the Base64URL parsing in your own browser, and the optional HS256 signature check runs on the browser's built-in Web Crypto — the token is never transmitted anywhere. That's an inspectable property, not a promise: open your browser's Network tab while you use it, as our in-browser processing guide shows.

The same instinct should apply everywhere: treat tokens like passwords. Keep them out of commits, logs, URLs and chat history. If you genuinely must hand one to a teammate, use a one-time link — the secret is encrypted in your browser and destroyed on first read — rather than leaving it in a chat scrollback forever.

A five-minute debugging checklist

  1. Count the dots. A JWT has exactly two, making three parts. Anything else means the token was truncated or double-encoded somewhere along the way.
  2. Decode the header and payload. Note the alg, then read the claims like a human — is the sub the user you think it is?
  3. Check the clock claims. Convert exp, iat and nbf to real dates. Expired? Issued in the future? That's skew.
  4. Check aud and iss. These are exact string matches against your service's configuration — one character off is a rejection.
  5. Verify the signature last, against the key the issuer actually used, with the algorithm your service has pinned.

A closing caveat, because JWTs aren't always the answer: a signed token can't be recalled. It stays valid everywhere until exp, so instant logout or banning requires a server-side deny-list — which quietly reintroduces the shared state JWTs promised to remove. A single web app talking to its own backend is often better served by plain server-side sessions. JWTs earn their keep when several services need to verify identity without phoning a central session store on every request. Like most tools, they're excellent at the job they were designed for and awkward everywhere else.

Related: Unix timestamps and time zones, demystified · How to share a password or secret safely · Why Toolkit runs entirely in your browser