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

Everything you need to know about JSON Web Tokens — from the three-part structure and Base64 encoding to security best practices, common vulnerabilities, and hands-on debugging techniques.

What Is a JSON Web Token (JWT)?

A JSON Web Token, universally abbreviated as JWT and pronounced "jot," is a compact, URL-safe token format defined in RFC 7519. It enables you to transmit information between two parties as a digitally signed JSON object. The signature guarantees the data has not been tampered with in transit, making JWTs the backbone of stateless authentication in modern web applications.

Unlike session-based authentication — where the server stores session data and the client carries only a session ID — JWT-based authentication is stateless. The token itself contains all the information the server needs to identify and authorize the user. No database lookup is required on each request, which makes JWTs extremely efficient at scale.

In practice, the flow works like this: the user logs in with their credentials, the server validates them and returns a signed JWT, and the client includes that token in the Authorization header of every subsequent request. The server verifies the signature, reads the claims, and processes the request — all without touching a database.

Why it matters: JWTs are used by virtually every modern API, single-page application, and mobile app for authentication. Understanding how they work is not optional for developers — it is foundational knowledge.

The Three-Part Structure of a JWT

Every JWT consists of exactly three parts, separated by dots (.): the Header, the Payload, and the Signature. Each part is Base64URL-encoded, which means it uses only URL-safe characters and can be safely transmitted in URLs, HTTP headers, and HTML forms.

Here is a real JWT you can decode right now:

A complete JWT token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3NzA3NjgwMDAsInJvbGUiOiJhZG1pbiJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Part 1 (Header):  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Part 2 (Payload): eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3NzA3NjgwMDAsInJvbGUiOiJhZG1pbiJ9
Part 3 (Signature): SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
🔑 Decode This JWT Instantly → JWT Decoder Tool

The header is a JSON object that describes how the token is signed. It typically contains two fields: alg (the signing algorithm) and typ (the token type, almost always "JWT").

Decoded Header (Base64URL → JSON)
{
  "alg": "HS256",
  "typ": "JWT"
}

The most common algorithms are:

Part 2: The Payload

The payload contains the claims — statements about the user and additional metadata. Claims are the actual data your application uses for authorization and identity.

Decoded Payload (Base64URL → JSON)
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1770768000,
  "role": "admin"
}

There are three categories of claims:

Registered claims are standardized names defined in the JWT specification. They are not mandatory but are strongly recommended for interoperability:

Public claims are custom names registered in the IANA JSON Web Token Claims registry to avoid naming collisions between different systems.

Private claims are entirely custom. These are the claims you define for your specific application — things like role, permissions, team_id, or plan. Just remember that anyone can decode the payload, so never put sensitive data here (passwords, credit card numbers, etc.).

Important: The payload is Base64URL-encoded, not encrypted. Anyone with access to the token can decode and read the claims. The signature only guarantees integrity (the data has not been modified), not confidentiality. If you need to hide the payload contents, use JWE (JSON Web Encryption) instead of JWS (JSON Web Signature).

Part 3: The Signature

The signature is what makes JWTs trustworthy. It is created by taking the encoded header and payload, concatenating them with a dot, and then signing them with the specified algorithm and a secret key (or private key for asymmetric algorithms).

How the signature is computed (HS256)
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

// With the example token:
HMACSHA256(
  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + "." +
  "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3NzA3NjgwMDAsInJvbGUiOiJhZG1pbiJ9",
  "your-256-bit-secret"
)

When the server receives a JWT, it recomputes the signature using the same algorithm and secret. If the recomputed signature matches the one in the token, the token is valid. If even a single character in the header or payload has been modified, the signature will not match and the token will be rejected.

Want to see this process in action? Use our Hash Generator to understand how HMAC-SHA256 works, then paste a complete token into the JWT Decoder to see the three parts broken down instantly.

Base64URL Encoding in JWTs

JWTs use Base64URL encoding (not standard Base64) for the header and payload. The difference is subtle but important: Base64URL replaces + with - and / with _, and it omits the trailing = padding characters. This makes the output safe for URLs, query parameters, and HTTP headers without any additional percent-encoding.

javascript — Decoding JWT parts manually
// Split the JWT into its three parts
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3NzA3NjgwMDAsInJvbGUiOiJhZG1pbiJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";

const [headerB64, payloadB64, signature] = token.split(".");

// Base64URL decode function
function base64UrlDecode(str) {
  // Replace Base64URL chars with standard Base64 chars
  let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
  // Add padding if needed
  while (base64.length % 4 !== 0) base64 += "=";
  return JSON.parse(atob(base64));
}

