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" }
Decoding a JWT in Python
import base64, json
def decode_jwt(token: str) -> dict:
header_b64, payload_b64, *_ = token.split(".")
def b64url(seg: str) -> dict:
seg += "=" * (-len(seg) % 4) # restore padding
return json.loads(base64.urlsafe_b64decode(seg))
return {"header": b64url(header_b64), "payload": b64url(payload_b64)}
# To verify (not just decode), use PyJWT:
# import jwt; jwt.decode(token, key, algorithms=["HS256"])
Decode vs. verify: the snippets above only read the token — they do not check the signature. Anyone can decode a JWT. To trust it, you must verify the signature with the secret or public key using a library (jsonwebtoken, PyJWT, jose, etc.).
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.
Registered claims reference
The seven standard claims defined by RFC 7519. All are optional, and exp, nbf, and iat are Unix timestamps (seconds since 1970):
| Claim | Name | Meaning |
|---|---|---|
| iss | Issuer | Who issued the token |
| sub | Subject | Who the token is about (usually a user ID) |
| aud | Audience | Intended recipient(s) of the token |
| exp | Expiration | After this time the token must be rejected |
| nbf | Not Before | Token is invalid before this time |
| iat | Issued At | When the token was created |
| jti | JWT ID | Unique identifier (helps prevent replay) |
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}`;
}
Frequently Asked Questions
Is a JWT just Base64 encoded?
The header and payload of a JWT are Base64URL encoded and can be decoded by anyone. Only the signature requires the secret key to verify. This means JWT payloads are NOT secret — never store sensitive data in a JWT payload without additional encryption.
How do I decode a JWT without a library?
Split the token by '.' to get three parts (header, payload, signature). Add padding to make the length a multiple of 4, replace - with + and _ with /, then base64-decode each part. The header and payload are JSON strings. Or paste the whole token into the decoder at the top of this page.
Can I decode a JWT without the secret key?
Yes. Decoding only reads the Base64URL header and payload, which need no key. The secret is required only to verify the signature or to create a valid token — not to read the claims.
What is the difference between decoding and verifying a JWT?
Decoding turns the Base64URL segments back into JSON (anyone can do it). Verifying recomputes the signature with the secret/public key and confirms the token was not tampered with and has not expired. Never trust a decoded JWT you have not verified.
Is it safe to decode a JWT online?
On base64.dev, yes — this decoder runs entirely in your browser and the token is never sent to a server. Still, avoid pasting production tokens into tools you do not trust, since a JWT can grant access until it expires.
How do I check if a JWT is expired?
Read the exp claim (a Unix timestamp in seconds) and compare it to the current time. The decoder above shows the expiry status automatically. In code: Date.now() / 1000 > payload.exp.
Need to encode text, files, or URL-safe Base64?
The main base64.dev tool handles text, images, files, and URL-safe Base64 with auto-detect.
Open base64.dev →