JWT Token Explained: How to Decode, Debug, and Secure Your Tokens

DevToolkit Team · · 15 min read

JSON Web Tokens (JWTs) are everywhere. If you've built a modern web application, integrated with an OAuth provider, or worked with any API that requires authentication, you've almost certainly encountered JWTs. Yet despite their ubiquity, JWTs are one of the most misunderstood and misused tools in web development.

In this guide, we'll break down exactly what JWTs are, how they work, how to decode and debug them, and — critically — how to use them securely. Want to inspect a token right now? Paste it into our JWT Decoder to see the header, payload, and expiration instantly.

What Is a JWT?

A JSON Web Token is a compact, URL-safe means of representing claims between two parties. It's defined in RFC 7519 and is the de facto standard for stateless authentication in web applications.

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

It's three Base64URL-encoded strings separated by dots. That's it. No magic — just encoded JSON with a cryptographic signature appended.

JWT Structure: The Three Parts

Every JWT consists of three parts: Header, Payload, and Signature.

1. Header

The header typically contains two fields:

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

This JSON is Base64URL-encoded to form the first part of the token. You can decode it with our Base64 Encoder/Decoder or directly with the JWT Decoder.

2. Payload (Claims)

The payload contains the actual data — called claims. There are three types:

Registered claims (standardized, optional but recommended):

{
  "iss": "https://auth.example.com",     // Issuer
  "sub": "user-123",                     // Subject (user ID)
  "aud": "https://api.example.com",      // Audience
  "exp": 1748505600,                     // Expiration (Unix timestamp)
  "nbf": 1748419200,                     // Not Before
  "iat": 1748419200,                     // Issued At
  "jti": "unique-token-id"              // JWT ID (for revocation)
}

Public claims (custom, registered in the IANA JWT Claims Registry):

{
  "name": "John Doe",
  "email": "[email protected]"
}

Private claims (custom, agreed between parties):

{
  "role": "admin",
  "permissions": ["read", "write", "delete"],
  "tenant_id": "acme-corp"
}

Important: the payload is not encrypted. Anyone who has the token can decode and read the payload. Never put sensitive data (passwords, API keys, PII) in a JWT payload. The signature only ensures the data hasn't been tampered with, not that it's secret.

3. Signature

The signature verifies that the token hasn't been modified. It's computed by:

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

For asymmetric algorithms (RS256, ES256), the signature is created with a private key and verified with the corresponding public key. This is essential for microservices architectures where the signing service and verifying services are separate.

How JWT Authentication Works

Here's the typical JWT authentication flow:

1. User sends credentials (email + password) to /auth/login
2. Server validates credentials against database
3. Server creates a JWT with user claims and signs it
4. Server returns the JWT to the client
5. Client stores the JWT (localStorage, cookie, or memory)
6. Client sends JWT in Authorization header: Bearer <token>
7. Server verifies JWT signature and extracts claims
8. Server processes the request using the claims (user ID, role, etc.)

The key advantage: the server doesn't need to store session data. The JWT itself contains all the information needed to authenticate the request. This makes JWTs ideal for stateless, horizontally scalable architectures.

Signing Algorithms Explained

Choosing the right algorithm is critical for security. Here's what each algorithm family does:

HMAC (HS256, HS384, HS512)

Symmetric signing — the same secret key is used to create and verify the signature. Simple and fast, but every service that needs to verify the token must have the secret key.

// Node.js example
const jwt = require('jsonwebtoken');
const token = jwt.sign({ userId: '123' }, 'your-secret-key', { expiresIn: '1h' });
const decoded = jwt.verify(token, 'your-secret-key');

Best for: monolithic applications, single-service architectures.

RSA (RS256, RS384, RS512)

Asymmetric signing — a private key creates the signature, a public key verifies it. The auth server keeps the private key; all other services only need the public key.

// Sign with private key
const token = jwt.sign({ userId: '123' }, privateKey, { algorithm: 'RS256' });

// Verify with public key (any service can do this)
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });

Best for: microservices, multi-service architectures, OAuth 2.0 providers.

ECDSA (ES256, ES384, ES512)

Asymmetric signing using elliptic curves. Same security as RSA but with smaller keys and faster operations.

Best for: mobile applications, IoT devices, performance-sensitive systems.

Decoding and Debugging JWTs

Debugging JWTs is straightforward because the header and payload are just Base64URL-encoded JSON. Here are several ways to inspect tokens:

Using DevToolkit's JWT Decoder

Our JWT Decoder provides instant decoding with:

Using the Command Line

# Decode header
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d

# Decode payload
echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0" | base64 -d

# Full decode with jq
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .

Using JavaScript

function decodeJwt(token) {
  const parts = token.split('.');
  return {
    header: JSON.parse(atob(parts[0])),
    payload: JSON.parse(atob(parts[1])),
  };
}