const header = base64UrlDecode(headerB64);
const payload = base64UrlDecode(payloadB64);

console.log("Header:", header);
// { alg: "HS256", typ: "JWT" }

console.log("Payload:", payload);
// { sub: "1234567890", name: "John Doe", iat: 1516239022, exp: 1770768000, role: "admin" }

// Check if token is expired
const isExpired = payload.exp * 1000 < Date.now();
console.log("Expired:", isExpired);

If you work with Base64 encoding regularly, our Base64 Encoder/Decoder lets you encode and decode strings instantly. This is especially handy when debugging tokens where the payload appears garbled or you suspect encoding issues.

🔄 Base64 Encoder/Decoder → Free Tool

How to Debug JWT Tokens in Practice

Debugging JWT issues is one of the most common tasks in web development. When authentication fails, the token is usually the first suspect. Here is a systematic approach to diagnosing JWT problems.

Step 1: Decode the Token

Start by decoding the token to inspect its contents. You can do this in your browser console, in your terminal, or with an online tool. Our JWT Decoder is purpose-built for this — paste the token and instantly see the header, payload, and expiration status.

Step 2: Check Expiration

The most common JWT issue is an expired token. Look at the exp claim and compare it to the current Unix timestamp. Remember that exp is in seconds (not milliseconds) since the Unix epoch (January 1, 1970).

bash — Quick expiration check in terminal
# Decode the payload (middle part) of a JWT
echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3NzA3NjgwMDAsInJvbGUiOiJhZG1pbiJ9" | \
  tr '_-' '/+' | base64 -d 2>/dev/null | python3 -m json.tool

# Check if a timestamp is expired
python3 -c "
import time
exp = 1770768000
now = int(time.time())
print(f'Expires: {time.ctime(exp)}')
print(f'Now:     {time.ctime(now)}')
print(f'Expired: {now > exp}')
print(f'Remaining: {(exp - now) // 3600} hours')
"

Step 3: Verify the Issuer and Audience

If the token is not expired, check that iss and aud match what your server expects. A token issued by auth.staging.myapp.com will be rejected by a server that only accepts tokens from auth.myapp.com. This is a frequent source of bugs when switching between environments.

Step 4: Verify the Signature

If the claims look correct, the issue might be a signature mismatch. This happens when the signing secret has been rotated, the token was created by a different service, or (in asymmetric setups) the wrong public key is being used for verification. Our JWT Decoder highlights signature verification status so you can quickly identify this class of issue.

JWT Security Best Practices

JWTs are secure when implemented correctly. The problem is that "correctly" involves several non-obvious decisions. Here are the practices that matter most.

Where to Store Tokens

This is the single most debated topic in JWT security, and the answer is unambiguous: use HttpOnly cookies.

Token Expiration Strategy

Short-lived access tokens combined with longer-lived refresh tokens is the standard pattern:

javascript — Token refresh flow
async function fetchWithAuth(url, options = {}) {
  let accessToken = getAccessToken(); // From memory

  // Try the request with the current access token
  let response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      "Authorization": `Bearer ${accessToken}`
    }
  });

  // If 401 Unauthorized, try refreshing the token
  if (response.status === 401) {
    const refreshResponse = await fetch("/api/auth/refresh", {
      method: "POST",
      credentials: "include" // Sends the HttpOnly refresh token cookie
    });

    if (refreshResponse.ok) {
      const { access_token } = await refreshResponse.json();
      setAccessToken(access_token); // Store in memory

      // Retry the original request with the new token
      response = await fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          "Authorization": `Bearer ${access_token}`
        }
      });
    } else {
      // Refresh failed — redirect to login
      window.location.href = "/login";
    }
  }

  return response;
}

Additional Best Practices

🛠 Generate Test JWT Tokens → JWT Generator Tool

Common JWT Vulnerabilities

Understanding how JWTs can be attacked is just as important as knowing how to use them correctly. Here are the vulnerabilities that have caused real-world breaches.

1. The "none" Algorithm Attack

This is the most infamous JWT vulnerability. The JWT specification technically allows "alg": "none", which means "no signature." If a server's JWT library naively trusts the algorithm specified in the header, an attacker can:

  1. Decode the payload of a valid token
  2. Modify the claims (e.g., change "role": "user" to "role": "admin")
  3. Set the header algorithm to "alg": "none"
  4. Remove the signature entirely
  5. Send the forged token, which the server accepts as valid
