How to Decode a JWT Without the Secret (and Why That Is Not Verification)

Updated

Yes, you can decode a JWT without the secret. A signed JWT's header and payload are only base64url-encoded JSON, so splitting the token at the dots and decoding the first two parts shows every claim. The secret or public key is needed only to verify the signature, which is the step that tells you whether the token is genuine and unchanged.

That difference matters in both directions. Developers can inspect tokens freely while debugging, and anyone who holds your token can read it too. Decoding is never a substitute for verification on the server.

Decoding a JWT vs verifying it A JWT has three parts separated by dots. The header and payload are base64url-encoded JSON that anyone can decode without a key. The signature can only be checked with the secret or public key, which is what verification does. Decoding a JWT vs verifying it eyJhbGciOi… . eyJzdWIiOi… . aWbbNTzOs7… Header Payload (claims) Signature Decode: no key needed base64url → bytes → UTF-8 → JSON {"alg":"HS256"} {"sub":"123…","exp":1767229200} Verify: key needed secret (HMAC) or public key (RSA/EC) Verification also checks the claims exp has not passed · aud is this API · iss is the expected issuer Fix the allowed algorithm on the server; never trust alg from the token (no "none").
A JWT split into header, payload and signature, showing which parts decoding reveals and which part needs the key

What is inside a JWT

RFC 7519 defines a JWT as a sequence of URL-safe parts separated by periods, each part base64url-encoded. A signed token (a JWS, defined in RFC 7515) has three parts:

Part Contains Needs a key to read?
Header JSON such as {"alg":"HS256","typ":"JWT"} No
Payload JSON claims such as sub, exp, iat No
Signature Bytes computed over header.payload with the key It is just bytes; the key is needed to check it

Base64url (RFC 4648, section 5) is ordinary Base64 with - and _ in place of + and /, and JWTs drop the = padding. To decode a part, you swap those two characters back, add padding if your decoder needs it, decode the Base64 and parse the JSON.

Here is a sample token used throughout this article:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRG9lIiwiaWF0IjoxNzY3MjI1NjAwLCJleHAiOjE3NjcyMjkyMDB9.aWbbNTzOs7uEA6DpB2uelr-7-QYf31QJPnzbaTj3aSA

It decodes to the header {"alg":"HS256","typ":"JWT"} and the payload {"sub":"1234567890","name":"Jane Doe","iat":1767225600,"exp":1767229200}. The iat and exp values are NumericDate, seconds since 1970-01-01T00:00:00Z, so this token was issued at 2026-01-01 00:00 UTC and expired one hour later.

Decode a JWT in code

JavaScript (browser)

function decodeJwtPart(part) {
  const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
  const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
  const bytes = Uint8Array.from(atob(padded), c => c.charCodeAt(0));
  return JSON.parse(new TextDecoder().decode(bytes)); // handles UTF-8 such as "Zoë"
}
const [header, payload] = token.split(".");
console.log(decodeJwtPart(header), decodeJwtPart(payload));

atob on its own returns one character per byte, which garbles non-ASCII text; TextDecoder turns the bytes back into UTF-8 correctly.

Node.js

const [h, p] = token.split(".").slice(0, 2)
  .map(s => JSON.parse(Buffer.from(s, "base64url").toString("utf8")));

Python

import base64, json

def b64url_decode(part):
    return base64.urlsafe_b64decode(part + "=" * (-len(part) % 4))

header, payload, _sig = token.split(".")
print(json.loads(b64url_decode(header)))
print(json.loads(b64url_decode(payload)))

Command line (bash)

