Command Palette

Search for a command to run...

JWT Decoder Online: Decode, Inspect, and Actually Understand Your Tokens

JWT Decoder Online: Decode, Inspect, and Actually Understand Your Tokens

T
Toolz Team
|Jul 11, 2026|21 min read

Last spring, users started getting logged out of toolz.dev. Not sometimes โ€” constantly. Sign in, click one tool, boom: back to the login screen. The backend issues 15-minute access tokens and 7-day refresh tokens, and I'd tested that flow a hundred times. So naturally I assumed the refresh endpoint was broken and spent an hour and forty minutes reading Express middleware that had nothing wrong with it.

Then I finally did the obvious thing. I grabbed a live access token out of the Authorization header, pasted it into a decoder, and looked at the claims. The exp was fine. The iat was fine. The token was valid for another 14 minutes. Which meant the server was fine โ€” and the bug had to be on the client. Sure enough: my frontend was checking payload.exp < Date.now(). exp is seconds since epoch. Date.now() is milliseconds. Every freshly minted token looked like it had expired somewhere around 1970, so the client "helpfully" logged everyone out before the server ever got a say. Three characters of fix โ€” /1000 โ€” after nearly two hours of hunting.

That's the whole pitch for having a JWT decoder in your toolbox. A JWT looks like line noise โ€” three chunks of base64url gibberish glued together with dots โ€” but it's just JSON wearing a trench coat. The moment you can read the claims, half your auth bugs stop being mysteries. Wrong audience, expired token, missing role, clock skew, milliseconds-vs-seconds โ€” they're all sitting right there in plain text once you decode.

But โ€” and this matters โ€” where you decode is not a neutral choice. A real access token is a live credential. Paste it into a decoder site that ships tokens to a server, and you've just deposited a working key to your API into a stranger's request logs. That's the specific reason I built the toolz.dev JWT Decoder to run entirely in your browser. More on that below.

TL;DR: To decode a JWT online, paste it into the toolz.dev JWT Decoder โ€” it splits header, payload, and signature instantly, translates exp/iat into human dates, and runs 100% client-side so the token never leaves your machine. One thing to burn into memory: decoding is NOT verifying. A JWT is just base64url-encoded JSON that anyone can read โ€” only signature verification with the key proves it's trustworthy.

Key Features

Instant Header, Payload, and Signature Split

Paste a token and the decoder immediately breaks it into its three parts: the header (algorithm and token type), the payload (your claims), and the signature (left encoded, because it's a raw MAC or signature โ€” there's nothing human-readable in there). No submit button, no page reload. This mirrors exactly what your auth library does internally before verification: split on ., base64url-decode the first two segments, parse as JSON. Seeing the parts laid out side by side is the fastest way to build intuition for the format. After a few dozen tokens, you'll start recognizing an RS256 Auth0 token versus an HS256 Laravel token at a glance โ€” the header gives it away every time.

Human-Readable exp, iat, and nbf Timestamps

The single most useful feature, full stop. exp, iat, and nbf are NumericDate values โ€” seconds since the Unix epoch โ€” and nobody, myself included, can read 1783430700 and tell you whether that's next Tuesday or the heat death of the universe. The decoder converts every timestamp claim into an actual date and time, in your local timezone and UTC. This is where the classic milliseconds-vs-seconds bug becomes instantly visible: if your decoded exp renders as a date in the year 56,000-something, someone stuffed a JavaScript Date.now() into a field that expects seconds. I've shipped that bug. Seeing the absurd date is the diagnosis. For deeper timestamp archaeology, the Timestamp Converter is one tab away.

Expiry Countdown and Status

Beyond just rendering the date, the decoder tells you the token's current state: valid, expired, or not-yet-active (when nbf is in the future). If the token's still alive, you get a countdown to expiry. This sounds like a small convenience until you're debugging an intermittent 401 and need to answer "was this specific token dead when the request fired?" over and over. Comparing the countdown against your server's configured TTL also catches misconfiguration fast โ€” if your access tokens are supposed to live 15 minutes and the countdown says 6 days, your issuing code is reading the wrong config value.

