JWT Decoder & Inspector — Free HTML CSS JS Snippet

JWT Decoder & Inspector · Dev · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Real base64url decoding (not plain base64) matching RFC 4648 §5, including correct re-padding
UTF-8 safe decode via TextDecoder on a Uint8Array, so unicode claim values render correctly
Independent JSON.parse try/catch for header and payload — one bad segment doesn't hide the other
Live status badge: Decoded OK, Expired (via exp claim vs current time), or malformed/invalid
Colored raw-segment breakdown showing exactly which characters are header, payload, and signature
Quick-reference claim chips for alg, typ, iat, exp, and sub with human-readable UTC timestamps
Entirely client-side — no network request, no token ever transmitted anywhere
Updates on every keystroke via a single input listener, no submit button required

About this UI Snippet

JWT Decoder — Client-Side Base64Url Decode of Header, Payload & Expiry Detection

Screenshot of the JWT Decoder & Inspector snippet rendered live

A JSON Web Token is three base64url-encoded segments joined by dots: a header describing the signing algorithm, a payload carrying claims, and a signature proving the first two segments weren't tampered with. Debugging a JWT usually means copy-pasting it into some third-party website — this snippet does the same decoding entirely in the browser, with no network request and no token ever leaving the page.

Base64url is not the same as base64

JWTs use base64url encoding (RFC 4648 §5), which swaps + and / for - and _ and drops trailing = padding so the token is safe to embed in URLs and HTTP headers without escaping. The browser's built-in atob() only understands standard base64, so base64UrlDecode() first reverse-swaps those characters and re-pads the string to a multiple of four characters before calling atob(). The result is then run through TextDecoder('utf-8') on a Uint8Array built from the decoded binary string — a detail that matters because atob() alone mangles any non-ASCII characters in claim values (unicode names, for example) if you skip the byte-array round trip.

Splitting and rendering the three segments

decode() calls token.split('.') and immediately checks the result has exactly three parts. A token with the wrong number of segments is flagged as malformed rather than silently decoding garbage. Each of the three raw segments is also rendered as a colored inline chip in the .segments row so you can see visually which characters of the raw token correspond to header, payload, and signature — useful when comparing two tokens byte-for-byte.

Header and payload are parsed as JSON, independently

The header and payload segments are decoded and JSON.parsed in separate try/catch blocks. This means a JWT with a valid header but a corrupted payload (or vice versa) still shows you the segment that *did* parse correctly, with a clear message on the one that didn't — rather than a single try/catch around the whole token that would hide which half failed.

Expiry detection using the `exp` claim

Per RFC 7519, the exp claim is a Unix timestamp (seconds, not milliseconds) after which the token should be rejected by any consumer. The tool compares payload.exp against Math.floor(Date.now() / 1000) and flips the status badge to an amber "Expired" state when the token is past its expiry — a fast way to check "is this the stale token I've been debugging with all morning" without doing the math by hand.

Signature is shown, never verified

The signature segment is displayed raw as the third panel, but this tool makes no attempt to verify it — verifying an HMAC-signed token requires the shared secret, and verifying an RS256/ES256 token requires the issuer's public key, neither of which a client-side decoder should ever ask you to paste in. The note under the signature panel is intentional: decoding a JWT tells you what it *claims*, not whether it's authentic. Trusting a JWT's contents always requires signature verification on a server that holds the correct key.

Claim chips for common fields

Below the three panels, decode() builds small chips for whichever of alg, typ, iat, exp, and sub are present, formatting the Unix timestamps into a readable UTC string via fmtTime(). This gives an at-a-glance summary without having to mentally parse the raw JSON for the fields you check most often when debugging an auth issue.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's JavaScript into an AI assistant like Claude and ask it to walk through exactly why base64url decoding needs the character substitution and re-padding steps before atob() will accept the string — it's a detail that trips up a lot of hand-rolled JWT tooling. It's also a good starting point to extend: ask for HS256 signature verification using the Web Crypto SubtleCrypto API when a shared secret is supplied, support for the nbf (not-before) claim alongside exp, or a compact "copy as curl Authorization header" button.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a client-side JWT (JSON Web Token) decoder in plain HTML, CSS, and JavaScript, no libraries.

