JWT Tokens Explained: How to Decode, Verify and Debug

By Taylor Feb 22, 2026 12 min read

JSON Web Tokens (JWTs) are the backbone of modern web authentication. If you have ever logged into a web application, used an API, or implemented OAuth, you have almost certainly encountered JWTs. Despite their ubiquity, they are frequently misunderstood, misconfigured, and misused -- often with serious security consequences.

This guide covers everything you need to know: what JWTs are, how they work internally, how to decode and inspect them, and the security pitfalls that catch even experienced developers.

Table of Contents
  1. What Is a JWT?
  2. The Three Parts: Header, Payload, Signature
  3. The Header Explained
  4. The Payload and Claims
  5. The Signature and Verification
  6. Decoding a JWT Step by Step
  7. Standard JWT Claims Reference
  8. JWT Authentication Flow
  9. Security Best Practices
  10. Common Issues and Debugging
  11. JWKs and Key Rotation
  12. JWT vs. Sessions vs. Opaque Tokens

1. What Is a JWT?

A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe token format defined in RFC 7519. It allows two parties to exchange claims -- verifiable statements about a user or entity -- as a self-contained JSON object that is digitally signed.

The critical property of a JWT is that it can be verified without contacting the server that issued it. The recipient can validate the signature using a shared secret (HMAC) or a public key (RSA/ECDSA), which means JWT-based systems scale better than session-based systems because the authentication check requires no database lookup.

A JWT looks like this in practice -- three Base64url-encoded strings separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Each colored section serves a different purpose: the header describes the algorithm, the payload contains the claims, and the signature proves the token has not been tampered with.

2. The Three Parts: Header, Payload, Signature

Every JWT consists of exactly three parts, separated by periods. Each part is Base64url-encoded (a URL-safe variant of Base64 that replaces + with - and / with _, and omits padding =).

HEADER.PAYLOAD.SIGNATURE

Each part:
  1. Base64url( JSON header )
  2. Base64url( JSON payload )
  3. Base64url( HMAC-SHA256( header + "." + payload, secret ) )

This structure means you can always decode the header and payload without any secret or key. The signature, however, can only be created by someone who knows the secret, and can only be verified by someone who knows the secret (HMAC) or has the public key (RSA/ECDSA).

The header is a JSON object that specifies two things: the signing algorithm and the token type.

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

The alg field is the most important. It tells the verifier which algorithm to use when checking the signature. Common algorithms include:

Security Warning: Never trust the alg field from an untrusted token. A classic attack involves changing alg to "none" to bypass signature verification. Always enforce the expected algorithm on the server side.

4. The Payload and Claims

The payload contains the claims -- statements about the user and metadata about the token. Claims come in three types: registered (standard), public (shared), and private (custom).

Typical JWT payload
{
  "sub": "1234567890",
  "name": "John Doe",
  "email": "john@example.com",
  "role": "admin",
  "iat": 1708617600,
  "exp": 1708621200,
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com"
}

The registered claims (sub, iat, exp, iss, aud) are defined in RFC 7519 and have specific meanings. The custom claims (name, email, role) can be anything your application needs.

Key Insight: The payload is only Base64url-encoded, NOT encrypted. Anyone who intercepts the token can read all claims. Never put sensitive data (passwords, credit card numbers, secrets) in the payload unless you are using JWE (JSON Web Encryption).

5. The Signature and Verification

The signature is what makes JWTs trustworthy. It proves that the header and payload have not been modified since the token was issued.

How the signature is computed (HS256)
signature = HMAC-SHA256(
  base64url(header) + "." + base64url(payload),
  secret_key
)

Final token = base64url(header) + "." + base64url(payload) + "." + base64url(signature)

When a server receives a JWT, it recalculates the signature using the same algorithm and key. If the recalculated signature matches the one in the token, the token is valid. If even a single character of the header or payload was changed, the signatures will not match and the token is rejected.

Verification in Node.js
const jwt = require('jsonwebtoken');

try {
  const decoded = jwt.verify(token, 'your-secret-key', {
    algorithms: ['HS256'],  // ALWAYS specify expected algorithm
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com'
  });
  console.log(decoded);
} catch (err) {
  console.error('Token invalid:', err.message);
}
Verification in Python
import jwt

try:
    decoded = jwt.decode(
        token,
        'your-secret-key',
        algorithms=['HS256'],  # ALWAYS specify expected algorithm
        issuer='https://auth.example.com',
        audience='https://api.example.com'
    )
    print(decoded)
except jwt.InvalidTokenError as e:
    print(f'Token invalid: {e}')

Decode Your JWT Instantly

Paste any JWT into JWT Forge to see the decoded header, payload, and signature. Color-coded display with automatic expiry detection.

Open JWT Forge

6. Decoding a JWT Step by Step

Let us decode a real JWT manually to understand what happens under the hood.

Step 1: Split by dots
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Part 1 (Header):    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Part 2 (Payload):   eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
Part 3 (Signature): SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Step 2: Base64url-decode the header
atob("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")

Result: {"alg":"HS256","typ":"JWT"}
Step 3: Base64url-decode the payload
atob("eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ")

