LocalFirstTools.com — data tools that never leave your device
No upload — everything runs on your device

How to decode a JWT (and why that isn't verifying it)

Last updated

To decode a JWT, split it on its two dots and Base64URL-decode the first two parts — or skip the manual work and paste it into the JWT Decoder, which shows the header and payload as formatted JSON without the token leaving your browser. Decoding takes seconds and requires no key, and that fact is the most important thing to understand about JWTs: anyone can read one. Decoding is not verification.

The three segments

A JWT (RFC 7519) is three Base64URL blobs joined by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiI0MiIsImV4cCI6MTc2NzIyNTYwMH0 . SflKxwRJ...
└── header ─────────────────────────┘  └── payload ──────────────────────────┘  └─ signature ─┘
  1. Header — metadata, typically {"alg": "HS256", "typ": "JWT"}: which algorithm signed the token.
  2. Payload — the claims: registered ones like sub (subject), exp (expiry), iat (issued at), iss (issuer), plus whatever custom claims the issuer added.
  3. Signature — cryptographic proof over the first two segments. It is not decodable into anything readable; it can only be verified with the right key.

Decoding it yourself

The segments are Base64URL, not standard Base64 — - and _ replace + and /, and padding is dropped (see Base64 vs Base64URL). That is why atob() on a raw segment sometimes throws. In JavaScript:

const payload = JSON.parse(
  atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))
);

The decoder tool does exactly this — plus JSON pretty-printing — client-side.

A worked example

Here is a complete (expired, HS256-signed) token and what falls out of it:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

segment 1 decodes to:  {"alg":"HS256","typ":"JWT"}
segment 2 decodes to:  {"sub":"1234567890","name":"John Doe","iat":1516239022}
segment 3:             the HMAC-SHA256 signature (not decodable)

Reading the claims: sub identifies the subject, name is a custom claim the issuer chose to add, and iat: 1516239022 is Unix seconds — converted, 2018-01-18T01:30:22Z, so this token is years old. There is no exp claim at all, which itself is a finding: a token without exp never expires unless the server enforces its own lifetime. The registered claims worth knowing on sight: iss (who minted it), aud (who it is for), sub, exp/nbf/iat (the three timestamps), and jti (unique token ID, used for revocation lists).

What decoding tells you (a lot)

Debugging auth usually only needs the payload: Is the token expired? (exp is Unix seconds — convert it and compare with now.) Was it issued for the right user (sub), audience (aud) and scopes? Did the identity provider include the custom claim your code reads? Nine out of ten “my API returns 401” investigations end at an expired exp or a missing claim.

What decoding cannot tell you

Whether the token is genuine. Anyone can mint a JWT with any claims — alg set to HS256, sub set to admin — and it will decode beautifully. Only signature verification against the issuer's key separates a real token from a forged one, which is why servers must verify (never just decode) and why a browser decoder honestly labels the signature “not verified”. Treat decoded claims as a debugging view, never as an authorization decision.

The alg field is attacker-controlled

One header claim deserves special paranoia: alg. RFC 7519 permits "alg": "none" — an unsigned JWT whose third segment is empty (the token ends with a dot and nothing after it). Early JWT libraries famously honored whatever alg the token declared, so an attacker could strip the signature, set none, and sail through verification; a related classic swapped RS256 for HS256 to trick servers into using a public key as an HMAC secret. Modern libraries force you to allowlist algorithms — do that, server-side, always. From the decoding side, the lesson is simpler: everything you can read in the header and payload, an attacker can write.

Do it now — locally

Inspect header and payload of a JSON Web Token locally. Nothing is uploaded.

Open the JWT Decoder

Frequently asked questions

Is it safe to paste a JWT into an online decoder?

Only if the decoding happens client-side, because a live token is a bearer credential — whoever holds it can use it. The LocalFirstTools decoder runs entirely in your tab; when using any other tool, verify in the network tab that the token is not transmitted, and prefer expired or test tokens.

Why can I read a JWT without any key?

A signed JWT (JWS) is encoded, not encrypted. Base64URL is a reversible transport encoding; the cryptography in the third segment proves integrity and origin, not confidentiality. Secrets must not be put in JWT claims unless the token is an encrypted JWE.

How do I check if a JWT is expired?

Decode it and read the exp claim — a Unix timestamp in seconds. Convert it to a date and compare with now; if exp is in the past the token is expired. The iat (issued-at) and nbf (not-before) claims use the same unit.

How do I actually verify a JWT then?

Server-side, with a JWT library: verify the signature against the issuer's public key or shared secret, then check exp, iss and aud. Libraries exist for every language (jose for JavaScript, PyJWT for Python). Verification requires key material a browser tool should not have.

Why does my token end with a dot and nothing after it?

That is an unsigned JWT — "alg": "none" — with an empty signature segment. Legal per RFC 7519, and exactly why verifiers must allowlist algorithms: a server that accepts alg none accepts any forged claims.

Why doesn't my token decode at all?

Check the dots first: two dots means JWS (decodable header and payload), four dots means an encrypted JWE with five segments — its payload is genuinely encrypted and no decoder can show it without the key. Also make sure you copied the whole token; truncation breaks the Base64URL groups.