Base64 in JSON Web Tokens (JWT)

A JWT looks like random text but is actually three Base64URL-encoded segments separated by dots. The header and payload are publicly readable — only the signature provides integrity. Here's exactly how it works.

JWT Structure

A JWT has three parts separated by .:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9    ← Header (Base64URL)
.
eyJzdWIiOiJ1c2VyXzEyMyIsIm5hbWUiOiJBbGljZSIsImlhdCI6MTcwMDAwMH0   ← Payload (Base64URL)
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  ← Signature (Base64URL of HMAC/RSA output)

Each part uses Base64URL encoding (RFC 4648 §5): + replaced by -, / replaced by _, and no padding.

Decoding a JWT Manually

// JavaScript — decode JWT header and payload without a library
function decodeJWT(token) {
  const [headerB64, payloadB64, signature] = token.split('.');

  function b64urlDecode(str) {
    // Restore standard Base64
    let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
    // Restore padding
    while (base64.length % 4) base64 += '=';
    return JSON.parse(atob(base64));
  }

  return {
    header: b64urlDecode(headerB64),
    payload: b64urlDecode(payloadB64),
    signature, // raw Base64URL — verification requires the secret
  };
}

const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsIm5hbWUiOiJBbGljZSJ9.xxx";
const { header, payload } = decodeJWT(jwt);
// header: { alg: "HS256", typ: "JWT" }
// payload: { sub: "user_123", name: "Alice" }

JWT Header

The header specifies the token type and signing algorithm:

// Base64URL decode: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
{
  "alg": "HS256",   // HMAC SHA-256
  "typ": "JWT"
}

// Other common algorithms:
// RS256 — RSA with SHA-256 (asymmetric, common for APIs)
// ES256 — ECDSA with SHA-256 (smaller signatures than RSA)
// HS512 — HMAC SHA-512
// none  — no signature (DANGEROUS — never accept in production)

JWT Payload (Claims)

The payload contains "claims" — statements about the user or token:

// Base64URL decode: eyJzdWIiOiJ1c2VyXzEyMyIsIm5hbWUiOiJBbGljZSIsImlhdCI6MTcwMDAwMH0
{
  // Registered claims (defined by RFC 7519)
  "sub": "user_123",     // Subject: who this token is about
  "iss": "auth.app.com", // Issuer: who issued the token
  "aud": "api.app.com",  // Audience: intended recipient
  "exp": 1700003600,     // Expiry: Unix timestamp when token expires
  "iat": 1700000000,     // Issued at: when the token was created
  "jti": "abc-123",      // JWT ID: unique token identifier

  // Custom claims — application-specific
  "name": "Alice",
  "role": "admin",
  "permissions": ["read", "write"]
}

Security warning: Anyone can decode the JWT header and payload — they are only Base64URL encoded, not encrypted. Never store passwords, credit card numbers, or other sensitive data in JWT claims.

The Signature

The signature is what makes JWTs secure. It's computed as:

signature = HMAC_SHA256(
  key: "your-secret-key",
  data: base64url(header) + "." + base64url(payload)
)
// Then the signature itself is Base64URL encoded

The signature ensures the token hasn't been tampered with. If you change even one character in the header or payload, the signature won't match and the token will be rejected.

Creating JWTs in JavaScript

// Using the 'jsonwebtoken' library (Node.js)
const jwt = require('jsonwebtoken');

const token = jwt.sign(
  { sub: 'user_123', name: 'Alice', role: 'admin' },
  'your-secret-key',
  { expiresIn: '1h' }
);

// Verify and decode
const decoded = jwt.verify(token, 'your-secret-key');
console.log(decoded.name); // "Alice"
// Using the Web Crypto API (browser / Edge Runtime)
async function createJWT(payload, secret) {
  const header = { alg: 'HS256', typ: 'JWT' };

  const encode = (obj) =>
    btoa(JSON.stringify(obj))
      .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

  const headerB64 = encode(header);
  const payloadB64 = encode(payload);
  const data = `${headerB64}.${payloadB64}`;

  const key = await crypto.subtle.importKey(
    'raw', new TextEncoder().encode(secret),
    { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
  );
  const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(data));
  const sigB64 = btoa(String.fromCharCode(...new Uint8Array(sig)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

  return `${data}.${sigB64}`;
}

Inspecting JWTs

To quickly inspect a JWT header and payload, paste the token into base64.dev — paste just the header or payload segment (without the dots and other parts), and it will decode the Base64URL to JSON. Switch to "URL Safe" mode first.

Decode JWT segments with base64.dev

Paste a JWT header or payload (the Base64URL part before or after the first dot) into the URL Safe mode to inspect the JSON contents.

Open base64.dev →