How to decode a JWT safely, without a website
A JWT is three Base64URL parts: header, payload and signature. Here is how to decode one offline, read exp, iat and nbf, and why decoding is not verifying.
Sanjeev Maharjan5 min read
To decode a JWT, split it at the two dots, take the first two parts (the header and the payload), turn each from Base64URL back into text and read the JSON inside. You can do this offline on your own machine, so the token never leaves it. Decoding shows you what the token claims; it does not prove the claims are true. Only checking the signature with the signer's key does that.
The short version: a JWT is header.payload.signature, each part Base64URL encoded. Decode the first two locally, read exp, iat and nbf as Unix timestamps, and never treat a decoded token as a verified one.
What a JWT is made of
A JSON Web Token is a short string with three parts joined by dots. It looks like xxxxx.yyyyy.zzzzz. Each part has a job.
- Header. A small JSON object that says which algorithm signed the token, such as
HS256orRS256, and usually"typ": "JWT". - Payload. A JSON object of claims: who the token is about, who issued it, who it is for and when it expires. Services add their own claims too, such as roles or an email address.
- Signature. Bytes made from the header and payload with a secret or a private key. It is what lets a server check that nobody changed the first two parts.
The header and payload are encoded with Base64URL, a variant of Base64 that swaps + for - and / for _ and drops the = padding, so the token is safe to put in a URL or a header. If Base64 itself is new to you, the guide on Base64 encoding and decoding on a Mac covers it.
The important thing to take from this: the payload is encoded, not encrypted. Anyone holding the token can read it. That is why a JWT should never carry a password or anything else you would not show the user.
How to decode a JWT offline
You do not need a website for this. In Terminal, take the middle part of the token and decode it. Base64URL needs its characters swapped back and its padding restored before the standard base64 tool will accept it:
TOKEN='eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3MDAwMDAwMDB9.sig'
PART=$(echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+')
while [ $(( ${#PART} % 4 )) -ne 0 ]; do PART="$PART="; done
echo "$PART" | base64 -D
That prints the payload JSON, here {"sub":"123","exp":1700000000}. Change -f2 to -f1 to read the header. On Linux use base64 -d. If you would rather not type a loop, most languages do it in a line: in Node, Buffer.from(part, 'base64url').toString() gives the same result.
Leave the third part alone. The signature is raw bytes, so decoding it gives you nothing readable.
Reading exp, iat and nbf
Three registered claims tell you about time. Each is a number of seconds since 1 January 1970 UTC, a Unix timestamp.
| Claim | Name | What it means |
|---|---|---|
iat | Issued at | When the token was made. |
nbf | Not before | The token must not be accepted before this time. |
exp | Expires | The token must not be accepted at or after this time. |
To turn one into a date on a Mac, run date -r 1700000000. If the date is in the past, the token has expired, and that is often the whole answer to a "why am I getting 401" question. Watch for clock skew too: if your machine's clock and the server's disagree, a token can look valid to one and expired to the other. The guide on converting a Unix timestamp explains seconds, milliseconds and time zones in more depth.
Decoding is not verifying
This is the part people get wrong. Decoding a token tells you what it says. It tells you nothing about whether it is genuine. Anyone can write a header and payload, encode them and stick any bytes on the end.
Verifying means recomputing the signature with the right key and comparing it. For HS256 that is the shared secret; for RS256 or ES256 it is the issuer's public key. A server must verify every token it receives, and it must decide for itself which algorithm it accepts rather than trusting the header. Never accept "alg": "none".
If your code reads claims from a token without checking the signature first, anyone can make themselves an admin by editing the payload. Use your framework's verify function, not a decode function, wherever trust matters.
Why pasting tokens into websites is a risk
A JWT is often a live credential. A bearer token from your browser or an API log lets whoever holds it act as you until it expires. Pasting it into a random decoding website hands it to a page you do not control, running scripts you have not read, possibly with analytics and logging attached. Even if the site is honest, you have no way to check.
The safer habit is simple: decode on your own machine, and if you must share a token with someone, share the decoded header and payload, not the token itself. The same thinking applies to AI chats; see keeping secrets out of AI prompts.
How DailyUtil does it
DailyUtil is a native Mac app, and its JWT Debugger is one of the free tools. Paste a token and it decodes the header and payload, shows the algorithm, and shows the issued and expiry dates as readable times, with an "Expired" warning when the token is past its exp. The signature is shown as "Not verified: that needs the signer's key", because decoding cannot honestly claim more. Everything runs offline on the Mac; nothing pasted is sent anywhere by the tool. If a token is on your clipboard when you open the window, DailyUtil notices the three dot-separated Base64URL parts and opens the JWT Debugger, locally. If you ask the AI to explain a token, only its decoded header and payload are sent, never the token itself. You can download DailyUtil for macOS 14 or later on Apple Silicon.
Questions people ask
Is it safe to decode a JWT?
Decoding itself is harmless; it only reads text that was already readable. The risk is where you do it. Decode on your own machine rather than pasting a live token into a website you do not control.
Can I decode a JWT without the secret?
Yes. The header and payload are only Base64URL encoded, so anyone can read them without a key. You need the secret or the public key only to verify the signature, which proves the token is genuine and unchanged.
Why does my JWT say it has expired?
Its exp claim is a Unix time in the past, or your clock disagrees with the server's. Convert exp to a date to check, then get a fresh token from the issuer rather than editing the old one.
Is a JWT encrypted?
A standard signed JWT is not encrypted. Its payload is readable by anyone who holds it. Encrypted tokens exist under a separate standard called JWE, but the common three part tokens you meet in APIs are signed only.
Written by Sanjeev Maharjan
Sanjeev Maharjan builds DailyUtil, developer tools for the Mac. Found a mistake in this guide? Write to [email protected].