Skip to content
PithLab

↑↓ to move · Enter to open · Esc to close

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:

  1. Accepting alg: none. An unsigned token should never be accepted. Libraries reject it by default; make sure yours does.
  2. Algorithm confusion. If your server expects RS256 (signed with a private key, checked with a public key) but accepts whatever alg the 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:

  1. Decode it. Is it actually a JWT, or an opaque token you can't read?
  2. Check exp against the current time, allowing a minute or two of clock skew.
  3. Check aud and iss match what the API expects. Tokens from a staging issuer won't work in production.
  4. Check the header's alg and kid (key ID). A kid the API doesn't know usually means keys were rotated.
  5. 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.