All posts

JWT decoder vs JWT verifier

Why decoding a JWT proves nothing: what signature verification checks, how alg:none and algorithm confusion attacks work, and how to handle real tokens safely.

By Last reviewed 5 min readJWTSecurity

A JWT decoder answers "what does this token say?" A verifier answers "should I believe it?" They sound like the same job, and treating them as one is among the most common JWT security bugs: code that reads the claims and acts on them without checking who signed them.

The JWT Decoder decodes tokens in your browser and does only the first job. Every decoded token carries a "Signature not verified" note in the status bar. This guide covers what the second job involves and why it can't be skipped.

Decoding needs no key at all

The header and payload of a signed JWT are Base64URL-encoded JSON. Decoding them is a text transformation, not cryptography. Your browser, a reverse proxy, a log aggregator and anyone who copies the token from a screenshot can all read the claims.

Signing prevents changes; it doesn't hide anything. If the payload must stay confidential, you need an encrypted JWT (JWE), or you keep that data out of the token. How to read JWT claims walks through a full decode.

A decoder shows whatever the token says

Take the sample token from the claims guide. It carries "roles":["support"] and is signed with HS256 using the demo secret devprix-demo-secret-do-not-use-in-prod-0001. Edit the payload JSON to say "admin", Base64URL-encode it again, and keep the original signature. Only the end of the payload segment changes:

original payload ends   …cm9sZXMiOlsic3VwcG9ydCJdfQ   →  "roles":["support"]}
edited payload ends     …cm9sZXMiOlsiYWRtaW4iXX0      →  "roles":["admin"]}
signature (unchanged)   CIMGchg31ZesoUd1r-ZBeLG4cPkVptSgf5mTiX2Kgt4

Paste the edited token into the decoder and it shows "roles": ["admin"] without complaint, because decoding never looks at the signature. Paste it into the JWT Builder's Verify token mode with the demo secret and it reports an invalid signature, since the signature still covers the old payload. A valid signature for the edited token would be Tl0iBwNK7-f_Z5u34zZSvn6IVYjEfEn_ShBdI3B0cVY, and computing that requires the secret.

Now picture server code that decodes without verifying and then trusts roles. That is an authorization bypass any user can perform with a text editor. Most libraries ship a decode-only function for debugging, such as jwt.decode() in Node's jsonwebtoken or options={"verify_signature": False} in PyJWT. Use them for logging, never for access decisions.

What verification actually checks

Verification is more than the signature. A verifier doing its job checks, in this order:

  1. The algorithm is one you configured for this key. It comes from your config, never from the token's own header.
  2. The signature over the exact bytes header.payload, using the right key: the shared secret for HS256, or the issuer's public key for RS256, chosen by kid from a key set you trust.
  3. exp and nbf against the current time, with a small leeway for clock skew.
  4. iss matches your expected issuer exactly.
  5. aud contains your service, so a token minted for a different API is rejected.
  6. Your own rules on scope, roles and the like, and only once everything above has passed.

Mature libraries handle steps 1 to 5 in one call when you give them the expected values:

// Node.js, jsonwebtoken
const claims = jwt.verify(token, publicKey, {
  algorithms: ["RS256"],
  issuer: "https://auth.example.com/",
  audience: "https://api.example.com",
  clockTolerance: 30,
});

# Python, PyJWT
claims = jwt.decode(
    token, public_key, algorithms=["RS256"],
    issuer="https://auth.example.com/",
    audience="https://api.example.com", leeway=30,
)

Check what your library does when an option is missing. jsonwebtoken, for example, only checks aud and iss when you pass audience and issuer, so a forgotten option is a silent gap rather than an error.

Two attacks on lazy verification

alg: none

RFC 7518 defines none for unsecured JWTs: the header says {"alg":"none"} and the signature is empty, so the token ends with a dot.

header     eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0   →  {"alg":"none","typ":"JWT"}
payload    eyJpc3MiOi…                            →  any claims the attacker likes
signature  (empty, so the token ends with ".")

A verifier that reads alg from the token and dispatches on it will "verify" this with no key at all. The fix is an explicit allow-list of algorithms that never contains none. The JWT Builder's Verify mode refuses such tokens outright: "This token is unsigned (alg: none) — there's no signature to verify."

Algorithm confusion

A service expects RS256 and verifies with the issuer's RSA public key. An attacker changes the header to HS256 and computes an HMAC over a forged payload, using the public key's bytes as the HMAC secret. The public key is, by design, public. If the library takes the algorithm from the header and treats the configured key as raw bytes, the forged token verifies.

The fix is the same: bind each key to one algorithm, pass the allowed algorithms explicitly, and use typed key objects so an RSA public key can't be mistaken for an HMAC secret. RFC 8725, the JWT best current practices document, covers both attacks.

What the DevPrix JWT tools do

  • JWT Decoder: splits and decodes the token with JavaScript in the page rather than sending it to a server to be decoded. It strips a Bearer prefix, shows the header, the payload and a Claims view, and reports time validity from exp and nbf. It does not verify signatures, and says so in the status bar and under the signature.
  • JWT Builder: signs tokens with HS256, HS384 or HS512 using the browser's Web Crypto API. Its Verify token mode checks an HMAC-signed token against a secret you type. It doesn't verify RS256 or ES256 tokens; for those it tells you a public key is needed, and that check belongs in your server-side library with the issuer's published keys.

Handling real tokens safely

  • A live token is a credential. Anyone holding an access token can call your API as that user until exp. When debugging, prefer expired tokens or tokens from a development environment; they decode the same way.
  • Don't paste production tokens or secrets into tools you haven't checked. You can't see whether a web tool logs what you paste on its server.
  • Watch share links. The DevPrix Share button copies a link with your input in the URL fragment, the part after # that browsers don't send to the server. That is handy for a test token, but a shared link containing a live token hands the token to whoever opens it.
  • Keep production signing keys out of browser tabs entirely. Use throwaway secrets in the JWT Builder; production keys belong in your secret manager.
  • If a live token leaks, revoke the session or refresh token behind it. Rotating the signing key and retiring the old one invalidates every token it signed, which is the bigger hammer.

Next steps

Use the JWT Decoder to read tokens and the JWT Builder to check HMAC signatures while you develop, and keep real verification in your server code. If you're choosing an algorithm for that code, HS256 vs RS256 for beginners covers the trade-offs.

Paste anything. devprix picks the tool.

Press ⌘K on any page, paste JSON, a JWT, a timestamp or a URL, and the right tool opens with your data filled in.