DEV Community

Ryan Caldwell
Ryan Caldwell

Posted on

How to Read a JWT Without Trusting a Third-Party Server

Last month I watched a colleague paste a production JWT into the first Google result for "jwt decode online." I opened DevTools on that site. The token — with real user IDs, roles, and an expiry 24 hours out — was sent to their backend in a POST request.

He had no idea.

This happens constantly. Developers treat online decoders like calculators — paste in, get result, move on. But a JWT often contains session data, user identifiers, permissions, and sometimes PII. Sending it to a third-party server is a security incident waiting to happen.

Here's how to decode a JWT safely, what to actually look for when you do, and how to verify the claims that matter.


What's Actually Inside a JWT

A JWT has three parts separated by dots: header.payload.signature

Each of the first two parts is just Base64URL-encoded JSON. There is no encryption involved in a standard JWT — anyone with the token can read the header and payload. The signature verifies integrity, not confidentiality.

This is the part that surprises people: JWTs are not encrypted by default. If your JWT contains an email address, anyone who intercepts it can read that email.

Step 1: Decode It Locally

You don't need a website. You can decode a JWT in your browser's console right now:

const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";

const [header, payload] = token.split('.').slice(0, 2).map(
  part => JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/')))
);

console.log('Header:', header);
console.log('Payload:', payload);
Enter fullscreen mode Exit fullscreen mode

That's it. No npm package. No website. Pure browser JavaScript.

If you want something more visual with syntax highlighting and expiration checks, Anytools JWT Decoder does exactly this — entirely client-side. Open your Network tab while using it and you'll see zero outbound requests after page load.

Step 2: Check the Header

The header tells you the signing algorithm:

{
  "alg": "HS256",
  "typ": "JWT"
}
Enter fullscreen mode Exit fullscreen mode

Things to watch for:

  • alg: "none" — This is the classic JWT vulnerability. If your backend accepts alg: none, anyone can forge tokens. This should never appear in production.
  • alg: "HS256" with a public key — If your system uses RS256 (asymmetric) but the token says HS256 (symmetric), someone might be trying an algorithm confusion attack.
  • Missing typ — Not a vulnerability, but sloppy. Indicates the issuing system might have other configuration issues.

Step 3: Read the Payload Claims

Standard claims to check:

Claim What it is What to look for
exp Expiration time Is it unreasonably far in the future? Tokens valid for 30 days are a red flag.
iat Issued at Does this match when the token was actually created?
nbf Not before Is the token being used before it's valid?
iss Issuer Does it match your expected auth server?
aud Audience Is this token intended for your service?
sub Subject The user identifier. Is PII leaking here?

Step 4: Check for PII in the Payload

This is the one most teams skip. Search the payload for:

  • Email addresses
  • Full names
  • Phone numbers
  • Internal user IDs that map to real identities
  • IP addresses
  • Role names that reveal your internal permission model

If any of these are present, ask whether they need to be. JWTs are often stored in local storage, sent in URLs, and logged by proxies. Every claim in the payload is a potential data leak.

Step 5: Verify the Expiration Makes Sense

Convert the exp claim from a Unix timestamp to a human-readable date. A quick way:

new Date(1516239022 * 1000).toISOString()
// "2018-01-18T01:30:22.000Z"
Enter fullscreen mode Exit fullscreen mode

Or use a timestamp converter if you want instant conversion without doing mental math.

Common issues:

  • Tokens that never expire — no exp claim at all
  • Tokens valid for days or weeks — access tokens should typically expire in minutes to hours
  • Refresh tokens in the JWT format — these should be opaque, not JWTs

The Takeaway

Decoding a JWT doesn't require trust. The data is just Base64-encoded JSON. You can do it in a browser console, in a local script, or in a client-side tool that doesn't make network requests.

What you should never do is paste production tokens into a website that processes them server-side. If you can't verify that a decoder is client-side only (by checking the Network tab), don't use it.

The habit takes 30 seconds to build. Open DevTools. Watch the Network tab. If you see a POST request with your token in the body, close that tab and don't go back.

Top comments (0)