If you build anything with authentication, you decode JWTs constantly: to see why a login failed, what a token claims, when it expires. And almost everyone does it the same way: paste the token into the first “JWT decoder” that comes up in search. That habit is faster than it is safe, and it’s worth knowing why before your next copy-paste.

A JWT (JSON Web Token) is the little string that proves who you are on most modern apps. Three chunks separated by dots: header.payload.signature. Here is the part that surprises people: the first two chunks aren’t encrypted. They are just Base64url-encoded JSON. Anyone holding your token can read everything in it in about two seconds.

What’s actually inside

Decode the payload and you get a plain JSON object of “claims”, the token’s contents:

  • sub: the subject, usually the user ID.
  • exp / iat / nbf: expiry, issued-at, and not-before, as Unix timestamps. exp is the one you check when a token “randomly” stops working.
  • iss / aud: who issued the token and who it’s meant for.
  • Whatever the app bolted on: roles, email, plan tier, and sometimes a lot more than the developer realized they were exposing.

How to decode one by hand

You don’t strictly need a tool. The payload is just Base64url, so you can read it in a browser console in one line:

JSON.parse(atob(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')))

That splits the token on dots, takes the middle segment, converts Base64url to standard Base64 (JWT uses the URL-safe - and _ characters), and parses the JSON. You’ll see the claims printed straight out.

Decoding is not verifying

The single most important thing to understand: reading a JWT tells you what it says, not whether it’s true. The signature (that third chunk) is what proves the token wasn’t forged, and checking it requires the issuer’s key. A decoder shows you the claims; it does not tell you the token is valid. Never treat a decoded payload as proof of anything on the server side.

The part the listicles skip: don’t paste live tokens into random sites

Here is the security angle the “top 10 JWT decoders” posts never mention. A production JWT is often a live session. Paste it into an unknown web decoder and you have just handed a stranger’s server a working credential, with no idea whether it’s logged, stored, or quietly replayed against your account. Most decoders are probably fine. “Probably” is a strange bet to make with the token that is your login.

That’s the unglamorous reason we built our own $1 JWT decoder: it splits out the header and payload, turns the exp/iat timestamps into real dates, flags an expired token, and doesn’t keep your token afterward. No account, no signup, nothing stored on someone’s server. A dollar for not gambling with a credential is a fair trade. The browser-console method above is free if you’d rather.

Decoding is not verifying

This is the distinction that matters most, and the one that decoder websites quietly blur. Decoding a JWT is just base64url: it is reversible by anyone, requires no key, and proves nothing. Verifying it means recomputing the signature over the header and payload with a secret or public key and confirming it matches.

A decoded token tells you what it claims. Only a verified token tells you those claims are true. If you are debugging and the payload looks right, you have learned that someone wrote those values, not that your issuer did.

Two classic attacks live in exactly this gap, and both are worth recognising:

  • The none algorithm. The spec defines an unsecured JWT with alg set to none and an empty signature. A library that honours the token's own header instead of the algorithm you configured will happily accept a forged payload with no signature at all.
  • Algorithm confusion. If a service verifies with RS256 (public/private key) but can be talked into treating the token as HS256 (shared secret), an attacker can sign a token using the public key as the HMAC secret, and the public key is, by definition, public.

The defence in both cases is the same: your verifier decides the algorithm, never the token. Pin the expected algorithm and key explicitly rather than letting the header choose.

What to actually look at in a token you are debugging

When a token is being rejected and you want to know why, these are the claims that explain most failures:

  • exp: expiry. Overwhelmingly the most common cause of a token that “worked a minute ago.” It is Unix time in seconds; a value that looks a thousand times too large is milliseconds, and something in your stack is producing the wrong unit.
  • nbf and iat: not-before and issued-at. These produce the confusing failure where a freshly minted token is refused: if the issuing and verifying machines disagree on the time by more than the allowed skew, a token can be legitimately “not yet valid.”
  • aud: audience. A token meant for one service being presented to another. Common in setups where several APIs share an identity provider.
  • iss: issuer. Should match your provider exactly, tenant and environment included. Staging tokens reaching production fail here.

If all of those look correct and verification still fails, suspect key rotation before you suspect your code, particularly with providers that publish a key set and rotate on their own schedule.

The quick checklist

  • Split on dots; the payload is the middle segment.
  • Base64url-decode it to read the claims.
  • Check exp first when a token suddenly stops working.
  • Remember that decoding is not verifying: the signature is where the trust lives.
  • Never paste a live token into a site you don’t trust.

A JWT is a postcard, not a sealed envelope: readable by anyone who holds it, trustworthy only if you check the signature. Decode them freely to debug; just remember what you’re actually holding when you copy one out of a request.

We build software that treats your credentials like credentials. That’s what we do at Rebel Studios.