Skip to content
Back to Blog
developerjwt-decodersecurityauthentication

How to Decode a JWT (Decode vs Verify)

Anyone can base64-decode a JWT and read it; that is not the same as verifying it. Here is the difference, the attacks that exploit the gap, and how to decode safely.

SZ
Founder, Molixa
10 min read
Share
How to Decode a JWT (Decode vs Verify)
Table of contents7 sections

To decode a JWT, base64url-decode its first two dot-separated parts to read the header and payload as JSON. Decoding is just reading: it skips the signature, so it tells you what the claims say, not whether they are genuine. Verifying is the separate step that proves the token was not tampered with.

That distinction is the whole game. A JWT is signed, not encrypted, which means anyone holding the token can read every claim inside it without a key. Treating "I decoded it and the claims look right" as proof of authenticity is how real vulnerabilities slip into production. Let us pull a token apart, see exactly where decode ends and verify begins, then cover the attacks that live in that gap.

What a JWT Actually Contains#

A JSON Web Token is three base64url-encoded sections joined by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFkYSIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTczNTY4OTYwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

The three parts are header.payload.signature. Decode the first two and you get readable JSON:

// Header
{ "alg": "HS256", "typ": "JWT" }

// Payload (the claims)
{ "sub": "1234", "name": "Ada", "role": "admin", "exp": 1735689600 }

The alg tells you which algorithm signed the token (here HMAC-SHA256). The payload carries claims: sub (subject/user id), role, and exp (expiry as a Unix timestamp). The third part, the signature, is a keyed hash of the first two. Crucially, the payload is just base64url, which is encoding, not encryption. Never put a secret in a JWT payload. Anyone can read it.

Base64url is a URL-safe variant of base64: it swaps + and / for - and _ and drops padding. If you have ever tried to decode a JWT with a standard base64 tool and gotten an error, the character set is usually why.

Decode vs Verify: The Distinction That Matters#

These two operations look similar and are constantly conflated in tutorials. They are not the same, and mixing them up is a security bug.

DecodeVerify
What it doesReads header and payloadConfirms the signature is valid
Needs a key?NoYes (secret or public key)
Can a stranger do it?Yes, with any base64 toolNo, only the signer's counterpart
Proves authenticity?NoYes
When to useDebugging, inspecting claimsEvery time you trust the token

Here is the difference in code. Decoding is trivial and trustless:

// DECODE ONLY: reads claims, proves nothing
function decodeJwt(token) {
  const [header, payload] = token.split(".");
  const json = (part) =>
    JSON.parse(atob(part.replace(/-/g, "+").replace(/_/g, "/")));
  return { header: json(header), payload: json(payload) };
}

const { payload } = decodeJwt(token);
console.log(payload.role); // "admin" ... but is this token even real?

That code happily returns role: "admin" for any token you hand it, including one an attacker forged five seconds ago. Verifying is what makes the claims trustworthy:

// VERIFY: checks the signature and expiry before trusting anything
import jwt from "jsonwebtoken";

try {
  const claims = jwt.verify(token, process.env.JWT_SECRET, {
    algorithms: ["HS256"], // pin the algorithm explicitly
  });
  // Only now is claims.role safe to act on
  grantAccess(claims.role);
} catch (err) {
  // Signature invalid, token expired, or algorithm not allowed
  denyAccess();
}

The rule is simple: decode to look, verify to trust. Any code path that makes an authorization decision must verify. Decoding without verifying on the server is equivalent to letting users edit their own permissions.

The Attacks That Live in the Gap#

When developers skip or mishandle verification, three classic attacks open up. All of them exploit the fact that the token is attacker-controlled until the signature is checked correctly.

The "none" algorithm attack#

The JWT spec includes an alg value of none, meaning "unsigned." If a server reads the algorithm from the token's own header and trusts it, an attacker can strip the signature, set "alg": "none", and forge any payload they like:

// Attacker's forged header
{ "alg": "none", "typ": "JWT" }

// Attacker's forged payload: instant admin
{ "sub": "9999", "role": "admin" }

With the signature removed and alg set to none, a naive verifier that "honors" the header accepts it. The fix is to never let the token dictate its own algorithm. Pin the expected algorithm on the server side, exactly like the algorithms: ["HS256"] option above, so none and any other unexpected value is rejected outright.

The algorithm confusion attack (RS256 to HS256)#

This one is subtler and has bitten major libraries. Suppose your system signs tokens with RS256 (asymmetric: a private key signs, a public key verifies). The public key is, by definition, public.

An attacker takes that public key, switches the header to "alg": "HS256" (symmetric HMAC), and signs a forged token using the public key as the HMAC secret. A vulnerable verifier that picks the algorithm from the header will now run HMAC verification using the public key it already has, and the forged signature checks out. The attacker just used your published verification key to sign tokens.

