Skip to main content
ToolstikToolstik
BlogDeveloper

How to Decode a JWT Safely

ToolstikUpdated 5 min read
A JWT split into header, payload, and signature segments being decoded locally in a browser

To decode a JWT safely, paste it into a decoder that runs entirely in your browser, such as the Toolstik JWT Decoder, and click Decode JWT to see the header and payload as JSON. Treat what you see as unverified: decoding shows what a token claims, not whether those claims are true. Prefer test or expired tokens over live production ones, and verify signatures only on your server.

Try it now

JWT Decoder

Open full tool

Loading tool…

Before you paste: a 20-second safety check

A JWT is usually a bearer credential. Anyone who has a valid, unexpired token can call your API as that user. So before pasting one anywhere:

  • Prefer a test token from a dev or staging environment, or one that has already expired.
  • Check where decoding happens. The Toolstik decoder is a client-side function: it splits the token, Base64URL-decodes the first two parts, and parses them as JSON in your browser tab. The token isn't sent to a server, and the analytics events it records don't include the token itself.
  • Never paste signing secrets or private keys into any website. You don't need them to decode, and Toolstik never asks for them.
  • Redact before sharing. In bug reports, include the decoded claims you need and leave out the signature. With the signature removed, a token can't be replayed.

Step by step: decode a JWT in Toolstik

  1. Open the JWT Decoder. The JWT token box has a sample token preloaded so you can see the output format.
  2. Replace it with your token. Paste only the token: strip any Bearer prefix from an Authorization header.
  3. Click Decode JWT.
  4. Read the output. A note at the top reminds you that the tool "decodes JWTs locally" and "does not verify signatures or trust claims". Below it is one JSON block with a header object and a payload object, already indented.
  5. Click Copy to copy that JSON, or Reset to restore the sample.

The signature segment isn't displayed, because it's binary data that can't be read without a key.

For a token like this (signed with a throwaway demo secret):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ.tulDXbfVqDWHrXTAOEEdlUfcIeFRjIRiBiMlcRqwA6Y

you get:

{
  "header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "payload": {
    "sub": "user_123",
    "iat": 1700000000,
    "exp": 1700003600
  }
}

Reading the claims that matter for debugging

  • exp (expiry) and iat (issued at) are Unix timestamps in seconds. 1700003600 is 2023-11-14T23:13:20Z, one hour after iat. Paste either value into the Unix Timestamp Converter to see it in UTC and your local time. Our guide on how to convert a Unix timestamp explains seconds versus milliseconds.
  • nbf (not before) is also seconds. A token used before nbf should be rejected.
  • aud and iss tell you which API the token is for and who issued it. A mismatch is a common cause of "valid token, still 401" errors.
  • alg in the header tells your server which algorithm to use. Your server should enforce an allow-list rather than trust this value.

A quick expiry check in JavaScript:

const payload = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
const expired = payload.exp < Math.floor(Date.now() / 1000);

Note the / 1000. Date.now() returns milliseconds, while exp is in seconds. Mixing them up is one of the most common JWT bugs. This snippet is for debugging only, because it doesn't check the signature. (Plain atob also doesn't decode UTF-8, which is fine for numeric claims like exp.)

Troubleshooting decode errors

MessageLikely causeFix
A JWT must have three parts separated by dots.Truncated copy, extra text, or a five-part encrypted JWERe-copy the full token. JWEs can't be decoded without the key.
A JWT must have header, payload, and signature.An empty segment, for example an unsigned token ending in a dotToolstik expects all three parts to be present.
Invalid JWT header: not valid JSON.Often a leftover Bearer prefixRemove everything before eyJ.
Invalid JWT payload: not valid Base64URL.A character was lost or changed while copyingCopy the token again from the source.

Accented or non-Latin characters look garbled (for example José instead of José)? The decoder reads the bytes without UTF-8 decoding. Copy just the payload segment, the text between the two dots, into the Base64 Encoder / Decoder, switch on URL-safe Base64, and click Decode. That tool decodes UTF-8 correctly. Then tidy the result with the JSON Formatter.

Decoding is not verifying

This is the rule that matters most. The header and payload are only encoded, not encrypted, and not protected by decoding. Anyone can change "role": "user" to "role": "admin", re-encode it, and paste it into a decoder, which will happily display it.

What stops that attack is signature verification on your server, using a maintained library (for example jose in Node.js or PyJWT in Python) with:

  • the correct secret (HS256) or public key (RS256, ES256),
  • an explicit list of allowed algorithms,
  • checks on exp, nbf, iss, and aud.

Never make an authorization decision, client-side or server-side, from a decoded payload you haven't verified. Decoded claims in the browser are fine for display, such as showing a user's name, but not for access control.

To learn what each segment contains and how the signature is computed, read JWT header vs payload vs signature explained. For the rest of the toolkit, see free developer tools online.

Frequently asked questions