How to decode a JWT (with and without code)

A JWT's header and payload are just base64url-encoded JSON — no key or secret is needed to read them. Split the token on dots, decode the first two parts, done. The only subtlety is that it's base64url (RFC 4648 §5): - and _ instead of + and /, with padding stripped.

The rule that matters: decoding proves nothing. Anyone can mint a token claiming to be anyone; only verifying the signature with a key makes claims trustworthy. Never make an authorization decision from decoded-but-unverified claims — that's the root of several real JWT attacks.

Online, without sending the token anywhere

Paste the token into the TokenPrism debugger. Decoding happens entirely in your browser — verifiably: no network request is made — and every known claim is annotated, timestamps like exp are translated, and an RFC 8725 linter flags weak patterns.

JavaScript

function decodeJwtPayload(token) {
  const part = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')
  return JSON.parse(atob(part))
}

In Node.js, Buffer.from(part, 'base64url').toString() handles base64url directly. For production code prefer a maintained library (jose, jwt-decode) — and remember neither call above checks the signature.

Python

import base64, json

def decode_jwt_payload(token):
    part = token.split('.')[1]
    padded = part + '=' * (-len(part) % 4)
    return json.loads(base64.urlsafe_b64decode(padded))

With PyJWT, jwt.decode(token, options={"verify_signature": False}) does the same — the explicit opt-out exists precisely so you can't skip verification by accident.

Java

String part = token.split("\\.")[1];
String json = new String(Base64.getUrlDecoder().decode(part), StandardCharsets.UTF_8);

Go

part := strings.Split(token, ".")[1]
payload, err := base64.RawURLEncoding.DecodeString(part)

Common decode failures

  • "Invalid character" / bad padding — you used a plain base64 decoder on base64url input, or forgot to re-pad in Python.
  • Only 2 segments, or 5 — 2 means an unsecured (alg:none) token; 5 means it's an encrypted JWE, whose payload can't be read without the key.
  • Absurd expiry dates — a producer wrote milliseconds into exp; it's defined in epoch seconds.