A forged "none" algorithm token
// Original token (signed with HS256):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InVzZXIifQ.validSignature

// Attacker's forged token (no signature):
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6ImFkbWluIn0.

// Decoded header: { "alg": "none", "typ": "JWT" }
// Decoded payload: { "sub": "1234567890", "role": "admin" }  <-- elevated to admin!

Prevention: Always explicitly whitelist the algorithms your server accepts. Never let the token header dictate which algorithm to use for verification.

javascript — Secure verification with explicit algorithm
import jwt from "jsonwebtoken";

// WRONG: Trusts the algorithm from the token header
const decoded = jwt.verify(token, secret);

// CORRECT: Explicitly specifies allowed algorithms
const decoded = jwt.verify(token, secret, {
  algorithms: ["HS256"],       // Only allow HS256
  issuer: "https://auth.myapp.com",
  audience: "https://api.myapp.com"
});

2. Token Exposure in URLs

Passing JWTs as URL query parameters is a common anti-pattern:

https://api.example.com/data?token=eyJhbGciOiJIUzI1NiIs...

This is dangerous because URLs are logged in server access logs, browser history, proxy logs, and analytics tools. Anyone with access to these logs gains access to valid authentication tokens. Additionally, the Referer header will leak the full URL (including the token) to any external resource loaded on the page. Always send tokens in the Authorization header or in HttpOnly cookies.

3. Long-Lived Tokens Without Revocation

A JWT with a 30-day expiration and no revocation mechanism is essentially an irrevocable 30-day key to your system. If the token is stolen, the attacker has a full month of access. The solution is to use short-lived access tokens (5-15 minutes) paired with refresh tokens that can be revoked individually.

4. Algorithm Confusion (RS256 vs HS256)

In systems using RS256, the server verifies tokens with the public key. An attacker can change the header to "alg": "HS256" and sign the token using the public key as the HMAC secret. If the server does not enforce the expected algorithm, it will verify the forged token using the public key as an HMAC secret, and the verification will succeed. This is another reason to always whitelist algorithms explicitly.

5. Sensitive Data in the Payload

Because the JWT payload is only encoded (not encrypted), any data in the payload is readable by anyone who intercepts the token. Never include passwords, social security numbers, credit card numbers, or other sensitive data in JWT claims. If you need the payload to be confidential, use JWE (JSON Web Encryption).

Security tip: Run your JWT implementation through a checklist: Are algorithms explicitly whitelisted? Are tokens short-lived? Is there a revocation mechanism? Are tokens stored in HttpOnly cookies? Is the payload free of sensitive data? If the answer to any of these is "no," you have work to do.

JWT vs. Session Tokens vs. OAuth

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

For most modern APIs and SPAs, JWTs with refresh token rotation strike the best balance of performance, scalability, and security.

Practical JWT Implementation

Here is a complete, production-ready JWT authentication flow in Node.js with Express. This example implements all the best practices discussed above: short-lived access tokens, refresh token rotation, HttpOnly cookies, and explicit algorithm verification.

javascript — Complete JWT auth middleware (Node.js + Express)
import jwt from "jsonwebtoken";
import crypto from "crypto";

const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;  // 256-bit random key
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET; // Different 256-bit key
const ACCESS_EXPIRY = "15m";    // 15 minutes
const REFRESH_EXPIRY = "7d";    // 7 days

// Store for revoked refresh tokens (use Redis in production)
const revokedTokens = new Set();

// Generate token pair
function generateTokens(user) {
  const accessToken = jwt.sign(
    { sub: user.id, role: user.role },
    ACCESS_SECRET,
    {
      algorithm: "HS256",
      expiresIn: ACCESS_EXPIRY,
      issuer: "https://auth.myapp.com",
      audience: "https://api.myapp.com"
    }
  );

  const refreshToken = jwt.sign(
    { sub: user.id, jti: crypto.randomUUID() },
    REFRESH_SECRET,
    { algorithm: "HS256", expiresIn: REFRESH_EXPIRY }
  );

  return { accessToken, refreshToken };
}

// Auth middleware — verifies access token from Authorization header
function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Missing or malformed token" });
  }

  const token = authHeader.slice(7);

  try {
    const payload = jwt.verify(token, ACCESS_SECRET, {
      algorithms: ["HS256"],   // Explicit algorithm whitelist
      issuer: "https://auth.myapp.com",
      audience: "https://api.myapp.com"
    });

    req.user = { id: payload.sub, role: payload.role };
    next();
  } catch (err) {
    if (err.name === "TokenExpiredError") {
      return res.status(401).json({ error: "Token expired" });
    }
    return res.status(401).json({ error: "Invalid token" });
  }
}

