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 (.):
- Header — algorithm & token type
- Payload — claims (data)
- 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— alwaysJWTfor 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.