← All articles

How to Decode a JWT and Read Its Claims

A JWT is three Base64url sections. Learn how to decode the header and payload in JavaScript and Python — and why decoding a token is not the same as verifying it.

A JWT (JSON Web Token) is three Base64url-encoded sections joined by dots. Decoding one to read its claims takes a single function call and no secret key — which is exactly why a JWT payload is not a safe place for sensitive data. This guide explains what a JWT is, how to decode each part, and the critical difference between decoding and verifying a token.

What Is a JWT?

A JWT is a compact, URL-safe token used to carry claims — typically a user's identity and permissions — between parties. It was standardised as RFC 7519. A token looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFkYSJ9.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

The two dots split it into three parts: header, payload, and signature.

The Three Parts

  • Header — a small JSON object describing the signing algorithm, e.g. {"alg":"HS256","typ":"JWT"}.
  • Payload — a JSON object of claims such as sub (subject), exp (expiry), and any custom fields.
  • Signature — a cryptographic signature over the header and payload, used to detect tampering. It is not human-readable and is not JSON.

The header and payload are Base64url-encoded JSON. The signature is produced by JWS (RFC 7515).

How to Decode a JWT in JavaScript

Split on the dots and Base64url-decode the first two parts. Note that JWT uses Base64url (with - and _ instead of + and /, and no padding), so convert before decoding:

function decodeJwt(token) {
  const [headerB64, payloadB64] = token.split('.');

  const decode = (b64url) => {
    // Base64url → Base64, then decode and parse
    const b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
    const json = atob(b64);
    return JSON.parse(json);
  };

  return { header: decode(headerB64), payload: decode(payloadB64) };
}

decodeJwt(token);
// {
//   header:  { alg: "HS256", typ: "JWT" },
//   payload: { sub: "1234", name: "Ada" }
// }

For full Unicode safety, decode the Base64 to bytes and run them through TextDecoder rather than atob alone.

How to Decode a JWT in Python

import base64, json

def decode_jwt(token):
    header_b64, payload_b64, _sig = token.split('.')
    def decode(part):
        # Add back padding Base64url strips
        padded = part + '=' * (-len(part) % 4)
        return json.loads(base64.urlsafe_b64decode(padded))
    return decode(header_b64), decode(payload_b64)

Decoding Is Not Verifying

This is the most important point about JWTs. Anyone can decode a token and read its payload — no key required. Decoding tells you what the token claims; it does not tell you whether those claims are trustworthy.

To trust a token you must verify the signature against the server's secret or public key, using a vetted library — never roll your own:

import jwt from 'jsonwebtoken';

// Verifies the signature AND checks expiry; throws if invalid
const claims = jwt.verify(token, process.env.JWT_SECRET);

// jwt.decode(token) — does NOT verify; use only for inspection

Verification on the server is what makes a JWT a credential. Treating a decoded payload as trusted — without checking the signature — is a classic authentication bug.

A JWT Payload Is Not Encrypted

Because the payload is just Base64url-encoded JSON, it offers zero confidentiality. This is the same misconception covered in Base64 Is Not Encryption: encoding is reversible by anyone.

Never put passwords, full credit-card numbers, or sensitive personal data in a JWT payload. If you genuinely need an encrypted token, use JWE (JSON Web Encryption, RFC 7516) — a separate, less common standard. And avoid pasting real tokens into untrusted online decoders; see why you shouldn't paste sensitive JSON into online tools.

Validation Order: Signature First, Then Claims

When you verify a token in production, the order matters. A safe checklist:

  1. Algorithm allow-list — reject anything not in your expected set (e.g. ['RS256']). Never accept none.
  2. Signature — verify against the right key (look up by kid in your JWKS).
  3. exp — token must not be expired.
  4. nbf (not-before) — token must not be used too early.
  5. iat — sanity-check the issued-at isn't absurdly in the future (clock skew aside).
  6. iss — issuer must match the trusted authority you expect.
  7. aud — audience must include your service.

Most JWT libraries do steps 1–4 by default and require you to opt in for 6–7. Until iss and aud are checked, a token valid for some other service in your ecosystem can be replayed against yours.

Common Standard Claims

ClaimMeaning
issIssuer — who created the token
subSubject — who the token is about (often a user ID)
audAudience — who the token is intended for
expExpiry — Unix timestamp after which the token is invalid
iatIssued-at — Unix timestamp when the token was created
nbfNot-before — token isn't valid until this time

The kid Header and the alg: none Pitfall

Two header fields show up so often in real JWTs that they're worth knowing on sight:

  • kid (key ID) — a hint that tells the verifier which key from a key set (JWKS) signed this token. It belongs in the header so the verifier can pick the right key before checking the signature. Treat kid as untrusted input: look it up in a fixed allow-list (or a fetched JWKS), never use it as a file path, a SQL parameter, or a URL.
  • alg: "none" — a legal but historically dangerous algorithm that means "no signature." Decoders happily decode it, and a permissive verifier may accept it, turning any forged payload into a "valid" token. Reject alg: none explicitly, and configure your library with the exact algorithm(s) you expect (e.g. ['RS256']) instead of trusting whatever the token's own header claims.

Decode a JWT in Your Browser

To inspect a token's claims quickly, paste either Base64url section into fixjson's Base64 decoder — it decodes to the underlying JSON locally, with nothing sent to a server. For non-sensitive tokens you can also use a dedicated debugger, but for anything containing real user data, a local-first tool is the safe choice.

Frequently Asked Questions

How do I decode a JWT?

Split the token on its dots and Base64url-decode the first two parts (header and payload) into JSON. The third part is the signature and isn't human-readable. No key is needed to decode.

Is decoding a JWT the same as verifying it?

No. Decoding just reads the claims; anyone can do it. Verifying checks the signature against a secret or public key to confirm the token is authentic and untampered. Always verify on the server before trusting a token.

Can anyone read the data inside a JWT?

Yes. The payload is Base64url-encoded, not encrypted — it's plaintext to anyone who has the token. Never store secrets in a JWT payload. See Base64 Is Not Encryption.

What's the difference between JWT, JWS, and JWE?

A signed JWT uses JWS (RFC 7515) — readable payload, tamper-evident. JWE (RFC 7516) actually encrypts the payload for confidentiality. Plain JWTs you see day-to-day are JWS.

What is the kid claim used for?

kid is a header field (not a payload claim) that identifies which key from a JWKS signed the token, so the verifier can pick the right one. Look it up against a fixed key set you control — never trust it as a file path or URL.

Why is alg: "none" dangerous?

It means "no signature." A permissive verifier that honours the token's own algorithm field will accept any forged payload. Always configure your library with the exact algorithm you expect (e.g. ['RS256']) and explicitly reject none.

Inspect Tokens Locally