# 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.

Source: https://rankcert.com/blog/how-to-decode-a-jwt
Published: 2026-08-29 · Updated: 2026-08-29

---


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.

```bash
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:

```js
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 tool: [JWT Decoder](https://rankcert.com/tools/jwt-decoder) - Paste 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.

| Claim | Name | Meaning |
|---|---|---|
| `iss` | Issuer | Who minted the token |
| `sub` | Subject | Who the token is about - usually a user ID |
| `aud` | Audience | Who the token is *for*; verifiers must reject tokens not addressed to them |
| `exp` | Expiration | Seconds since epoch after which the token is invalid |
| `nbf` | Not before | Seconds since epoch before which the token is invalid |
| `iat` | Issued at | When it was minted |
| `jti` | JWT ID | Unique 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.

```js
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.

<Callout>
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.
</Callout>

## 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.

<Cta />
