JWT
8 min readMar 27, 2026

How to Debug a JWT Token Locally — Without Sending It to a Server

JWT tokens are the backbone of modern authentication — but debugging them often means pasting sensitive credentials into jwt.io or similar online tools. This guide shows you how to decode, inspect, and verify JWTs entirely in your browser, with no data sent to any third-party server.

What Is a JWT and How Is It Structured?

A JSON Web Token (JWT) consists of three Base64url-encoded parts separated by dots: header.payload.signature. Each part encodes different information.

  • Header — the token type and signing algorithm, e.g. {"alg":"HS256","typ":"JWT"}
  • Payload — the claims object containing your actual data: user ID, roles, expiration time, issuer, etc.
  • Signature — a cryptographic hash of the header + payload, created with a secret key. It proves the token has not been tampered with.

Importantly, the payload is not encrypted — it is only encoded. Anyone who has the token can read its contents. Sensitive data (passwords, payment details) should never go inside a JWT payload.

Why Pasting JWTs Into Online Debuggers Is Risky

Tools like jwt.io decode tokens server-side — your token travels across the internet before you see the result. If the token is still valid and carries sensitive claims (user roles, admin access, internal user IDs), you are effectively sharing live credentials with a third party.

A safer approach: decode locally, either in your browser console or with a tool that runs entirely client-side. The result is identical — JWT decoding is deterministic — but your token stays on your device.

Decoding a JWT in the Browser Console

You can decode any JWT payload manually using atob(), the browser's built-in Base64 decoder. Open DevTools (F12) and run:

function decodeJwt(token) {
  const parts = token.split('.');
  if (parts.length !== 3) throw new Error('Invalid JWT format');

  // Base64url → Base64 → JSON
  const pad = (s) => s + '=='.slice(s.length % 4);
  const decode = (s) => JSON.parse(atob(pad(s.replace(/-/g, '+').replace(/_/g, '/'))));

  return {
    header:  decode(parts[0]),
    payload: decode(parts[1]),
    // Note: signature bytes are not human-readable
  };
}

const result = decodeJwt('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
console.log(result.payload);

This reads the payload without verifying the signature — useful for debugging, but never use this output to make authorization decisions on the server.

Checking Token Expiration

The exp claim is a Unix timestamp in seconds (not milliseconds). To check expiration:

const { payload } = decodeJwt(token);
const nowSeconds = Math.floor(Date.now() / 1000);

if (nowSeconds > payload.exp) {
  console.log('Token is EXPIRED');
  console.log('Expired', nowSeconds - payload.exp, 'seconds ago');
} else {
  console.log('Token is valid');
  console.log('Expires in', payload.exp - nowSeconds, 'seconds');
}

Standard JWT Claims Explained

The JWT specification (RFC 7519) defines these standard registered claims:

ClaimFull NameMeaning
subSubjectUser ID or entity identifier
issIssuerWho created the token
audAudienceWho the token is intended for
expExpirationUnix timestamp after which it is invalid
iatIssued AtWhen the token was created
nbfNot BeforeToken is invalid before this time
jtiJWT IDUnique identifier for token revocation

Verifying the Signature Locally (HS256)

If you have the signing secret, you can verify the signature using the browser's built-in Web Crypto API — no libraries, no server calls:

async function verifyHS256(token, secret) {
  const encoder = new TextEncoder();
  const [header, payload, sig] = token.split('.');

  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['verify']
  );

  // Base64url decode the signature
  const pad = (s) => s + '=='.slice(s.length % 4);
  const sigBytes = Uint8Array.from(
    atob(pad(sig.replace(/-/g, '+').replace(/_/g, '/'))),
    (c) => c.charCodeAt(0)
  );

  const data = encoder.encode(header + '.' + payload);
  const valid = await crypto.subtle.verify('HMAC', key, sigBytes, data);
  return valid; // true = signature is valid
}

For RS256 or ES256, use importKey with the public key in SPKI format and RSASSA-PKCS1-v1_5 or ECDSA respectively.

Common JWT Errors and What They Mean

  • jwt malformed

    Token does not have exactly three dot-separated parts, or one part contains invalid Base64url characters. Often caused by accidentally including the 'Bearer ' prefix.

  • jwt expired

    The exp claim is in the past. The token is technically valid but no longer acceptable. Issue a new token or extend the expiry window.

  • invalid signature

    The computed signature does not match the provided one. Usually means the wrong secret, wrong algorithm, or the token has been tampered with.

  • jwt not active

    The nbf (not before) claim is set to a future time. The token has been issued but is not yet valid. Check for clock skew between services.

  • invalid audience

    The aud claim does not match the expected audience. Tokens issued for one service are being used with another.

Try it now — no uploads, ever

The noserver JWT Debugger decodes tokens, checks expiration, and verifies HS256/RS256 signatures entirely in your browser. Your tokens never leave your device.

Open JWT Debugger

Frequently Asked Questions

Is it safe to paste a production JWT into an online tool?+

It depends on the tool. Services like jwt.io decode tokens server-side — your token travels across the internet. Use a local browser-based tool (like noserver) that never transmits your token anywhere.

Can I decode a JWT without knowing the secret?+

Yes. The header and payload are only Base64url-encoded — not encrypted. Anyone with the token can read its contents. The secret is only needed to verify the signature prevents tampering.

What is the difference between decoding and verifying a JWT?+

Decoding reads the header and payload by Base64url-decoding them — no key required. Verifying checks the cryptographic signature to confirm the token was issued by a trusted party and has not been modified. Always verify on the server before trusting claims.

How do I check if a JWT token has expired?+

Decode the payload and read the exp claim (Unix timestamp in seconds). Compare to Math.floor(Date.now() / 1000) — if exp is less than the current time, the token is expired.

What signing algorithm should I use for JWT?+

HS256 is fine when you control both issuer and verifier. Use RS256 or ES256 when different services need to verify tokens without sharing a secret — such as a microservices architecture or third-party token consumers.

Can I revoke a JWT before it expires?+

Not natively — JWTs are stateless. Common strategies: token blocklist checked on each request; short expiry (5-15 min) with refresh tokens; or switching to opaque session tokens for use cases requiring reliable revocation.

What does 'jwt malformed' error mean?+

The token does not have exactly three dot-separated parts or contains invalid Base64url characters. Common causes: accidentally including the 'Bearer ' prefix, a truncated token, or whitespace in the string.