The defense is the same principle: pin the algorithm. If your server expects RS256, configure it to accept only RS256 and verify with the public key as an RSA key, never as an HMAC secret. Allowing the header to choose is the root cause every time.

Ignoring expiry and other claims#

Even a perfectly verified signature does not mean the token is currently valid. Verification confirms the token was issued by you and not altered; it does not, on its own, check that the token is still in date or meant for this audience. Always validate:

  • exp (expiry) and nbf (not-before) so stale or future-dated tokens are rejected.
  • aud (audience) so a token minted for one service cannot be replayed against another.
  • iss (issuer) so you only accept tokens from the identity provider you trust.

Most libraries check exp automatically when you call verify, but aud and iss usually require you to pass the expected values explicitly.

Where You Store and Send the Token Matters Too#

Verification protects the token's contents, but token handling is its own attack surface. A few non-negotiables:

  • Prefer HTTP-only cookies over localStorage for session tokens, so client-side scripts (and any XSS payload) cannot read them.
  • Always use HTTPS so the token is not sniffed in transit.
  • Keep payloads minimal and non-sensitive. Because anyone can decode the payload, never store passwords, full PII, or secrets there.
  • Use short lifetimes plus refresh tokens so a leaked access token expires quickly.

A leaked token is a valid token until it expires. That is why expiry windows and storage hygiene matter as much as the signing math.

How to Decode a JWT Safely for Debugging#

You will constantly need to inspect tokens while building auth: checking what claims an identity provider issued, debugging a 401, confirming an expiry. The catch is that a JWT is a live credential. Pasting a real session token into a random website means that site's server now has a working token for your account.

Step 1: Use a decoder that runs in your browser#

Reach for a free JWT decoder that does all parsing client-side. When decoding happens entirely in the browser tab, the token is never transmitted anywhere, so even a production token stays on your machine. This is the single most important habit when handling real tokens: confirm the tool is client-side before you paste.

Step 2: Read the header, payload, and expiry#

The decoder splits the token and shows the header and payload as formatted JSON, plus a human-readable expiry so you do not have to convert a Unix timestamp by hand. Check the alg, eyeball the claims, and confirm exp is in the future. Remember you are reading, not validating, at this stage.

Step 3: Verify the signature only with a non-production secret#

If the JWT decoder and verifier lets you test a signature, only ever paste a test or development secret, never a production signing key, into any web form. Signature math is also something you can reproduce locally: an HS256 signature is just an HMAC, which you can compute with a hash generator, and the base64url pieces decode with a Base64 encoder and decoder. For a broader look at handling tokens safely, see our guide on JWT decoders and token security.

The Bottom Line#

Decoding a JWT reads the claims; verifying proves they are authentic. They are different operations with different requirements, and conflating them is the root of the none algorithm and algorithm-confusion attacks. On the server, verify every token, pin the algorithm explicitly, and check exp, aud, and iss. For debugging, decode locally so a live credential never leaves your browser.

Frequently Asked Questions#

How do I decode a JWT? Split the token on its two dots into three parts, then base64url-decode the first part (header) and second part (payload) into JSON. The third part is the signature and is not meant to be read. Decoding requires no key and only shows you the claims, it does not confirm the token is authentic.

What is the difference between decoding and verifying a JWT? Decoding reads the header and payload and needs no key, so anyone can do it. Verifying recomputes the signature with the secret or public key to confirm the token was issued by you and not altered. Decoding tells you what the claims say; only verifying tells you whether to trust them.

Is it safe to decode a JWT online? Only with a tool that decodes entirely in your browser. A JWT is a live credential, so pasting a real session token into a site that sends it to a server hands that server a working token for your account. Use a client-side decoder so the token never leaves your tab.

What is the JWT "none" algorithm attack? It exploits servers that read the signing algorithm from the token's own header. An attacker sets "alg": "none", strips the signature, and forges any payload. A verifier that honors the header accepts the unsigned token. The fix is to pin the expected algorithm on the server and reject none outright.

Can someone read the data inside my JWT? Yes. A JWT is signed, not encrypted, so the payload is only base64url-encoded and anyone holding the token can decode and read every claim. Never store passwords, secrets, or sensitive personal data in a JWT payload. The signature protects against tampering, not against reading.

How do I check if a JWT is expired? Decode the payload and read the exp claim, a Unix timestamp in seconds; if it is in the past, the token is expired. During real verification, most libraries reject expired tokens automatically when you call verify. A decoder that shows a human-readable expiry lets you confirm this quickly while debugging.

developerjwt-decodersecurityauthentication

More from Molixa

Try Molixa Tools

50+ free AI tools for content creation, SEO, coding, and more. No signup, no watermark.

Explore all tools