DEV Community

Cover image for Stop pasting JWTs into random websites
Yassine Sellami
Yassine Sellami

Posted on

Stop pasting JWTs into random websites

A JWT isn't just JSON you can inspect. It's a live bearer token. Here's a safer way to decode one.

A few days ago I was reviewing a bug with a teammate. They wanted to see what was inside an access token, so they copied it into the first JWT decoder Google returned.

It wasn't a dummy token.

It was a production access token with almost an hour left before it expired.

Nobody was trying to do anything risky—it was just the quickest way to inspect a JWT. That's exactly why this keeps happening.

The thing people forget

A JWT looks like this:

header.payload.signature
Enter fullscreen mode Exit fullscreen mode

The payload isn't encrypted. It's just Base64URL-encoded JSON.

Because of that, people often think:

"The payload isn't secret, so the token is probably safe to paste."

Those aren't the same thing.

The payload may be readable, but the token itself is still your credential. Anyone holding it can usually authenticate as you until it expires.

Why online decoders make me nervous

Some JWT tools only decode locally in your browser.

Others offer things like signature verification, claim validation, or key management. Features like those often require talking to a backend, which means the token gets sent somewhere else.

Maybe the site is trustworthy.

Maybe it isn't.

From the UI alone, you usually can't tell.

Even if a decoder claims everything runs client-side, I don't like assuming that's true when I'm holding a production credential.

You don't need a website to inspect a JWT

Most of the time I'm only interested in the payload anyway.

echo "$TOKEN" \
  | cut -d '.' -f2 \
  | base64 --decode \
  | jq
Enter fullscreen mode Exit fullscreen mode

Because JWTs use Base64URL encoding, you may need to translate the alphabet and add padding first:

decode_jwt() {
  local payload=$(echo -n "$1" | cut -d. -f2 | tr '_-' '/+')

  while [ $(( ${#payload} % 4 )) -ne 0 ]; do
    payload="${payload}="
  done

  echo "$payload" | base64 --decode | jq
}

decode_jwt "$TOKEN"
Enter fullscreen mode Exit fullscreen mode

That gives you the claims, expiration time, issuer, audience—everything most people open a decoder for.

Checking whether it's expired

Once you've decoded it:

exp=$(decode_jwt "$TOKEN" | jq .exp)
now=$(date +%s)

if [ "$exp" -lt "$now" ]; then
  echo "expired"
else
  echo "valid for $((exp-now)) seconds"
fi
Enter fullscreen mode Exit fullscreen mode

If you really want a web UI

Sometimes a terminal isn't the most convenient way to inspect claims.

I ran into that enough times that I ended up building my own decoder:
https://astound.tools/dev/jwt/

It does everything in the browser—decode the header and payload, inspect claims, show expiration information, and verify signatures locally. No backend required.

Don't take my word for it, though.

Open DevTools, switch to the Network tab, paste in a token, and make sure nothing gets sent anywhere. That's the easiest way to verify any JWT tool, including mine.

Already pasted a real token?

I'd assume it's compromised and rotate it.

Maybe nothing happened.

Maybe the site never stored it.

But access tokens are cheap to replace, and it's not worth gambling over a debugging shortcut.

My rule

If a token came from production, it stays on my machine.

If I need to inspect it, I use the terminal or a tool whose behavior I've verified myself.

It's one of those habits that takes almost no effort once you start doing it.

Top comments (0)