How to read a JWT without leaking it
· 4 min read
If you work with APIs, sooner or later a bug report lands with a token attached: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…. That long string is a JSON Web Token (JWT), and reading it is the fastest way to answer questions like "which user is this?", "has it expired?" and "which key signed it?".
The good news is that a JWT isn't encrypted. The bad news is the same: anyone holding it can read it, and anyone holding it can usually use it. So it's worth knowing how to decode one safely.
What a JWT is made of
A JWT has three parts separated by dots:
header.payload.signature
- Header: a small JSON object saying how the token was signed, for example
{"alg":"HS256","typ":"JWT"}. - Payload: the claims, such as who the token is for and when it expires.
- Signature: proof that the header and payload haven't been changed since the issuer signed them.
The header and payload are JSON encoded with Base64url, a URL-safe variant of Base64 that uses - and _ instead of + and / and drops the = padding. That's why decoding them takes no key at all. You can even do it by hand with the Base64 encoder and decoder, though you'll need to swap the URL-safe characters back first.
The claims worth checking
Most debugging comes down to a handful of registered claims from RFC 7519:
| Claim | Meaning | What to look for |
|---|---|---|
sub |
Subject: who the token is about | The right user or service ID |
iss |
Issuer | Your auth server, not a test one |
aud |
Audience | The API that should accept it |
exp |
Expiry, in Unix seconds | Is it in the past? |
nbf |
Not before | Is it in the future? |
iat |
Issued at | Roughly when the user signed in |
Times are Unix timestamps in seconds, not milliseconds. A token with "exp": 1798761600 expires at 00:00 UTC on 1 January 2027. If the expiry looks like it's 50,000 years away, someone wrote milliseconds where seconds were expected.
Decode it without sending it anywhere
Paste a token into the JWT decoder and it splits the three parts, pretty-prints the header and payload, and turns exp, nbf and iat into readable dates with a note if the token has expired. It runs in your browser: the token never leaves your device, and it's never put into a share link.
That matters more than it sounds. A valid access token is a password with an expiry date. Pasting one into a site that logs requests, or into a chat, is the same as handing over the session it belongs to. Treat tokens from production like secrets:
- Decode them locally, never on a site that sends them to a server.
- Redact the signature (the third part) before sharing a token in a ticket. The header and payload are usually enough to debug.
- If a live token has leaked, revoke the session rather than waiting for it to expire.
Decoding is not verifying
Reading the payload tells you what the token claims. It doesn't tell you whether to believe it. Verification means recomputing the signature with the right key and algorithm, and only then trusting the claims.
Two classic mistakes:
- Accepting
alg: none. An unsigned token should never be accepted. Libraries reject it by default; make sure yours does. - Algorithm confusion. If your server expects RS256 (signed with a private key, checked with a public key) but accepts whatever
algthe token says, an attacker can sign an HS256 token using your public key as the HMAC secret. Always pin the algorithms you accept.
To see verification working, the JWT encoder can sign a test token with HS256, RS256, PS256 or ES256 and verify one against a secret or public key. It refuses alg: none and flags tokens whose algorithm doesn't match the key you gave it.
A quick debugging checklist
When an API says "401 Unauthorized" and you have the token:
- Decode it. Is it actually a JWT, or an opaque token you can't read?
- Check
expagainst the current time, allowing a minute or two of clock skew. - Check
audandissmatch what the API expects. Tokens from a staging issuer won't work in production. - Check the header's
algandkid(key ID). Akidthe API doesn't know usually means keys were rotated. - Only then dig into scopes or roles in the payload.
Nine times out of ten the answer is in the first three steps, and none of them need the token to leave your machine.
Tools in this post
Keep reading
- JSON vs YAML: when to use which
JSON and YAML describe the same data, but they fail in different ways. A practical guide to choosing between them, with the YAML gotchas that bite in production.
- Unix timestamps and time zones, explained
Why 1700000000 is a date, why your timestamp is 1,000 times too big, and how to store and show times without time zone bugs. A practical guide with examples.
- Cron expressions explained with 12 examples
Five fields, a few symbols and a lot of confusion. Learn to read and write cron schedules with 12 real examples, plus the time zone and day-of-week traps to avoid.