const { header, payload } = decodeJwt(token);
console.log('Expires:', new Date(payload.exp * 1000));

Note: atob() decodes standard Base64. JWTs use Base64URL encoding (replacing + with - and / with _). Most browsers handle this correctly, but for edge cases you may need to normalize first. Use our Base64 tool for quick encoding/decoding.

JWT Security Best Practices

JWTs are secure when used correctly, but dangerous when misused. Follow these rules:

1. Always Verify the Signature

Never trust a JWT without verifying its signature. The whole point of the signature is to prove the token hasn't been tampered with. Always specify the expected algorithm to prevent algorithm confusion attacks:

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

// BAD: accept any algorithm (vulnerable to alg:none attack)
jwt.verify(token, secret);

2. Set Short Expiration Times

JWTs can't be revoked after issuance (without a blocklist). Keep expiration times short:

Always check the exp claim. Our Timestamp Converter can help you convert Unix timestamps to human-readable dates for debugging.

3. Use Strong Secrets

For HMAC algorithms, use a randomly generated secret of at least 256 bits (32 bytes). Never use dictionary words or short strings. Generate secure random strings with our Password Generator.

4. Store Tokens Securely

// BEST: HttpOnly cookie (not accessible to JavaScript, immune to XSS)
Set-Cookie: token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600

// OK: Memory (lost on page refresh, but immune to XSS and CSRF)
let token = null; // in-memory variable

// AVOID: localStorage (accessible to any JavaScript on the page)
localStorage.setItem('token', jwt); // Vulnerable to XSS

5. Never Put Sensitive Data in the Payload

The payload is Base64-encoded, not encrypted. Anyone with the token can read it. Never include:

6. Validate All Claims

Don't just verify the signature — also validate:

JWT vs Sessions vs API Keys

When should you use JWTs vs other authentication methods?

                  JWT             Sessions         API Keys
Stateless         Yes             No               Yes
Scalable          High            Medium           High
Revocable         Hard            Easy             Easy
Size              Medium          Small (ID only)  Small
Best for          SPAs, APIs,     Traditional      Server-to-server,
                  microservices   web apps         third-party access

JWTs are ideal when you need stateless, scalable authentication — especially for SPAs and microservices. For traditional server-rendered applications with simple session needs, cookie-based sessions may be simpler and more secure.

Common JWT Vulnerabilities

Algorithm Confusion (alg:none)

Some libraries accept tokens with "alg": "none", which means no signature verification. Always whitelist allowed algorithms:

jwt.verify(token, secret, { algorithms: ['HS256'] });

Key Confusion (RS256 to HS256)

If a server expects RS256 (asymmetric) but an attacker sends HS256 (symmetric) and signs with the public key (which is publicly available), the server might accept it. Fix: always specify the expected algorithm.

Token Leakage

JWTs in URLs, logs, or error messages can be stolen. Always transmit via HTTPS, use Authorization headers (not query parameters), and sanitize logs.

Missing Expiration

A JWT without exp never expires. Always set and validate expiration. Use our JWT Decoder to quickly check if a token has an expiration claim.

Implementing JWT in Production

Node.js (Express)

const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET; // 256-bit random secret

// Issue token
app.post('/auth/login', async (req, res) => {
  const user = await authenticateUser(req.body.email, req.body.password);
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  const token = jwt.sign(
    { sub: user.id, role: user.role },
    SECRET,
    { expiresIn: '1h', issuer: 'https://api.example.com' }
  );

  res.cookie('token', token, {
    httpOnly: true, secure: true, sameSite: 'strict', maxAge: 3600000
  });
  res.json({ success: true });
});