p=$(printf '%s' "$JWT" | cut -d. -f2 | tr '_-' '/+')
while [ $(( ${#p} % 4 )) -ne 0 ]; do p="$p="; done
printf '%s' "$p" | base64 -d

Each of these snippets printed the payload above when run against the sample token. If you would rather not write code, paste the token into the JWT decoder; it runs in your browser and converts the dates for you.

Reading the dates in the payload

The time claims are plain numbers, so a decoded payload is easier to check once they are converted. RFC 7519 defines NumericDate as seconds since 1970-01-01T00:00:00Z UTC, ignoring leap seconds, while JavaScript dates count milliseconds:

const exp = 1767229200;                    // from the payload
new Date(exp * 1000).toISOString();       // "2026-01-01T01:00:00.000Z"
Date.now() / 1000 > exp;                  // true once the token has expired
from datetime import datetime, timezone
datetime.fromtimestamp(1767229200, tz=timezone.utc).isoformat()
# '2026-01-01T01:00:00+00:00'

Three claims matter most when a token is rejected:

  • exp in the past: the token has expired and must be refreshed.
  • nbf in the future: the token is not valid yet, often a clock difference between servers.
  • iat far in the past: an old token is being reused, for example from a cached session.

Why decoding is not verification

Anyone can create a token with any payload and encode it correctly. What they cannot do without the key is produce a valid signature. Verification therefore has two halves:

  1. Check the signature with the expected algorithm and key. For HS256 that means recomputing an HMAC-SHA256 over header.payload with the shared secret and comparing it in constant time. For RS256 or ES256 it means checking the signature with the issuer's public key.
  2. Validate the claims. RFC 7519 says the current time must be before exp. RFC 8725, the JWT best current practices, adds that applications must validate the audience and check that the issuer and subject are the ones they expect.

RFC 8725 also describes the classic mistakes that come from trusting the header too much:

  • alg: none. An attacker changes the algorithm to none, and a careless library accepts the token with no signature check. RFC 8725 says none should only be used when the JWT is protected by other means.
  • Algorithm confusion. An RS256 token is changed to HS256 and signed with the public key as if it were an HMAC secret. Libraries must let the caller fix the allowed algorithms instead of reading them from the token.

A good rule: the server decides which algorithm and key to use, and the token's header only has to agree.

Decoding vs verifying at a glance

Decode Verify
Needs secret or public key No Yes
Shows claims Yes Yes
Proves who issued the token No Yes
Detects tampering No Yes
Checks exp, aud, iss Only if you read them yourself Yes, as part of validation
Safe place to do it Anywhere, including the browser On the server that trusts the token

What this means for token design

  • Never store secrets in claims. Passwords, API keys and personal data you would not show the user should stay out of the payload.
  • Keep tokens short-lived. A readable token that leaks is less harmful if exp is close.
  • Use encryption when the payload must stay private. A JWE (five parts) encrypts the claims; only the protected header is readable without the key.
  • Treat client-side decoding as display only. A front end may read name or exp to update the interface, but access decisions belong to the server.

JWT parts are one of many places where Base64 appears. Base64 vs hex encoding explains why tokens use Base64 rather than hex, and why Base64 makes data about 33% larger shows what that costs in size. HMAC signatures are built on hash functions; the MD5 vs SHA-256 guide covers why SHA-256 replaced older hashes. For quick one-off decoding of a single part, the Base64 operations on the universal encoder decoder also work once you swap - and _ back to + and / and add = padding.

Frequently asked questions

Can anyone read the contents of my JWT?

Yes, for a normal signed JWT. The header and payload are base64url-encoded JSON, not encrypted, so anyone who has the token can read them. Do not put passwords, API keys or other secrets in JWT claims.

What is the difference between decoding and verifying a JWT?

Decoding turns the base64url text back into JSON and needs no key. Verifying recomputes or checks the signature with the secret (HMAC) or public key (RSA, ECDSA) and then validates claims such as exp, iss and aud. Only verification tells you the token is genuine.

Can I decode an encrypted JWT (JWE) without the key?

No. A JWE has five parts and its payload is ciphertext. You can read its protected header, but the claims need the decryption key.

Read the header and payload of a JSON Web Token. Shows exp and iat as dates.

Open the JWT Decoder

Sources