Skip to content
Security 8 min read

JWT Explained: Decoding JSON Web Tokens Without a Library

Understand the structure of JSON Web Tokens, how to decode the header and payload in the browser, verify signatures, and avoid common JWT security pitfalls.

ToolsVito Team

What Is a JWT?

A JSON Web Token (JWT) is a compact, self-contained way to transmit information between parties as a JSON object. The information is digitally signed, so the receiver can verify it hasn't been tampered with. JWTs are defined in RFC 7519.

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Three parts, separated by dots (.):

  1. Header — algorithm & token type
  2. Payload — claims (data)
  3. Signature — proof of integrity

Decoding the Header

The header is a Base64url-encoded JSON object:

atob("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")
// {"alg":"HS256","typ":"JWT"}
  • alg — signing algorithm: HS256, RS256, ES256, etc.
  • typ — always JWT for standard tokens.

Decoding the Payload

The payload contains claims — statements about the user and additional metadata:

// Registered claims (standardized)
{
  "sub": "1234567890",  // Subject (user ID)
  "iss": "auth.example.com",  // Issuer
  "aud": "api.example.com",   // Audience
  "exp": 1716239022,  // Expiration (Unix timestamp)
  "iat": 1516239022,  // Issued at
  "nbf": 1516239022   // Not before
}

You can add any custom claims alongside these registered ones.

Manually Decoding in JavaScript

function decodeJwt(token) {
  const [header, payload] = token.split(".");
  const decode = (b64) =>
    JSON.parse(atob(b64.replace(/-/g, "+").replace(/_/g, "/")));
  return {
    header: decode(header),
    payload: decode(payload),
  };
}

Note the -+ and _/ replacements — JWTs use URL-safe Base64, but atob() needs standard Base64.

Verifying the Signature

Decoding is trivial. Verifying the signature is what provides security. Never trust a JWT just because you decoded it:

// Node.js — verify HS256 with the Web Crypto API
async function verifyJwt(token, secret) {
  const [header, payload, sig] = token.split(".");
  const key = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["verify"]
  );
  const data = new TextEncoder().encode(header + "." + payload);
  const sigBytes = Uint8Array.from(atob(sig.replace(/-/g,"+").replace(/_/g,"/")), c => c.charCodeAt(0));
  return crypto.subtle.verify("HMAC", key, sigBytes, data);
}

Critical Security Pitfalls

1. The "alg: none" Attack

A malicious user can craft a JWT with "alg": "none" and no signature. Naive verifiers accept it. Always reject tokens with alg: none.

2. HS256 vs RS256 Confusion

If your server expects RS256 but an attacker sends an HS256 token signed with the public key (which is, well, public), some libraries will accept it. Explicitly specify the expected algorithm.

3. Expired Tokens

Always check exp. A valid signature on an expired token is still invalid.

4. Storing JWTs in localStorage

localStorage is accessible from JavaScript and therefore vulnerable to XSS. Prefer httpOnly cookies for session tokens.

When to Use JWTs

  • Stateless APIs: JWTs let the server validate a token without querying a database.
  • Cross-domain auth: A JWT signed by an auth server can be accepted by multiple API services.
  • Short-lived tokens: Set a low exp (15–60 min) and use refresh tokens for longer sessions.

JWTs are not a replacement for server-side sessions in every case — if you need instant revocation, a session database is more reliable.

Inspect a JWT Instantly

Paste any JWT into ToolsVito's JWT Decoder to see the header, payload, expiry status, and algorithm — all in your browser, nothing sent to a server.

Try it now — free, runs in your browser

JWT Decoder

Inspect JSON Web Tokens