// Verify middleware
function authMiddleware(req, res, next) {
  const token = req.cookies.token || req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token' });

  try {
    req.user = jwt.verify(token, SECRET, { algorithms: ['HS256'] });
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
}

Python (FastAPI)

from jose import jwt, JWTError
from datetime import datetime, timedelta

SECRET_KEY = os.environ["JWT_SECRET"]
ALGORITHM = "HS256"

def create_token(user_id: str, role: str) -> str:
    payload = {
        "sub": user_id,
        "role": role,
        "exp": datetime.utcnow() + timedelta(hours=1),
        "iss": "https://api.example.com",
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def verify_token(token: str) -> dict:
    try:
        return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    except JWTError:
        raise HTTPException(status_code=401, detail="Invalid token")

Refresh Token Pattern

Since access tokens should be short-lived, use refresh tokens to get new access tokens without re-authenticating:

1. User logs in → receives access token (15 min) + refresh token (30 days)
2. Access token expires → client sends refresh token to /auth/refresh
3. Server validates refresh token → issues new access token
4. Refresh token expires → user must log in again

Store refresh tokens in a database so they can be revoked. This gives you the stateless benefits of JWTs for most requests, with the ability to revoke access when needed.

FAQ

Can JWTs be decoded without the secret key?

Yes. The header and payload are Base64-encoded, not encrypted. Anyone can decode them. The secret key is only needed to verify the signature (i.e., confirm the token hasn't been tampered with).

Are JWTs encrypted?

Standard JWTs (JWS) are signed but not encrypted. If you need encrypted tokens, use JWE (JSON Web Encryption), defined in RFC 7516. However, this adds complexity — in most cases, just don't put sensitive data in the payload.

How do I revoke a JWT?

You can't revoke a JWT directly — it's valid until it expires. Workarounds: use short expiration times, maintain a token blocklist (checked on every request), or use a version counter in the user record.

Should I use JWT for session management?

It depends. For SPAs and APIs, JWTs work well. For traditional server-rendered apps, cookie-based sessions are simpler and easier to revoke. Don't use JWTs just because they're trendy — use them when stateless authentication is a genuine requirement.

What is the difference between JWS and JWE?

JWS (JSON Web Signature) is what most people mean when they say "JWT." It signs the token so you can verify it hasn't been tampered with, but the payload remains readable by anyone. JWE (JSON Web Encryption) encrypts the entire payload so only the intended recipient can read it. In practice, JWS covers the vast majority of use cases — just avoid putting secrets in the payload. If you truly need confidential claims, consider JWE or encrypt the payload before embedding it.

Can I use JWT with GraphQL APIs?

Absolutely. JWTs work with any transport layer. In a GraphQL setup, the client sends the JWT via the Authorization: Bearer <token> header just like a REST API. Your GraphQL server middleware verifies the token and attaches the decoded claims to the request context. This lets resolvers access user information (ID, role, permissions) without an extra database lookup on every query.

How do I handle JWT token size issues?

JWTs grow as you add more claims. A token with dozens of permissions or nested objects can exceed cookie size limits (typically 4 KB). To keep tokens small: store only essential claims (user ID, role), move detailed permissions to a server-side cache keyed by user ID, and avoid duplicating data already available from an API call. If your token is large, paste it into our JWT Decoder to see exactly which claims are consuming space.

JWT Alternatives: PASETO and Opaque Tokens

JWTs are the dominant standard, but they are not the only option. Two notable alternatives address specific shortcomings of the JWT specification.

PASETO (Platform-Agnostic Security Tokens)

PASETO was created as a direct response to JWT's flexibility-related security pitfalls. Where JWT lets developers choose from many algorithms (including the dangerous "alg": "none"), PASETO restricts choices to a small set of vetted, versioned protocols. Each PASETO version locks in a specific set of cryptographic primitives, eliminating algorithm confusion attacks entirely.

A PASETO token looks similar to a JWT but uses a version prefix:

v4.public.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0...

PASETO supports two purposes: local (symmetric authenticated encryption — the payload is encrypted and authenticated) and public (asymmetric signing — like JWS, the payload is readable but tamper-proof). The "local" purpose gives you encryption by default, something that requires the separate JWE spec in the JWT ecosystem.

The trade-off is ecosystem maturity. JWT libraries exist for virtually every language and framework. PASETO libraries are available for most popular languages but have a smaller community and fewer battle-tested integrations with identity providers and API gateways.

Opaque Tokens

Opaque tokens are the conceptual opposite of JWTs. Instead of encoding claims into the token itself, an opaque token is simply a random string (e.g., a UUID or a cryptographically random hex string) that maps to session data stored server-side.

// Opaque token example
Authorization: Bearer 7f3c8a2b-4d1e-9f6c-a5b0-3e8d7c2f1a4b

The server looks up this token in a database or cache (like Redis) to retrieve the associated user data. This makes revocation trivial — just delete the record. It also means no sensitive data can leak from the token, because the token contains no data at all.

The downside is that every request requires a lookup, which adds latency and creates a single point of failure (your token store). For high-throughput APIs or microservices architectures where you want to avoid per-request database calls, JWTs remain the better choice.

In many production systems, the best approach is a hybrid: use short-lived JWTs as access tokens (stateless, fast verification) and opaque strings as refresh tokens (stored server-side, easily revocable).

JWT Implementation Security Checklist

Before deploying JWTs in production, walk through this checklist. Each item addresses a real-world vulnerability or misconfiguration that has led to breaches.

Ready to inspect your own tokens? Head over to our JWT Decoder to paste and decode any JWT instantly — everything runs in your browser, so your tokens stay private.

Enjoyed this article?

Get the free Developer Cheatsheet Pack + weekly tips on tools, workflows, and productivity.

Subscribe Free

Try These Tools

Related free tools mentioned in this article

Back to Blog