DD
DevDash
securityjwtjavascripttutorial

JWT Tokens Explained: How to Decode and Debug Them

JWTs are everywhere. If you've built anything with authentication in the last decade, you've used them. But most developers treat them as opaque blobs -- copy the token, paste it into a header, and hope for the best.

Let's actually understand what's inside a JWT, how to decode one, and what security pitfalls to avoid.

What is a JWT?

A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe way to represent claims between two parties. In plain terms: it's a signed JSON object that proves something -- usually that a user is authenticated and has certain permissions.

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Those three sections separated by dots are:

HEADER.PAYLOAD.SIGNATURE

Each section is Base64URL-encoded. Let's break them down.

The three parts

1. Header

The header specifies the token type and the signing algorithm.

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

Common algorithms:

  • HS256 -- HMAC with SHA-256 (symmetric, same key signs and verifies)
  • RS256 -- RSA with SHA-256 (asymmetric, private key signs, public key verifies)
  • ES256 -- ECDSA with P-256 curve (asymmetric, smaller keys than RSA)

2. Payload

The payload contains the claims -- the actual data the token carries.

{
  "sub": "1234567890",
  "name": "John Doe",
  "email": "john@example.com",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}

Standard claims (called "registered claims"):

ClaimFull nameMeaning
subSubjectWho the token is about (usually user ID)
issIssuerWho created the token
audAudienceWho the token is intended for
expExpirationWhen the token expires (Unix timestamp)
iatIssued AtWhen the token was created
nbfNot BeforeToken is invalid before this time

You can add any custom claims you want (role, email, org_id, etc.).

3. Signature

The signature ensures the token hasn't been tampered with. For HS256:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

The server that created the token can verify the signature. If anyone modifies the header or payload, the signature won't match.

Decoding a JWT manually

Since the header and payload are just Base64URL-encoded, you can decode them without any special tools.

In JavaScript (browser or Node.js)

const token = 'eyJhbGciOiJIUzI1NiIs...'; // your JWT

// Split into parts
const [header, payload, signature] = token.split('.');

// Decode header
const decodedHeader = JSON.parse(atob(header));
console.log('Header:', decodedHeader);

// Decode payload
const decodedPayload = JSON.parse(atob(payload));
console.log('Payload:', decodedPayload);

// Check expiration
const exp = new Date(decodedPayload.exp * 1000);
console.log('Expires:', exp);
console.log('Expired?', Date.now() > decodedPayload.exp * 1000);

Note: atob() handles standard Base64, but JWT uses Base64URL encoding (replacing + with - and / with _). For most tokens this doesn't matter, but for a robust solution:

function base64UrlDecode(str) {
  str = str.replace(/-/g, '+').replace(/_/g, '/');
  const pad = str.length % 4;
  if (pad) str += '='.repeat(4 - pad);
  return atob(str);
}

In Python

import base64
import json

token = "eyJhbGciOiJIUzI1NiIs..."
header, payload, signature = token.split(".")

# Add padding and decode
def decode_part(part):
    padding = 4 - len(part) % 4
    part += "=" * padding
    decoded = base64.urlsafe_b64decode(part)
    return json.loads(decoded)

print("Header:", decode_part(header))
print("Payload:", decode_part(payload))

In the command line

echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' | base64 -d
# {"alg":"HS256","typ":"JWT"}

Visual decoding

If you want a quick visual breakdown without writing code, tools like devdash.io/tools/jwt-decoder let you paste a token and see the header, payload, and expiration status color-coded. Useful when debugging auth issues and you just need to check what's inside a token fast.

Security considerations

1. JWTs are NOT encrypted

Decoding is not the same as decrypting. Anyone who has the token can read the payload. Never put sensitive data in a JWT -- no passwords, no SSNs, no credit card numbers.

If you need encrypted tokens, look into JWE (JSON Web Encryption), but for most use cases, just keep sensitive data out of the payload.

2. The "none" algorithm attack

Some JWT libraries accept "alg": "none", which means no signature verification. An attacker can modify the payload and set the algorithm to "none" to bypass verification.

Always validate the algorithm on the server side. Don't let the token tell your code which algorithm to use.

// BAD - accepts whatever algorithm the token specifies
jwt.verify(token, secret);

// GOOD - explicitly require the expected algorithm
jwt.verify(token, secret, { algorithms: ['HS256'] });

3. Algorithm confusion attacks

If your server is configured for RS256 (asymmetric) but an attacker sends a token with HS256 (symmetric), some libraries will use the public key as the HMAC secret. Since the public key is... public, the attacker can forge tokens.

Again: always specify the expected algorithm explicitly.

4. Token expiration

Always set exp claims. A JWT without an expiration is valid forever (or until you rotate your signing key). Short-lived tokens (15-60 minutes) combined with refresh tokens are standard practice.

5. Token storage

  • Don't store JWTs in localStorage -- vulnerable to XSS attacks
  • HttpOnly cookies are generally safer for web applications
  • In-memory storage works for single-page apps but tokens are lost on refresh

When NOT to use JWTs

JWTs are a good fit for stateless authentication where the server doesn't need to look up session data. But they have drawbacks:

  • You can't revoke them (without a blocklist, which defeats the stateless purpose)
  • They can get large if you add many claims
  • Clock skew between servers can cause exp validation issues

For simple web apps with a single server, traditional session cookies are often simpler and more secure.

Debugging checklist

When a JWT isn't working, check these in order:

  1. Is it expired? Decode and check the exp claim
  2. Is the audience correct? Check aud matches your service
  3. Is the signature valid? Use the correct secret/key
  4. Is the algorithm correct? Match what the server expects
  5. Is there clock skew? Check iat and nbf against your server time
  6. Is the token being sent correctly? Header should be Authorization: Bearer

JWTs aren't complicated once you understand the three parts. Decode them, inspect them, and always validate them properly on the server side.

Related Tools

Want API access + no ads? Pro coming soon.