Algorithm and Header Inspection

The decoded header shows you alg and typ (plus kid and friends when present), which answers questions that matter for security, not just debugging. Is this token HS256 or RS256? Does the kid match a key your JWKS endpoint actually serves? And the big one: is alg something it should never be, like none? Tokens claiming "alg": "none" are either test fixtures or someone probing your verifier โ€” either way, you want to see it immediately. I check the header first on every unfamiliar token, before I read a single claim.

Syntax-Highlighted, Formatted JSON Claims

Raw decoded payloads are single-line JSON blobs, and identity providers love to pack them: nested objects, namespaced custom claims, arrays of scopes. The decoder pretty-prints everything with syntax highlighting so roles, scope, aud arrays, and nested permission objects are actually scannable. It's the same treatment the JSON Formatter gives arbitrary JSON, applied automatically to your claims. When you're comparing two tokens โ€” say, one from a user who can access an endpoint and one from a user who can't โ€” formatted output turns a squinting exercise into a ten-second diff.

100% Client-Side โ€” Your Token Never Leaves the Browser

This is the feature I'd fight for. A pasted access token is not sample data โ€” it's a live credential that authenticates as a real user until exp. Any decoder that POSTs your token to a backend has just written a working key into server logs, analytics, maybe a third-party error tracker. The toolz.dev decoder does all decoding in JavaScript, in your tab. Nothing is transmitted, nothing is stored. Don't take my word for it: open DevTools, watch the Network tab, paste a token. Zero requests. I wrote up why this architecture matters for every sensitive-input tool in my piece on data privacy in online tools.

Works With Any JWT, From Any Stack

JWTs are a standard โ€” RFC 7519 โ€” so the decoder doesn't care who minted yours. Auth0 and Firebase tokens with their namespaced custom claims, Laravel Sanctum-adjacent setups, Keycloak, Supabase, AWS Cognito, or the hand-rolled HS256 tokens my own Express backend signs for toolz.dev โ€” if it's three base64url segments joined by dots, it decodes. That includes malformed almost-JWTs: if segment two won't parse as JSON, the decoder tells you which part is broken instead of failing silently, which is itself diagnostic. Truncated-on-copy tokens are more common than you'd think.

How to Use the JWT Decoder

Step 1: Grab the Token

Find the token wherever your app keeps it. Most commonly: DevTools โ†’ Network tab โ†’ click a request โ†’ copy the Authorization: Bearer eyJ... header value (without the word "Bearer"). Or check Application โ†’ Local Storage / Cookies, since plenty of apps stash tokens there. On the backend, log it or pull it from your test suite. Copy the entire string โ€” a JWT that loses its last few characters still decodes but will never verify, and that's a confusing hour you don't need.

Step 2: Paste It In

Open the JWT Decoder and paste. Decoding happens as you type โ€” no button. If you're nervous about pasting a production token anywhere (good instinct), open the Network tab first and confirm nothing is transmitted. It isn't. That paranoia check takes ten seconds and it's exactly what I'd do on someone else's tool.

Step 3: Read the Three Parts

