JWT Decoder Checklist: Three Parts, One Padding Fix, and an Expiry Trap

You paste a token into a decoder, you get JSON back, and you move on. The

problem is what the page did not check before showing that JSON. We read our

own decoder line by line and ran the arithmetic to pin down exactly which

checks run, which values are trustworthy, and where the badge can lie to you.

The three-part rule is the first gate

A JSON Web Token is three base64url strings separated by dots:

header.payload.signature

Our decoder splits the pasted text on dots and rejects anything that does not

produce exactly three parts. Two parts, four parts, or text with no dots at

all triggers the invalid token card, which tells you the token must have

3 parts separated by dots. A second gate follows: the first two parts must

decode to text that JSON.parse accepts. Either failure shows the same error

card, so a token that splits correctly but contains a typo in the payload

still gets rejected.

This gate catches the two most common paste mistakes: copying only half of a

wrapped token from a log line, and pasting a session string that is not a JWT

at all.

Base64url is not base64, and padding decides the parse

JWT parts use a URL-safe alphabet. Plus becomes a dash and slash becomes an

underscore, so the token can travel in headers and URLs without escaping.

Standard atob rejects that alphabet, so the decoder converts it back and adds

the missing padding:

pad = (4 - (length % 4)) % 4

We verified the two real cases with the RFC 7519 example token. The header

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 is 36 characters. 36 modulo 4 is 0, so

zero padding characters are added. The payload

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

is 74 characters. 74 modulo 4 is 2, so two equals signs are appended before

atob runs. Skip that padding step and the second, longer half of the token

family fails to decode even though the token is valid.

What the header and payload actually prove

The decoder pretty-prints both parts as two-space indented JSON and shows two

badges above them.

The algorithm badge copies the alg field from the header. Treat it as a claim

written by whoever built the token. A header that says HS256 does not prove

the signature used HS256, and a header that says none is a claim you should

reject in server code regardless of what a decoder displays.

The expiry badge appears only when the payload contains exp and that value is

a number. The tool multiplies it by 1000, renders a local date, and compares

it against a captured timestamp. For a deterministic check, a payload with

exp set to 1000000000 renders as 2001-09-09 01:46:40 UTC and shows the

expired badge on any clock set after 2001. Numbers you can reproduce in one

paste.

Everything else in the payload, sub, iat, scopes, tenant ids, is displayed

as data. Readable is not the same as verified.

The signature is displayed, never checked

The third part appears as-is in base64url with a note that it cannot be

decoded without the secret. That note is the honest framing. The signature is

HMAC output over the first two parts. You can base64-decode those bytes, but

they carry no readable content, and without the signing secret you cannot

recompute and compare them. A browser decoder that shows a green checkmark on

an arbitrary pasted token is asserting something it did not test. Ours shows

claims, and stops there.

Verification belongs where the secret lives. Your server recomputes the

signature, checks the algorithm allowlist, and checks exp against its own

clock on every request.

The expiry badge can go stale, and that is a real bug class

The tool captures the current time once, in the state initializer that runs

when the page loads. Every expiry comparison for the life of that page view

uses that snapshot. Paste a token with exp ninety seconds in the future, read

"valid", leave the tab open for two minutes, and the badge still says valid.

The token is expired, the display is not.

The fix on the reader side is simple. Treat the badge as a hint and make your

own decision with a fresh timestamp. On our side it is a known defect worth

fixing with a periodic clock update.

Steps to inspect a token

1. Count the dots before you paste. Two dots means three parts. Any other

count will fail the gate.

2. Paste the token. A red invalid card means the split or the JSON parse

failed. Check for a truncated copy before you suspect the token.

3. Read the alg badge. Decide whether your server would accept that

algorithm.

4. Read the expiry badge against the stale-clock caveat above.

5. Copy the payload JSON with the copy button and check each claim you plan

to trust.

6. Remember the signature pane is informational. Do verification server-side.

Checklist before you trust what you read

  • Token splits into exactly three parts on dots.
  • Header and payload decode and parse as JSON.
  • alg value is on your server allowlist.
  • exp compared against a clock fresher than the page load.
  • No authorization decision made from the decoded payload alone.

If you have a token shape that a decoder rejects but your backend accepts,

that mismatch is worth documenting. Test the three-part gate, the padding

arithmetic, and the expiry badge with your own sample tokens in the

JWT decoder at https://webrecast.com/en/jwt-decoder