JWT Decoder
Decode JWT tokens and inspect header, payload, and signature claims.
The JWT Decoder takes a header.payload.signature token and unpacks it into a Claims Summary, pretty Header and Payload JSON blocks, and the raw Signature segment — all without verifying anything. Paste a token (or click "Load example" for a canned HS256 JWT with sub/name/iat/exp/iss/role claims) and after a 150 ms debounce you get an expiry pill ("Valid for {duration}", "Expired {duration} ago", or "No expiry (exp) claim") plus claim rows for Algorithm, Type, iss, sub, aud, jti, iat, nbf, and exp — missing values show "—".
Each of Header, Payload, and Signature has its own Copy button; iat/nbf/exp timestamps render as both raw seconds and locale-formatted dates, and audience arrays are joined with ", ". Errors are precise: "Empty input", "Invalid JWT: expected 3 dot-separated segments, found {n}", "Invalid {header|payload}: not valid Base64URL", or "Invalid {header|payload}: not valid JSON". The card is explicit that it "Runs locally — nothing is uploaded" — and just as explicitly, it never verifies signatures.
How it works
lib/jwtDecoder.ts splits the token on "." (requiring exactly 3 segments), validates each part against the Base64URL alphabet /^[A-Za-z0-9_-]+$/, then decodes by mapping -→+, _→/, adding padding, calling atob, and running the bytes through TextDecoder before JSON.parse — which must yield a non-array object. getClaimSummary extracts iss/sub/aud/jti/iat/nbf/exp; getJwtAlgorithm/getJwtTokenType read header alg/typ with "Unknown"/"JWT" defaults.
getExpiryInfo compares exp*1000 against Date.now() and formats the delta via s → m → h → "Xd Yh" for the pill label. Decoding is debounced 150 ms after each keystroke to avoid thrashing on long pastes. Signature bytes are shown raw for inspection only — there is no key handling, no alg check, and no HMAC/RSASSA computation anywhere in the pipeline.
How to use
- Paste a JWT into the textarea (placeholder "Paste a JWT token (header.payload.signature)…") or click "Load example".
- Wait for the 150 ms debounce — Claims Summary renders if valid; otherwise an Alert shows the precise error.
- Check the expiry pill: "Valid for …", "Expired … ago", or "No expiry (exp) claim".
- Review the claim rows: Algorithm, Type, Issuer (iss), Subject (sub), Audience (aud), JWT ID (jti), Issued At (iat), Not Before (nbf), Expires At (exp) — missing values show "—".
- Expand the "Header" and "Payload" pretty JSON blocks (max-height scrollable) and use each section's Copy button as needed.
- Read the raw "Signature" segment — display only, no verification is performed.
- Click "Clear" to empty the token and reset the view.
Tips
- This decoder only decodes — it never verifies signatures or checks alg against a key, so treat decoded claims as untrusted input.
- iat, nbf, and exp show both the raw seconds value and a toLocaleString() human date for cross-checking.
- Audience arrays are joined with ", " in the claim row, so multi-audience tokens read cleanly.
- Empty input returns a friendly "Empty input" rather than the segment-count error, so a cleared box doesn't look broken.
JWT Decoder FAQ
Does it verify the signature?
No — only Base64URL decode + JSON parse of header and payload; the signature is displayed raw for inspection and never checked.
What if the token doesn't have 3 segments?
You get "Invalid JWT: expected 3 dot-separated segments, found {n}" — the decoder requires the standard header.payload.signature shape.
How is expiry calculated?
getExpiryInfo compares exp * 1000 against Date.now() and labels the pill "Valid for …" or "Expired … ago" using an Xd Yh formatter.
Is the token uploaded?
No — the card says "Decode JWT tokens and inspect their claims. Runs locally — nothing is uploaded." Decoding is pure atob/TextDecoder/JSON.parse in the browser.
Credits
- Web APIs (atob, TextDecoder) — Base64URL decode + JSON parse implemented in lib/jwtDecoder.ts; no jwt library