DEV Community

TateLyman
TateLyman

Posted on

stop using websites to decode JWTs — do it in the terminal

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

now you can just do:

jwt eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ.xxxxx
Enter fullscreen mode Exit fullscreen mode

output:

{
    "sub": "1234567890",
    "name": "John"
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
$ jwt_exp eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MTA1MjgwMDB9.xxxxx
EXPIRED 283847 seconds ago
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.

  2. JWTs are not encrypted. they're just encoded. anyone with the token can read the payload. don't put sensitive data in there.

  3. always validate on the server. client-side JWT checks are for UX only. the server must verify the signature on every request.

  4. 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())
Enter fullscreen mode Exit fullscreen mode

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)