Header first: confirm alg is what your system expects and typ is JWT. Then the payload: iss (who minted it), aud (who it's for), sub (which user), plus whatever roles, scopes, or custom claims your stack adds. The signature stays encoded โ€” it's cryptographic output, not data. If the header says none, stop and go check your verifier's allow-list before anything else.

Step 4: Check exp and the Claims That Bite

Look at the decoded exp date and the expiry status. Expired? There's your 401. Valid but rejected anyway? Now compare aud and iss against your verifier's config โ€” mismatches there are the second-most-common cause after expiry. And if any timestamp renders as a five-digit year, congratulations: you've found a milliseconds-vs-seconds bug, and I hereby welcome you to a very large club.

What's Actually Inside a JWT? Anatomy of the Three Parts

A JSON Web Token, defined in RFC 7519, is three base64url-encoded segments joined by periods: header.payload.signature. (Strictly, the signed variety is a JWS per RFC 7515 โ€” there's an encrypted cousin, JWE, but nearly every token you'll meet in the wild is a signed JWS.)

The key word is encoded. Base64url is a transport encoding โ€” a reversible way to make bytes URL-safe โ€” not encryption. Anyone holding a JWT can read everything in the header and payload with zero keys, zero secrets, zero effort. Play with the raw encoding in the Base64 Converter and you'll see it's the standard alphabet with + and / swapped for - and _, padding dropped. I wrote more about the encoding itself in the Base64 encoding guide.

Decode a typical header and you get:

{ "alg": "HS256", "typ": "JWT" }

And a payload built from the registered claims RFC 7519 defines:

{
  "iss": "https://toolz.dev",
  "sub": "user_8f3a2c",
  "aud": "toolz-api",
  "exp": 1783431600,
  "nbf": 1783430700,
  "iat": 1783430700,
  "jti": "b4d1f0e2"
}

iss is the issuer, sub the subject (usually your user ID), aud the intended audience, jti a unique token ID. exp, nbf, and iat are NumericDate values: seconds since the Unix epoch. Not milliseconds. JavaScript's Date.now() returns milliseconds, and confusing the two produces either tokens that expire instantly (my toolz.dev logout bug) or tokens with exp dates in the year 56,000 that effectively never expire โ€” which is quietly the more dangerous failure.

HS256 vs RS256. HS256 signs with an HMAC over a shared secret โ€” fast, simple, but every service that verifies tokens also holds the secret, and anyone holding the secret can mint tokens. Fine for a monolith like my backend, where issuer and verifier are the same process. RS256 signs with a private key and verifies with a public one, so you can publish the public key (via JWKS) and let a dozen microservices verify without any of them being able to forge. Distributed systems and third-party IdPs should be on RS256 or its ECDSA/EdDSA siblings.

The alg: none attack. RFC 7519 permits unsecured JWTs where alg is none and the signature is empty. Early libraries trusted the header's alg blindly, so attackers stripped the signature, set alg to none, and sailed through verification with fully attacker-controlled claims. A related trick swaps RS256 for HS256 so the verifier uses the public key as an HMAC secret. This is why RFC 8725 โ€” JSON Web Token Best Current Practices โ€” is blunt: the verifier must pin its allowed algorithms in code and never let the token choose. If your library call doesn't include an explicit algorithms list, fix that today.

Decoding vs verifying โ€” the line that matters. Decoding is reading; verifying is trusting. The difference in code:

// Decoding: no secret, no trust. Anyone can do this.
const payload = JSON.parse(
  Buffer.from(token.split('.')[1], 'base64url').toString()
);

// Verifying: proves the signature AND pins the algorithm.
const verified = jwt.verify(token, secret, { algorithms: ['HS256'] });

An online decoder does the first thing. It can show you the claims; it cannot โ€” and should not pretend to โ€” tell you the token is authentic. Only verify, with the key, does that. Never make authorization decisions off decoded-but-unverified claims.

Which leads to the last rule: never put secrets in a JWT payload. No passwords, no API keys, no data you'd mind an attacker reading. The payload is public by construction โ€” signed against tampering, wide open to reading. If it's in the token, assume the whole internet can see it.

Common Use Cases

Debugging 401s During API Development

The 401 is the least informative status code in HTTP. The server said no โ€” but was the token expired? Wrong audience? Signed with a stale key? Missing entirely because your interceptor didn't fire? Decoding the actual token from the failing request collapses the search space in seconds. Half the time exp answers it alone. The other half, comparing iss and aud against your verifier's environment config finds a dev token being replayed against staging, or vice versa. I keep the decoder pinned next to my HTTP client for exactly this loop; it's a core entry in my API debugging toolkit. Decode first, read middleware second โ€” the reverse order cost me an hour and forty minutes once, and I intend to keep collecting interest on that lesson.

Explaining Surprise Logouts

When users report "it keeps logging me out," the token's timestamps are your witness statement. Decode a fresh access token and check the gap between iat and exp โ€” is it actually the 15 minutes you configured, or did an env var override it to 60 seconds? Then check whether the refresh token's 7-day window is what you think it is. Clock skew shows up here too: if your issuing server's clock runs a couple of minutes fast, tokens arrive already ancient by the client's reckoning. And of course, the seconds-vs-milliseconds comparison bug โ€” the one that bit toolz.dev โ€” announces itself the moment you see a perfectly valid exp on a token your client swears is expired.

Auditing What Your Identity Provider Puts in Tokens

Most teams have never actually read the tokens their IdP mints, and it's worth doing. Decode one and you may find email addresses, full names, picture URLs, tenant identifiers โ€” PII riding along in every single API request, stored in localStorage, and readable by anything that gets its hands on the token. Here's my opinionated stance, and I'll argue it with anyone: don't put user emails in JWT payloads. The sub claim exists precisely so you can carry an opaque identifier and look up the human details server-side. Every extra claim is data you're broadcasting and bytes you're paying for on every request. Decode, audit, then go trim your IdP's claim mappings.

Verifying Roles and Scopes During Authorization Debugging

Authentication says who you are; authorization says what you can do โ€” and when authorization misbehaves, the answer is in the claims. User swears they're an admin but gets 403s? Decode their token. If role says user, the token was minted before the promotion and they need to re-login โ€” a classic consequence of stateless tokens carrying stale snapshots. If the role is present but access still fails, check the exact claim name and shape: roles vs role, array vs string, scope as a space-delimited string vs scp as an array. Middleware checking payload.roles.includes('admin') against a payload that has role: "admin" fails silently and infuriatingly. Two decoded tokens side by side โ€” one working, one not โ€” usually settle it in under a minute.

Comparing Access and Refresh Token Contents

In a dual-token setup like mine, the two tokens should look meaningfully different, and decoding both is the audit. The access token: short exp, plus whatever claims the API needs per-request. The refresh token: long exp, a jti for revocation tracking, and as close to nothing else as possible. If your refresh token is carrying roles and profile data, something's off โ€” it's only ever presented to one endpoint and shouldn't duplicate the access token's job. This side-by-side check also catches the embarrassing bug class where both tokens accidentally get the same TTL, which turns your "15-minute access window" into security theater. Ask me how I know to check for that one.

JWT vs Opaque Session Tokens: An Honest Comparison

JWT Opaque session token
Statelessness Self-contained; any server with the key verifies without a lookup Server (or shared store like Redis) must look up every request
Revocation Hard โ€” valid until exp unless you build a denylist, which reintroduces state Trivial โ€” delete the server-side record, token dies instantly
Size per request Hundreds of bytes to over a kilobyte, on every request ~32โ€“64 bytes
Where validation happens Anywhere holding the (public) key โ€” great for microservices Wherever the session store lives
Claim freshness Snapshot at issue time; role changes wait for re-issue Always current โ€” reads live data
Debuggability Decode and read claims instantly Opaque by design; requires store access

I use JWTs for toolz.dev and I'll still tell you they're overprescribed. The revocation story is genuinely bad: when you ban a user, their access token keeps working until exp โ€” which is exactly why my access tokens live 15 minutes and the 7-day refresh token is the thing I can kill server-side. That hybrid is the honest pattern: short-lived stateless JWTs for cheap verification, one stateful checkpoint for control. If you're running a monolith with one database, plain sessions are simpler, smaller, and instantly revocable โ€” the JWT's superpower of distributed verification is solving a problem you don't have.

While we're comparing โ€” HS256 vs RS256 in one glance:

HS256 RS256
Key model One shared secret signs and verifies Private key signs, public key verifies
Who can mint tokens Anyone holding the secret Only the private key holder
Best fit Single service, issuer = verifier Microservices, third-party IdPs, JWKS
Signature size / speed Smaller, faster Larger, slower, safer to distribute

Frequently Asked Questions

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

Only if the decoder runs client-side. A real token is a live credential โ€” sending it to someone's server plants a working key in their logs. The toolz.dev JWT Decoder does all decoding in your browser and transmits nothing; you can confirm this yourself by watching the Network tab while you paste. For decoders you can't verify, use expired or test tokens only.

Can you decode a JWT without the secret?

Yes โ€” that's the point people miss most. The header and payload are base64url-encoded JSON, and encoding is not encryption. Anyone can read every claim without any key. The secret (or private key) is only needed to create or verify the signature. Decoding requires nothing; trusting requires verification.

What's the difference between decoding and verifying a JWT?

Decoding reads the contents: split on dots, base64url-decode, parse JSON. Verifying proves authenticity: recompute or check the signature using the secret or public key, confirm the algorithm, check expiry. A decoder shows you what a token claims; only verification, done server-side with the key, tells you whether to believe it. Never authorize based on decoded-but-unverified claims.

Why does my JWT show as invalid or expired?

Most often the exp has genuinely passed โ€” decode it and check the date. Next suspects: a truncated token from a sloppy copy-paste, an aud or iss that doesn't match your verifier's config, clock skew between servers, or a signature from a rotated key. If the decoded exp looks fine but your code rejects the token, check whether you're comparing seconds against milliseconds.

Are JWTs encrypted?

Standard JWTs โ€” technically JWS, per RFC 7515 โ€” are signed, not encrypted. The signature detects tampering but hides nothing; the payload is readable by anyone. An encrypted variant (JWE) exists but is rare in typical web auth. Practical rule: treat every JWT payload as public, and never put passwords, API keys, or sensitive data in one.

What format is the exp claim in?

exp is a NumericDate: seconds since the Unix epoch (January 1, 1970 UTC), as defined in RFC 7519. Same for iat and nbf. The classic bug is using JavaScript's Date.now(), which returns milliseconds โ€” producing tokens that either look expired instantly or carry expiry dates around the year 56,000. If a decoded timestamp shows a five-digit year, that's your bug.

What is the alg none attack?

RFC 7519 allows unsecured JWTs with "alg": "none" and an empty signature. Old libraries trusted the header's algorithm field, so attackers stripped signatures, set alg to none, and passed verification with forged claims. RFC 8725, the JWT Best Current Practices, requires verifiers to pin an explicit allow-list of algorithms in code and ignore whatever the token asks for.

Should I use HS256 or RS256?

HS256 uses one shared secret for signing and verifying โ€” simple and fast, right for a single service that both issues and checks its own tokens. RS256 signs with a private key and verifies with a public one, so many services can verify without being able to forge. Rule of thumb: monolith, HS256; microservices or third-party identity provider, RS256.

Wrapping Up

I built the JWT Decoder because I kept needing it while building toolz.dev itself โ€” the same 15-minute access tokens and 7-day refresh tokens I've been dissecting throughout this guide. It decodes instantly, translates the timestamps that cause 90% of the confusion, and never sends your token anywhere. That last part isn't a feature checkbox; for a tool that handles live credentials, it's the whole design.

If tokens are part of your daily debugging, the neighbors will earn their keep too: the Timestamp Converter for epoch archaeology, the Base64 Converter for poking at raw segments, the JSON Formatter for claim blobs, and the Hash Generator when you're working with digests. For the wider workflow, my API debugging tools guide covers where a decoder fits in the loop.

And keep the one-sentence version taped to your monitor: decoding tells you what a token says, verification tells you whether to believe it. Confuse the two and you'll ship the kind of bug that ends up as the opening anecdote in someone's blog post. This time it was mine.

Comments

0 comments

0/2000 characters

No comments yet. Be the first to share your thoughts!