⚡FTJ
← Blog
DeveloperBy The FreeToolJet Team

How to Read a JWT by Hand (and the Three 401s It Would Have Saved Me)

A JSON Web Token is three base64url chunks and a signature. Decoding one by eye explains most auth failures in under a minute: expiry, clock skew, wrong key, wrong audience.

It's late, the API returns 401 Unauthorized, and the only thing you have is a bearer token in a header. Nine times out of ten you don't need a debugger — you need to read the token. A JWT is not encrypted and not mysterious: it's three base64url chunks with a dot between each. Here's how to read one by hand, and then the three failures behind most of those 401s.

The three parts

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsImV4cCI6MTcwMDAwMDAwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Split on the two dots:

  • Header — the algorithm and token type.
  • Payload — your claims (who, when, what).
  • Signature — proof the server issued it, given the key.

It's base64url, not base64: the alphabet swaps + for - and / for _, and drops the = padding. Decode the header and payload — they're just JSON:

// header

// payload { "sub": "42", "exp": 1700000000 } `

That's the whole trick. The signature you don't decode — you can't, without the key. And that distinction is where people go wrong.

The three 401s this would have caught

1. The token expired (or you compared the wrong units)

exp is a Unix timestamp in seconds. If you (or a library) compared it against Date.now() — which is milliseconds — the token looks like it expired in 1970. Compare exp against Math.floor(Date.now() / 1000). While you're there, check nbf ("not before"): if the server clock runs a few seconds ahead, a freshly minted token is technically not valid yet. A 30–60 second leeway on both ends kills that flakiness.

2. The signature doesn't match the key (or the algorithm)

If the header says "alg": "RS256" (asymmetric) but your code verifies with an HS256 shared secret, every check fails even though the token is perfectly valid. The reverse happens too. And the classic hole: some libraries historically trusted the token's own alg field, so an attacker flips it to "none" and strips the signature. Never let the token choose its algorithm; pin it on the server. If the signature is wrong, decoding won't tell you why — but an alg mismatch usually will.

3. The token was minted for someone else

aud (audience) and iss (issuer) exist so you can't accidentally accept a token that's valid for a *different* service. You grabbed a token from staging, pointed your app at production, and the signature or audience check rejects it. Reading iss and aud tells you instantly which environment and service the token belongs to.

What decoding does *not* tell you

Anyone can base64url-decode a JWT — including attackers. So:

  • Never put secrets in the payload. It's readable by design.
  • Decoding is not verifying. Only the server, holding the key, can confirm the signature is real. If you decode and trust the claims, you've verified nothing.
  • Don't paste production tokens into random websites. They're live credentials until they expire.

For quick local checks I drop the token into the JWT Decoder, which splits header and payload and shows exp/iat as readable dates. If a timestamp looks wrong, the Timestamp Converter turns raw exp into a date so you can tell whether it's genuinely expired or you're just comparing seconds to milliseconds. And when the payload holds something base64url that won't decode cleanly, the Base64 Encoder tells you whether the encoding is the problem or the token is.

FT
The FreeToolJet Team
Maintainer of FreeToolJet

FreeToolJet is a small, independent project. The tools and guides here are written and maintained by the site's creator, who got tired of ad-heavy tool sites that upload your files to a server and built a private, browser-only alternative instead. If a guide is wrong or a tool breaks, tell us through the contact page — we read every message.

More about us →

Try These Tools

More Articles