DEV Community

Michael Lip
Michael Lip

Posted on • Originally published at zovo.one

Debugging JWTs: How to Read, Validate, and Stop Blindly Trusting Tokens

The first time I had to debug a JWT authentication issue, I stared at the token string for ten minutes before realizing I could just decode it and read the contents. JWTs are not encrypted by default. They are signed, which means anyone can read them, but only the holder of the secret key can create valid ones. That distinction is the single most important thing to understand about JWTs, and getting it wrong leads to real security vulnerabilities.

The three parts

A JWT is three Base64URL-encoded strings separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1pY2hhZWwiLCJpYXQiOjE3MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Enter fullscreen mode Exit fullscreen mode

Header (first part): Contains the signing algorithm and token type.

{
  "alg": "HS256",
  "typ": "JWT"
}
Enter fullscreen mode Exit fullscreen mode

Payload (second part): Contains the claims -- the actual data.

{
  "sub": "1234567890",
  "name": "Michael",
  "iat": 1716239022
}
Enter fullscreen mode Exit fullscreen mode

Signature (third part): The cryptographic signature that proves the header and payload have not been tampered with.

You can decode the header and payload with a single line of JavaScript:

JSON.parse(atob("eyJzdWIiOiIxMjM0NTY3ODkwIn0".replace(/-/g, "+").replace(/_/g, "/")));
Enter fullscreen mode Exit fullscreen mode

The Base64URL-to-standard-Base64 conversion (replacing - with + and _ with /) is necessary because JWTs use the URL-safe variant.

HS256 vs RS256: know which one you are using

HS256 (HMAC + SHA-256) uses a shared secret. The same key signs and verifies the token. This means your API server and any service that needs to verify tokens must all know the secret. If any one of them is compromised, an attacker can forge tokens.

RS256 (RSA + SHA-256) uses a public/private key pair. The private key signs tokens. The public key verifies them. Services that need to verify tokens only need the public key, which is safe to distribute. This is why RS256 is the standard for distributed systems, OAuth providers, and anything where multiple services need to verify tokens independently.

// Verifying RS256 in Node.js
const jwt = require("jsonwebtoken");
const publicKey = fs.readFileSync("public.pem");
const decoded = jwt.verify(token, publicKey, { algorithms: ["RS256"] });
Enter fullscreen mode Exit fullscreen mode

Always specify the algorithms parameter explicitly. If you do not, some libraries will accept any algorithm the token's header claims to use, which opens the door to the algorithm confusion attack.

The algorithm confusion attack

This is the most dangerous JWT vulnerability and it is surprisingly easy to exploit. Here is how it works:

  1. Your server uses RS256 and verifies tokens with the public key.
  2. An attacker takes your public key (which is, by definition, public).
  3. The attacker creates a new token with "alg": "HS256" in the header.
  4. The attacker signs this token using the public key as the HMAC secret.
  5. Your server sees HS256, uses the "secret" (which is actually the public key) to verify, and the signature passes.

The fix is to never let the token tell your server which algorithm to use. Always enforce the expected algorithm on the server side.

Claims you should always validate

exp (expiration): If you do not check this, tokens live forever. Always validate that exp is in the future before trusting a token.

iss (issuer): Verify that the token was issued by your auth server, not by some other service that happens to use compatible keys.

aud (audience): Verify that the token was intended for your service. A token issued for Service A should not be accepted by Service B, even if they share the same auth provider.

iat (issued at): Useful for rejecting tokens that were issued before a password change or a security event.

const decoded = jwt.verify(token, secret, {
  algorithms: ["HS256"],
  issuer: "https://auth.example.com",
  audience: "https://api.example.com",
  clockTolerance: 30, // seconds of leeway for clock skew
});
Enter fullscreen mode Exit fullscreen mode

Common mistakes

Storing sensitive data in the payload. JWTs are not encrypted by default. Do not put passwords, credit card numbers, or personal data in them. Anyone who intercepts the token can decode the payload. If you need encrypted tokens, use JWE (JSON Web Encryption), but consider whether you actually need the data in the token at all.

Not setting expiration times. A JWT without an exp claim is valid forever. Use short-lived access tokens (15 minutes is common) and longer-lived refresh tokens. This limits the damage window if a token is stolen.

Storing JWTs in localStorage. This makes them accessible to any JavaScript running on the page, including XSS payloads. HttpOnly cookies are more secure for web applications because JavaScript cannot read them.

Ignoring token size. Every claim you add to the payload increases the token size. JWTs are sent with every request, typically in the Authorization header. A 4KB token on every API call adds up. Keep the payload minimal -- user ID and roles are usually sufficient.

Decoding in the terminal

For quick inspection during debugging:

# Decode the payload (second part)
echo "eyJzdWIiOiIxMjM0NTY3ODkwIn0" | base64 -d 2>/dev/null | jq .

# Decode both parts of a full JWT
echo "your.jwt.here" | cut -d. -f2 | base64 -d 2>/dev/null | jq .
Enter fullscreen mode Exit fullscreen mode

For a faster feedback loop when debugging token issues -- checking expiration times, reading claims, comparing tokens from different environments -- I keep a JWT decoder bookmarked at zovo.one/free-tools/jwt-decoder. It splits the token into its three parts and decodes each one instantly.

JWTs are simple in concept but dangerous in the details. Decode them. Validate every claim. Pin the algorithm. And never confuse signed with encrypted.


I'm Michael Lip. I build free developer tools at zovo.one. 350+ tools, all private, all free.

Top comments (0)