How to Decode a JWT (and What Each Part Means)

Learn how to decode a JSON Web Token by hand or in your browser, read its header, payload and claims like exp and iat, and why decoding is not verifying.

· 4 min read

A JSON Web Token looks like random noise, but it's just three pieces of Base64Url-encoded text joined by dots. You can read the first two without any key at all. This guide shows what's inside a JWT, how to decode one in a few seconds, and the one mistake people make most often: treating a decoded token as a trusted one.

What a JWT looks like

Here's a short example token, split onto three lines so you can see the parts:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSIsImlhdCI6MTc2MDAwMDAwMCwiZXhwIjoxNzYwMDAzNjAwfQ
.lk8sRfkhPK6DSJBJ9VIZ_wILrtBHF885jTyVYUxyyfM

Every JWT has the same shape, header.payload.signature:

PartContainsReadable without a key?
HeaderThe signing algorithm (alg) and token type (typ)Yes
PayloadThe claims: who the token is about, when it expires, roles and so onYes
SignatureA hash of the header and payload, made with a secret or private keyNo, and you can't reverse it

The first two parts are encoded, not encrypted. Anyone holding the token can read them.

Decode a JWT in your browser

The fastest way is to paste the token into the JWT decoder. It splits the token, decodes the header and payload into formatted JSON, and turns timestamp claims like exp and iat into readable dates. Decoding happens in your browser, so the token isn't uploaded anywhere.

  1. Copy the full token, usually from an Authorization: Bearer … header, a cookie, or your browser's dev tools.
  2. Paste it into the decoder's input.
  3. Read the header and payload on the right. Click any section to copy it.

Decode a JWT by hand

Since each part is Base64Url, you can decode it with standard tools. Base64Url is ordinary Base64 with - and _ in place of + and /, and with the trailing = padding removed.

In the browser console or Node.js:

function decodeJwtPart(part) {
  const base64 = part.replace(/-/g, '+').replace(/_/g, '/');
  const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
  return JSON.parse(atob(padded));
}

const [header, payload] = token.split('.');
console.log(decodeJwtPart(header));  // { alg: "HS256", typ: "JWT" }
console.log(decodeJwtPart(payload)); // { sub: "1234567890", name: "Ada", iat: ..., exp: ... }

In a terminal:

echo 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSJ9' | tr '_-' '/+' | base64 -d

If base64 -d complains about invalid input, add one or two = characters to the end until the length is a multiple of four. You can also paste a single part into the Base64 decoder.

The claims you'll see most often

The payload is a JSON object of claims. A few are standardized in RFC 7519:

ClaimNameMeaning
issIssuerWho created the token, for example your auth server's URL
subSubjectWho the token is about, usually a user ID
audAudienceWhich service the token is meant for
expExpiration timeAfter this moment the token must be rejected
nbfNot beforeBefore this moment the token must be rejected
iatIssued atWhen the token was created
jtiJWT IDA unique ID, useful for revoking single tokens

Anything else, like role, email or scope, is a custom claim your app defines.

exp, nbf and iat are Unix timestamps in seconds, not milliseconds. If you're debugging an "expired token" error, convert exp with the timestamp converter and compare it with the current time, keeping time zones and clock skew between servers in mind.

Decoding is not verifying

This is the important part. Decoding only tells you what a token claims. It doesn't tell you whether those claims are true, because anyone can write a payload that says "role": "admin" and encode it.

Trust comes from the signature. Your server recomputes it from the header, the payload and its key:

  • HS256 / HS384 / HS512 use one shared secret (HMAC). The same key signs and verifies.
  • RS256 / ES256 and friends use a key pair. The auth server signs with its private key, and anyone can verify with the public key, often published at a JWKS URL.

When you verify on a server, use a maintained library and pin the algorithms you expect. Never accept "alg": "none", and check exp, aud and iss along with the signature. A decoder like ToolDock's is for reading and debugging tokens. It won't tell you a token is valid, and it shouldn't.

Is it safe to paste a token into a decoder?

A JWT is a credential. Whoever holds a live access token can usually act as that user until it expires. So:

  • Prefer decoders that run entirely in the browser (ToolDock's decoder does), or decode locally with the snippet above.
  • Use expired tokens or tokens from a test environment when you can.
  • Don't put secrets in the payload. Since anyone can decode it, a JWT is the wrong place for passwords, API keys or personal data you wouldn't show the user.

Quick reference

  • A JWT is header.payload.signature, each part Base64Url-encoded.
  • The header and payload are readable by anyone. Only the signature needs a key.
  • Time claims are Unix seconds.
  • Decoding is for debugging. Your server must verify the signature before trusting anything.

Got a token to inspect? Open the JWT decoder and paste it in.