How to decode a JWT (and what every claim actually means)

2026-08-29·Developer reference·4 min read·by Sourabh Singh

How to decode a JWT (and what every claim actually means)

Decode a JSON Web Token by hand or in the browser, understand every registered claim, and know exactly why decoding is not the same as verifying.

How to decode a JWT (and what every claim actually means)

A JWT is three base64url segments joined by dots. Decode the first two with atob after converting - to + and _ to /. The exp, iat and nbf claims are seconds since the Unix epoch, not milliseconds. Decoding is not verifying - only a signature check proves the claims are true.

A JSON Web Token looks like random noise, but it is three base64url-encoded strings joined by dots. Two of them are plain JSON you can read in a second, and the third is a signature you cannot read at all.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0In0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
    header                              payload                    signature

Decoding one by hand

Split on ., then base64url-decode each of the first two parts.

echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' | base64 -d
# {"alg":"HS256","typ":"JWT"}

Base64url is not the same as base64: + becomes -, / becomes _, and the trailing = padding is stripped. If base64 -d complains about invalid input, that is why - add padding back until the length is a multiple of four.

In JavaScript:

const decode = (segment) => {
  const padded = segment.replace(/-/g, "+").replace(/_/g, "/");
  const json = atob(padded + "=".repeat((4 - (padded.length % 4)) % 4));
  return JSON.parse(json);
};

Note that atob returns a binary string, so tokens containing non-ASCII characters need a TextDecoder pass to come out correctly.

Free toolJWT DecoderPaste a JSON Web Token to decode its header and payload and read every claim, including expiry. Runs entirely in your browser.

The header

Two fields matter.

alg - the signing algorithm. HS256 is symmetric (one shared secret), RS256 and ES256 are asymmetric (private key signs, public key verifies). If you see alg: "none", treat the token as hostile: it means the token claims to need no signature, and any library that honours it is exploitable.

kid - key ID. Present when the issuer rotates keys, telling the verifier which public key from the JWKS endpoint to use.

The registered claims

Seven claims are defined by RFC 7519. All are optional, all are conventional, and getting them wrong is the source of most JWT bugs.

ClaimNameMeaning
issIssuerWho minted the token
subSubjectWho the token is about - usually a user ID
audAudienceWho the token is for; verifiers must reject tokens not addressed to them
expExpirationSeconds since epoch after which the token is invalid
nbfNot beforeSeconds since epoch before which the token is invalid
iatIssued atWhen it was minted
jtiJWT IDUnique identifier, used to prevent replay

All three timestamps are NumericDate - seconds since the Unix epoch, not milliseconds. This is the single most common JWT bug in JavaScript codebases, because Date.now() returns milliseconds. A token with exp set from Date.now() is valid until the year 56,000.

const exp = Math.floor(Date.now() / 1000) + 3600; // correct: one hour

Decoding is not verifying

Reading the payload tells you what the token claims. It tells you nothing about whether those claims are true.

Anyone can take a valid token, change "role":"user" to "role":"admin", re-encode the payload, and hand it to you. The payload will decode perfectly. Only checking the signature against the key catches it.

So: never make an authorisation decision from a decoded payload. Verify first, then read. And never paste a token containing a real session into a website that offers to verify it, because verification requires the secret, and no legitimate tool asks for your signing key.

A JWT is signed, not encrypted. Anyone holding the token can read every claim in it. Do not put anything in a payload that you would not put in a URL.

The mistakes worth knowing

Trusting alg from the token. The classic attack: attacker changes alg from RS256 to HS256 and signs with the public key as the HMAC secret. Always pin the expected algorithm in your verifier rather than reading it from the header.

Skipping aud. If two of your services accept tokens from the same issuer and neither checks aud, a token minted for one is valid at the other.

Long expiry with no revocation. A JWT is valid until it expires, and there is no way to un-issue one. If you need instant revocation, you need a session store - a stateless token cannot give you that. Short exp plus a refresh token is the usual compromise.

Storing tokens in localStorage. Readable by any script on the page, which makes any XSS a total account compromise. An httpOnly cookie is the safer default.

Oversized tokens. Every claim you add ships on every request. Past about 4KB you start hitting header size limits in proxies and CDNs.

When not to use a JWT

If both sides are your own server and you already have a database, a random session ID in an httpOnly cookie is simpler, revocable instantly, and smaller. JWTs earn their complexity when the verifier cannot reach your database - third-party services, other teams' APIs, edge functions.

Launch it where the numbers are checked

RankCert ranks products on domain control we verify ourselves. Listing is free and the link stays dofollow whether or not you display the badge.

Submit a product - free

Tools from this guide