Skip to main content
← All Articles Β· Β· 10 min read

JWT Tokens Explained: How to Decode and Debug Them

Understand JSON Web Tokens from header to signature. Learn how to decode JWTs, read their claims, debug authentication issues, and use free online JWT tools.

jwtauthenticationsecuritydeveloper-toolsapi

JWTs (JSON Web Tokens) are everywhere in modern web development. They’re the standard format for authentication tokens in REST APIs, OAuth flows, session management, and microservice communication. But they look like impenetrable strings of random characters β€” until you know how to read them.

This guide explains the JWT format from scratch: what each part means, how to decode one, how to verify it, how to debug common authentication errors, and what security mistakes to avoid.


What Is a JWT?

A JWT is a compact, self-contained token that encodes a set of claims as a JSON object. It’s digitally signed, so the recipient can verify that the token came from a trusted source and hasn’t been tampered with.

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

It’s three Base64URL-encoded sections separated by dots:

[header].[payload].[signature]

JWT Structure: The Three Parts

Part 1: Header

The header describes the token type and the signing algorithm.

Decode the first part:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Result:

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

Common alg values:

AlgorithmTypeNotes
HS256HMAC-SHA256Symmetric β€” same key to sign and verify
HS384HMAC-SHA384Symmetric
HS512HMAC-SHA512Symmetric
RS256RSA-SHA256Asymmetric β€” private key signs, public key verifies
RS512RSA-SHA512Asymmetric
ES256ECDSA-SHA256Asymmetric, smaller signatures than RSA
noneNo algorithm⚠️ Extremely dangerous β€” never accept in production

Part 2: Payload

The payload contains the claims β€” the actual data in the token.

Decode the second part:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNTE2MjM5MDIyfQ

Result:

{
  "sub": "1234567890",
  "name": "Alice",
  "iat": 1516239022
}

Registered Claims (standardized, short names to save space):

ClaimNameMeaning
issIssuerWho issued the token
subSubjectWho the token is about (usually user ID)
audAudienceWho the token is intended for
expExpirationUnix timestamp when token expires
nbfNot BeforeToken not valid before this timestamp
iatIssued AtWhen the token was created
jtiJWT IDUnique identifier for this token

Custom Claims: You can add any additional data:

{
  "sub": "user-123",
  "role": "admin",
  "permissions": ["read", "write"],
  "tenant": "acme-corp",
  "exp": 1735689600
}

Part 3: Signature

The signature verifies that the token hasn’t been tampered with.

For HS256, the signature is calculated as:

HMAC-SHA256(
  base64url(header) + "." + base64url(payload),
  secret
)

For RS256, it’s calculated using a private key and can be verified using the corresponding public key.

Important: The signature verifies integrity, not confidentiality. The header and payload are just Base64URL encoded β€” not encrypted. Anyone can read the payload without the secret.


How to Decode a JWT Online

Use the JWT Decoder on DevPlaybook to instantly decode any JWT token.

  1. Paste the JWT string
  2. The tool splits it into header, payload, and signature
  3. Decodes each part from Base64URL
  4. Shows formatted JSON for header and payload
  5. Checks expiration (exp) against the current time

This is invaluable for debugging β€” when an authentication error occurs, the first step is always to decode the token and check what’s actually in it.


Decoding JWTs in Code

JavaScript (Browser & Node.js)

// Manual decode (no library needed for reading claims)
function decodeJWT(token) {
  const parts = token.split('.');
  if (parts.length !== 3) throw new Error('Invalid JWT');

  const decode = (str) => {
    // Add padding if needed
    const padded = str + '=='.slice(0, (4 - str.length % 4) % 4);
    return JSON.parse(atob(padded.replace(/-/g, '+').replace(/_/g, '/')));
  };

  return {
    header: decode(parts[0]),
    payload: decode(parts[1]),
    signature: parts[2]
  };
}

const { header, payload } = decodeJWT(token);
console.log(payload.sub); // user ID
console.log(new Date(payload.exp * 1000)); // expiration date

For verification (requires a library):

// Using jsonwebtoken (Node.js)
import jwt from 'jsonwebtoken';

const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded.sub);

// For RS256 with public key
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });

Python

import base64
import json

def decode_jwt(token):
    parts = token.split('.')

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

    return {
        'header': decode_part(parts[0]),
        'payload': decode_part(parts[1]),
    }

# For verification, use PyJWT
import jwt

payload = jwt.decode(token, secret, algorithms=['HS256'])
print(payload['sub'])

Go

import (
    "github.com/golang-jwt/jwt/v5"
)

// Parse and verify
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
        return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
    }
    return []byte(secretKey), nil
})

if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
    fmt.Println(claims["sub"])
}

JWT Authentication Flow

Understanding the full authentication flow helps you debug issues:

Client                          Server
  β”‚                               β”‚
  β”œβ”€β”€β”€ POST /login ──────────────►│
  β”‚    {email, password}          β”‚
  β”‚                               β”‚ Verify credentials
  │◄── 200 OK ────────────────────
  β”‚    {token: "eyJ..."}          β”‚ Create JWT, sign with secret
  β”‚                               β”‚
  β”œβ”€β”€β”€ GET /api/data ────────────►│
  β”‚    Authorization: Bearer eyJ..β”‚
  β”‚                               β”‚ Verify signature
  β”‚                               β”‚ Check exp, iss, aud
  │◄── 200 OK ────────────────────
  β”‚    {data: ...}                β”‚

The JWT is:

  1. Issued by the auth server (or the same server)
  2. Stored client-side (in memory, localStorage, or a cookie)
  3. Sent with every subsequent request in the Authorization: Bearer header
  4. Verified by the server on every request

Debugging JWT Authentication Errors

When JWT authentication fails, here’s a systematic debugging process:

Step 1: Decode the token

Paste the token into the JWT Decoder. Check:

  • Is the payload what you expect?
  • Is the exp in the future?
  • Is the iss (issuer) correct?
  • Is the aud (audience) correct?

Step 2: Check expiration

The exp claim is a Unix timestamp. If it’s in the past, the token is expired.

const isExpired = payload.exp < Date.now() / 1000;

If tokens are expiring too quickly, check:

  • The token TTL setting on your auth server
  • Whether the server’s clock is in sync with the client’s clock

Step 3: Verify the signature

If the signature verification fails:

  • Are you using the correct secret/key?
  • Is the algorithm in the header matching what you expect?
  • Did the token get modified in transit?

Step 4: Check the authorization header format

The standard format is:

Authorization: Bearer eyJhbGci...

Not:

Authorization: eyJhbGci...      ← missing "Bearer "
Authorization: bearer eyJhbGci... ← lowercase may fail in some parsers
Authorization: JWT eyJhbGci...  ← wrong prefix

If your token is in a cookie:

  • Is the SameSite attribute correct?
  • Is the Secure flag set? (required for HTTPS-only)
  • Is the HttpOnly flag set? (prevents JavaScript access)
  • Is the domain correct for cross-origin requests?

Common JWT Security Mistakes

Accepting alg: none

The none algorithm means β€œno signature.” Some early JWT libraries accepted it, allowing attackers to forge tokens by:

  1. Decoding an existing token
  2. Modifying the payload
  3. Setting alg: none and removing the signature

Fix: Explicitly specify which algorithms you accept. Never accept none.

// Correct β€” explicitly specify algorithm
jwt.verify(token, secret, { algorithms: ['HS256'] });

Storing JWTs in localStorage

localStorage is accessible to any JavaScript on the page. If your site has an XSS vulnerability, an attacker can read the JWT.

Fix: Use HttpOnly cookies. These cannot be accessed via JavaScript.

Not Validating Claims

Just because a JWT has a valid signature doesn’t mean you should trust it. You must verify:

  • exp β€” token not expired
  • iss β€” token from expected issuer
  • aud β€” token intended for your service

Using Weak Secrets for HS256

HS256 with a short or guessable secret can be brute-forced. Weak secrets exposed in public code allow attackers to generate arbitrary valid tokens.

Fix: Use secrets of at least 32 random bytes. Or switch to RS256 (asymmetric) where the secret is never shared.

Logging JWT Tokens

JWT tokens are credentials. Logging them exposes them in your log management system.

Fix: Redact tokens in logs. Log token IDs (jti) instead of full tokens.

Not Implementing Token Revocation

JWTs are stateless β€” once issued, they’re valid until they expire. If a user logs out or their account is compromised, you can’t easily invalidate the token.

Approaches:

  • Short expiration + refresh tokens
  • Token blocklist (redis-based deny list for jti values)
  • Reference tokens (opaque tokens that look up a database record)

JWT vs. Opaque Tokens

PropertyJWTOpaque Token
Self-containedYes β€” data encoded in tokenNo β€” data in database
Stateless verificationYesNo β€” requires DB lookup
RevocableDifficultEasy
SizeGrows with claimsFixed small size
InspectionDecodable without secretRequires DB lookup

JWTs are great for stateless APIs at scale. Opaque tokens are simpler when you need easy revocation.


Quick Reference: JWT Tools

TaskTool
Decode a JWT and inspect claimsJWT Decoder
Build a JWT from scratchJWT Builder
Decode Base64URL manuallyBase64 Encoder/Decoder
Test API endpoints with JWT authAPI Tester

Summary

JWT tokens are Base64URL-encoded JSON, split into three parts: header (algorithm), payload (claims), and signature (verification).

Key takeaways:

  • The payload is encoded, not encrypted β€” don’t put secrets in JWTs
  • Always verify the signature server-side before trusting claims
  • Check exp, iss, and aud claims explicitly
  • Never accept alg: none in production
  • Use the JWT Decoder to inspect tokens when debugging auth issues

When authentication breaks, the fastest path to understanding why is decoding the token and reading its actual claims β€” not guessing what the server might be checking.

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.