Requirements:
- A textarea where a user pastes a three-segment dot-separated JWT (header.payload.signature), decoding live on every input event.
- Implement real base64url decoding (RFC 4648 section 5: - and _ instead of + and /, no padding) built on the browser's atob(), including correct re-padding, and decode the resulting bytes through TextDecoder('utf-8') so unicode claim values render correctly rather than through atob()'s output directly.
- Parse the decoded header and payload segments as JSON independently, in separate try/catch blocks, so a failure in one segment doesn't prevent the other valid segment from displaying.
- Render the header and payload as pretty-printed JSON in separate panels, and show the raw signature segment in a third panel with a clear note that the tool does not and cannot verify the signature without the signing key.
- Show a status badge that reads a decoded-OK state, an amber "Expired" state when the payload's exp claim (a Unix timestamp in seconds) is earlier than the current time, or an invalid/malformed state when the token doesn't have exactly three segments or fails to decode.
- Never make a network request — everything must run entirely in the browser using only built-in APIs.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste a JWT into the textareaAny three-segment token (header.payload.signature) decodes live as you type or paste — no submit button needed.
  2. 2
    Read the colored segment breakdownThe row under the textarea shows exactly which raw characters belong to the header, payload, and signature.
  3. 3
    Inspect the Header and Payload panelsBoth segments are base64url-decoded and pretty-printed as JSON. A parse failure on either segment is reported independently.
  4. 4
    Check the status badgeIt reads "Decoded OK", "Expired" (when the exp claim is in the past), or an error state for malformed tokens.
  5. 5
    Scan the claim chipsCommon fields — alg, typ, iat, exp, sub — are pulled out into quick-reference chips with human-readable timestamps.
  6. 6
    Remember: this never verifies signaturesThe signature panel is display-only. Verifying authenticity requires the signing secret or public key on a trusted server, never in the browser.

Real-world uses

Common Use Cases

Debugging an auth integration
Paste a token straight from a browser dev-tools network tab or an Authorization header to see exactly which claims and expiry your backend is issuing, without leaving the browser or hitting a third-party decoder site.
Teaching how JWTs are structured
Use the colored segment breakdown to show students or teammates that a JWT is just three base64url JSON blobs joined by dots — not an opaque encrypted blob — and why the signature alone is what makes it trustworthy.
Internal developer tooling
Embed alongside an API response inspector or an API key manager in an internal dev-tools dashboard for quick token debugging during support tickets.
Verifying token expiry during incident response
When users report being logged out unexpectedly, decode the token they were using and check the exp claim against the incident timestamp to rule in or out a premature-expiry bug.
QA and staging environment checks
Confirm that a staging environment is issuing tokens with the expected alg, audience, or custom claims before promoting an auth change to production.
Related: CIDR / Subnet Calculator
See the CIDR / Subnet Calculator for a related dev pattern worth pairing with this one.
Related: User-Agent String Parser
See the User-Agent String Parser for a related dev pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

No. Verifying a signature requires either the shared HMAC secret (for HS256) or the issuer's public key (for RS256/ES256/etc.), and a client-side tool should never ask you to paste secrets into it. This snippet only decodes and displays the three segments — treat the decoded payload as unverified claims, not proof of authenticity.

JWTs use base64url encoding, which replaces the standard base64 characters + and / with - and _ and omits trailing = padding so the token is URL-safe. Calling atob() on a raw JWT segment without first reversing those substitutions and re-adding padding produces garbage or throws. base64UrlDecode() handles both steps before delegating to atob().

If the pasted text doesn't split into exactly three dot-separated segments, the badge reports it as malformed immediately. If it has three segments but one doesn't decode to valid base64url JSON, that specific panel reports a decode/parse failure while the other panel (if valid) still renders normally.

The tool reads the payload's exp claim, a Unix timestamp in seconds per RFC 7519, and compares it against Math.floor(Date.now() / 1000). If exp is in the past, the status badge switches to an amber "Expired" state.

No — the decoder runs the base64url-decoded binary string through a Uint8Array and TextDecoder('utf-8') rather than trusting atob()'s raw output directly, which is what makes multi-byte UTF-8 sequences in claim values render correctly instead of as mojibake.

No. Everything happens with browser built-ins (atob, TextDecoder, JSON.parse) directly in the page — nothing is transmitted anywhere, which is exactly why it's safe to paste production tokens into it for debugging.