// POST /api/auth/login
app.post("/api/auth/login", async (req, res) => {
  const user = await validateCredentials(req.body);
  if (!user) return res.status(401).json({ error: "Invalid credentials" });

  const { accessToken, refreshToken } = generateTokens(user);

  // Set refresh token as HttpOnly cookie
  res.cookie("refresh_token", refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: "strict",
    maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
    path: "/api/auth/refresh"          // Only sent to refresh endpoint
  });

  // Return access token in response body (stored in memory by client)
  res.json({ access_token: accessToken, expires_in: 900 });
});

// POST /api/auth/refresh — Refresh token rotation
app.post("/api/auth/refresh", (req, res) => {
  const oldRefreshToken = req.cookies.refresh_token;
  if (!oldRefreshToken) return res.status(401).json({ error: "No refresh token" });

  try {
    const payload = jwt.verify(oldRefreshToken, REFRESH_SECRET, {
      algorithms: ["HS256"]
    });

    // Check if this token has been revoked
    if (revokedTokens.has(payload.jti)) {
      return res.status(401).json({ error: "Token revoked" });
    }

    // Revoke the old refresh token (single-use)
    revokedTokens.add(payload.jti);

    // Issue new token pair
    const user = { id: payload.sub, role: payload.role };
    const { accessToken, refreshToken } = generateTokens(user);

    res.cookie("refresh_token", refreshToken, {
      httpOnly: true, secure: true, sameSite: "strict",
      maxAge: 7 * 24 * 60 * 60 * 1000, path: "/api/auth/refresh"
    });

    res.json({ access_token: accessToken, expires_in: 900 });
  } catch {
    return res.status(401).json({ error: "Invalid refresh token" });
  }
});

Decode Any JWT Instantly

Paste any JWT token and see the decoded header, payload, and expiration status in real time. No signup, no installation, completely free.

Open JWT Decoder

Essential Tools for Working with JWTs

Whether you are building an authentication system, debugging a failing API call, or auditing your security posture, these tools will save you significant time:

🔑 JWT Decoder — Decode Tokens Instantly →

Frequently Asked Questions

A JSON Web Token (JWT) is a compact, URL-safe token format used for securely transmitting information between parties as a JSON object. It consists of three Base64URL-encoded parts separated by dots: a Header (specifying the algorithm and token type), a Payload (containing claims like user ID, expiration time, and roles), and a Signature (a cryptographic hash that verifies the token has not been tampered with). The server creates the token upon login, the client stores it, and sends it with every subsequent request for stateless authentication.

To decode a JWT token, split it at the two dot characters to get three parts: Header, Payload, and Signature. The Header and Payload are Base64URL-encoded JSON strings — decode them using a Base64 decoder to see the JSON contents. The Signature cannot be decoded; it must be verified using the secret key or public key. You can use free online tools like NexTool's JWT Decoder to instantly decode and inspect any JWT token without writing code.

The safest place to store JWT tokens on the client side is in an HttpOnly, Secure, SameSite cookie. This prevents JavaScript from accessing the token, which protects against XSS attacks. Avoid storing JWTs in localStorage or sessionStorage because any JavaScript running on the page (including third-party scripts or injected code) can read them. If you must use localStorage, keep token lifetimes very short (under 15 minutes) and implement refresh token rotation with an HttpOnly cookie for the refresh token.

The "none" algorithm attack is a well-known JWT vulnerability where an attacker modifies the token header to set the algorithm to "none," effectively removing signature verification. If the server's JWT library accepts the "none" algorithm, it will treat the token as valid without checking any signature, allowing the attacker to forge tokens with arbitrary claims (like elevating their role to admin). To prevent this, always explicitly specify the allowed algorithms when verifying tokens and reject any token with alg set to "none."

HS256 (HMAC + SHA-256) is a symmetric algorithm that uses the same secret key for both signing and verifying tokens. It is simple and fast but requires sharing the secret with every service that needs to verify tokens. RS256 (RSA + SHA-256) is an asymmetric algorithm that uses a private key for signing and a separate public key for verification. RS256 is preferred in distributed systems and microservices architectures because only the authentication server needs the private key, while any service can verify tokens using the publicly available public key.

NexTool Pro — Unlimited Access to All Tools

Get advanced features, no usage limits, and priority access to new tools. One-time payment, lifetime access.

Get Pro — $29