Result: {"sub":"1234567890","name":"John Doe","iat":1516239022}
Step 4: Convert timestamps to human-readable dates
iat: 1516239022  -->  January 18, 2018, 01:30:22 UTC
(Use our Timestamp Forge tool for instant conversion)

The signature (Part 3) remains as a binary blob -- you cannot decode it into meaningful JSON. It exists solely for verification purposes.

7. Standard JWT Claims Reference

These are the registered claims defined in RFC 7519. While none are required, using them correctly improves interoperability and security.

8. JWT Authentication Flow

Here is the typical flow for JWT-based authentication in a web application:

1. User sends credentials (email + password) to /api/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 (usually in memory or httpOnly cookie)
6. Client includes JWT in subsequent requests:
   Authorization: Bearer eyJhbGciOi...
7. Server verifies the JWT signature on each request
8. Server extracts claims from the payload (no DB lookup needed)
9. Server processes the request based on the claims

The beauty of this flow is step 8: the server does not need to query a sessions table. The token itself contains all the information needed to authenticate and authorize the user. This is why JWTs are called stateless authentication.

9. Security Best Practices

JWTs are not inherently secure or insecure. Security depends entirely on how you implement them. Here are the critical rules.

Always Verify the Signature

Never decode a JWT and trust the claims without verifying the signature. This is the most common and most dangerous mistake. Any attacker can craft a valid-looking payload and Base64url-encode it.

Enforce the Algorithm

Always specify the expected algorithm when verifying. Never let the token's alg header dictate which algorithm to use. The "algorithm confusion" attack exploits this by changing RS256 (asymmetric) to HS256 (symmetric) and using the public key as the HMAC secret.

Critical: The "alg": "none" attack bypasses verification entirely. If your library supports none, disable it explicitly. Most modern libraries do this by default, but always verify.

Set Short Expiration Times

Access tokens should expire in 5 to 15 minutes. Use refresh tokens (stored server-side) for longer sessions. Short-lived tokens limit the damage if a token is leaked.

Use HTTPS Only

JWTs transmitted over HTTP can be intercepted by anyone on the network. Always use HTTPS. If storing tokens in cookies, set the Secure and HttpOnly flags.

Do Not Store Sensitive Data in the Payload

The payload is only encoded, not encrypted. Use JWE if you need encrypted claims, or simply keep sensitive data on the server and include only a user ID in the token.

Validate All Claims

Check exp (expiration), iss (issuer), and aud (audience) on every verification. This prevents tokens issued by other services or expired tokens from being accepted.

10. Common Issues and Debugging

"jwt malformed"

The token does not have three dot-separated parts. Check for trailing whitespace, missing characters from copy-paste, or URL encoding artifacts.

"jwt expired"

The current time is past the exp claim. Check clock synchronization between the issuer and verifier. Use a clock tolerance (e.g., 30 seconds) to account for minor drift.

"invalid signature"

The signature does not match. Common causes: wrong secret key, wrong algorithm, token was modified in transit, or the key was rotated but the token was signed with the old key.

"invalid audience"

The aud claim does not match the expected audience. This often happens in multi-service architectures where tokens from Service A are accidentally sent to Service B.

Token too large

JWTs go in the Authorization header, and some servers have header size limits (typically 8KB). If you are putting too many claims in the payload, consider using a reference token instead.

Debug Your Token Now

Paste your problematic JWT into JWT Forge to instantly see the decoded header, payload, expiration status, and all claims in a readable format.

Open JWT Forge

11. JWKs and Key Rotation

In production systems using asymmetric algorithms (RS256, ES256), public keys are typically distributed via a JWKS (JSON Web Key Set) endpoint. This is a URL that returns the public keys used to verify tokens.

Typical JWKS endpoint response
GET https://auth.example.com/.well-known/jwks.json

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "key-2026-02",
      "use": "sig",
      "n": "0vx7agoebGcQSuu...",
      "e": "AQAB"
    }
  ]
}

The kid (Key ID) in the JWT header tells the verifier which key from the JWKS to use. This enables key rotation -- you can publish a new key, start signing with it, and keep the old key in the JWKS until all old tokens expire.

12. JWT vs. Sessions vs. Opaque Tokens

JWTs are not always the best choice. Here is when to use each approach:

Use JWTs when: You have multiple services that need to verify authentication independently, or you need the token to carry claims without a database lookup.

Use server-side sessions when: You need the ability to instantly revoke access (logout), you have a single monolithic application, or you want the simplicity of a session ID stored in a cookie.

Use opaque tokens when: You need the scalability of tokens but also need instant revocation. Opaque tokens are random strings that map to server-side state, combining the portability of tokens with the control of sessions.

The tradeoff is always between statelessness (JWT) and revocability (sessions). JWTs cannot be truly revoked before expiration without maintaining a blacklist, which partially defeats the purpose of stateless tokens.

Inspect Any JWT Token

JWT Forge decodes tokens instantly with color-coded display, automatic expiry detection, and human-readable timestamps. No signup, no data sent to any server.

Open JWT Forge