every time you need to debug a JWT, you open jwt.io, paste your token, and hope you didn't just leak a production token to a third-party website.
stop doing that.
JWTs are just base64. you can decode them in one line. no websites, no npm packages, no risk.
the one-liner
echo 'YOUR_JWT_HERE' | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool
that's it. splits on the dots, grabs the payload (middle part), base64 decodes it, pretty prints the JSON. done.
wait, what even is a JWT?
a JWT (JSON Web Token) has three parts separated by dots:
header.payload.signature
each part is base64url encoded. the header says what algorithm was used. the payload has your actual data (claims). the signature proves it wasn't tampered with.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ.xxxxx
│ │ │
└─ header └─ payload (this is what you usually care about) └─ signature
you almost never need to verify the signature locally. you just want to see what's in the payload. which means you just need base64 decode.
make it a shell function
add this to your .bashrc or .zshrc:
jwt() {
local payload=$(echo "$1" | cut -d'.' -f2)
# fix base64url padding
local pad=$((4 - ${#payload} % 4))
[ $pad -ne 4 ] && payload="$payload$(printf '=%.0s' $(seq 1 $pad))"
echo "$payload" | base64 -d 2>/dev/null | python3 -m json.tool
}
now you can just do:
jwt eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ.xxxxx
output:
{
"sub": "1234567890",
"name": "John"
}
the node.js version
if you're already in a node project, you don't even need a function. it's built in.
function decodeJWT(token) {
const parts = token.split('.');
if (parts.length !== 3) throw new Error('not a valid JWT');
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
return { header, payload };
}
// usage
const token = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ.xxxxx';
const { header, payload } = decodeJWT(token);
console.log('algorithm:', header.alg);
console.log('payload:', payload);
console.log('expires:', payload.exp ? new Date(payload.exp * 1000) : 'never');
notice base64url — not base64. JWTs use a URL-safe variant that replaces + with - and / with _. node handles this natively with the base64url encoding. no extra processing needed.
a more useful version
here's what i actually use. it shows you everything you care about:
function inspectJWT(token) {
const parts = token.split('.');
if (parts.length !== 3) {
console.log('❌ not a valid JWT (expected 3 parts, got ' + parts.length + ')');
return;
}
try {
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
console.log('\n📋 JWT INSPECTION');
console.log('─'.repeat(40));
// header
console.log('\nHEADER:');
console.log(' algorithm:', header.alg);
console.log(' type:', header.typ || 'not specified');
if (header.kid) console.log(' key id:', header.kid);
// payload
console.log('\nPAYLOAD:');
for (const [key, value] of Object.entries(payload)) {
if (key === 'iat') {
console.log(` issued: ${new Date(value * 1000).toISOString()}`);
} else if (key === 'exp') {
const expDate = new Date(value * 1000);
const isExpired = expDate < new Date();
console.log(` expires: ${expDate.toISOString()} ${isExpired ? '⚠️ EXPIRED' : '✅ valid'}`);
} else if (key === 'nbf') {
console.log(` not before: ${new Date(value * 1000).toISOString()}`);
} else {
console.log(` ${key}: ${JSON.stringify(value)}`);
}
}
// signature
console.log(`\nSIGNATURE: ${parts[2].slice(0, 20)}... (${parts[2].length} chars)`);
console.log(' ⚠️ signature not verified (decode only)');
} catch (e) {
console.log('❌ failed to decode:', e.message);
}
}
this tells you:
- what algorithm was used (HS256, RS256, etc)
- when the token was issued and when it expires
- whether it's currently expired
- all the custom claims
- reminder that you're NOT verifying the signature
that last point matters. decoding ≠ verifying. anyone can decode a JWT. the signature is what proves it's legit. if you need to verify signatures, you need the secret key (HS256) or public key (RS256).
checking expiry from the command line
probably the most common thing i do — "is this token still valid?"
jwt_exp() {
local payload=$(echo "$1" | cut -d'.' -f2)
local pad=$((4 - ${#payload} % 4))
[ $pad -ne 4 ] && payload="$payload$(printf '=%.0s' $(seq 1 $pad))"
local exp=$(echo "$payload" | base64 -d 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('exp','none'))")
if [ "$exp" = "none" ]; then
echo "no expiry set"
else
local now=$(date +%s)
local diff=$((exp - now))
if [ $diff -lt 0 ]; then
echo "EXPIRED $((-diff)) seconds ago"
else
echo "valid for $((diff/3600))h $((diff%3600/60))m"
fi
fi
}
$ jwt_exp eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MTA1MjgwMDB9.xxxxx
EXPIRED 283847 seconds ago
when you DO want a browser tool
look, i get it. sometimes you want a visual breakdown. you're sharing something with a teammate. the terminal isn't always the answer.
i built a JWT decoder into the devtools site — it runs entirely in your browser, never sends the token anywhere. that's the key difference from jwt.io. no network requests with your token data.
but for day-to-day debugging? the terminal is faster. paste, decode, done. no tab switching, no cookie banners, no "sign up for our newsletter".
security stuff
real quick because this matters:
never paste production tokens into jwt.io or any online tool. you're sending auth credentials to a third party. yes, jwt.io says they decode client-side. do you trust that? i don't.
JWTs are not encrypted. they're just encoded. anyone with the token can read the payload. don't put sensitive data in there.
always validate on the server. client-side JWT checks are for UX only. the server must verify the signature on every request.
short expiry + refresh tokens. access tokens should expire in 15-60 minutes. use refresh tokens for longer sessions.
tl;dr
# decode a JWT in the terminal
echo 'TOKEN' | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# or in node.js
JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString())
stop pasting tokens into random websites. it takes 3 seconds in the terminal and your tokens stay on your machine where they belong.
Top comments (0)