How to Decode a JWT (and Why Decoding Isn't Verifying)

A JWT is three base64url-encoded strings joined by dots. Anyone can decode the first two parts and read the contents — that's by design. The third part is the signature, and verifying it is the only thing that tells you whether to trust what you read. This is the most common JWT misconception, and it has shipped real auth bugs.

The shape of a JWT

A JSON Web Token (RFC 7519) is three base64url-encoded segments separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Split on the dots and base64url-decode each segment, and you get:

That's it. The first two parts are JSON. The third is binary signature data, base64url-encoded for transport. The whole token is URL-safe text you can paste into headers, query strings, or cookies.

What "base64url" means

Base64url is base64 with two substitutions and padding stripped:

That makes the encoded string safe to drop into a URL without further escaping. To decode it manually, restore the padding ('='.repeat((4 - input.length % 4) % 4)) and substitute back, or use a base64url-aware decoder. Our base64 tool handles both variants.

Decoding ≠ verifying

This is the most important section. The header and payload are not encrypted. They are not signed-into-the-bytes. They are public, plaintext JSON behind a thin base64 wrapper. Anyone with the token can read everything in the payload.

The third segment, the signature, is what proves the token wasn't forged. To verify it:

  1. Take the bytes header_b64 + "." + payload_b64 (the literal bytes of the first two segments joined by a dot).
  2. Compute the signature of those bytes using the algorithm specified in header.alg and the issuer's secret (HS256) or private key (RS256/ES256).
  3. Compare your computed signature to the third segment.

If they match, the token wasn't tampered with after the issuer signed it. If they don't, somebody changed the header or payload — reject the token.

Verification requires the secret (HS*) or the public key (RS*/ES*). If you don't have either, you cannot verify. You can still decode and read the payload, but reading is not authentication.

Why this matters in practice

Real bugs have shipped because developers confused decoding with verifying. Two common patterns:

Our JWT decoder deliberately does not verify signatures — it's a debugging tool. Real verification belongs on your auth server with the actual key material.

Standard payload claims

RFC 7519 defines several "registered claim names" that have agreed-upon meanings:

ClaimNameTypeMeaning
issIssuerstringWho issued this token (your auth server's identifier)
subSubjectstringWho the token is about (typically a user ID)
audAudiencestring or arrayWho the token is intended for (your API's identifier)
expExpirationnumber (Unix seconds)Token is invalid after this time
nbfNot Beforenumber (Unix seconds)Token is invalid before this time
iatIssued Atnumber (Unix seconds)When the token was issued
jtiJWT IDstringUnique token identifier (for revocation)

You'll also see custom claims like email, roles, scope, etc. These are not standard, but parties that share a JWT format will agree on their semantics.

The exp claim is in Unix seconds (not milliseconds, despite some libraries getting this wrong). To check if it's in the past, compare against Math.floor(Date.now() / 1000). To convert to a human date, paste it into our Unix timestamp converter.

What's safe to put in a JWT

Because the payload is readable by anyone with the token, never put secrets in it. No passwords, no API keys, no PII you don't want exposed. The payload is for identity and authorization signals — user ID, role, scope, expiration. The opacity comes from the signature, not from any encryption.

If you need encrypted payload, use JWE (JSON Web Encryption, RFC 7516) instead of JWT. JWE wraps a JWT in an encryption layer; only parties with the decryption key can read the payload. This is rare in practice — most use cases are fine with signed-but-readable JWT.

Common debugging workflow

You're debugging an auth issue. The flow:

  1. Copy the JWT from the failing request (browser DevTools, server log, etc.).
  2. Paste into our JWT decoder. Read the payload.
  3. Check the standard claims: is exp in the past? Does aud match what your API expects? Does iss match your auth server?
  4. Check custom claims: does the user actually have the role/scope they need?
  5. If the payload looks right but verification still fails, check the algorithm in the header against what your verifier expects. RS256 vs HS256 mismatches are common.

Decoding tells you what the token says. Verifying tells you whether to trust it. Both have their place; don't confuse them.