JSON Web Tokens (JWTs) are the standard for authentication in modern APIs. You see them in Authorization headers, cookies, and URL parameters. This guide explains what they contain, how they work, and how to debug common JWT problems.
What is a JWT?
A JWT is a compact, self-contained token that encodes user information and a signature. It looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzE2MTU4NDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
It has three parts separated by dots — each part is Base64URL-encoded:
- Header — the algorithm and token type
- Payload — the claims (user data)
- Signature — verifies the token was not tampered with
The Header
{
"alg": "HS256",
"typ": "JWT"
}The alg field specifies the signing algorithm. The most common values are HS256 and RS256 — more on those below.
The Payload (Claims)
{
"sub": "1234567890",
"name": "Alice",
"email": "[email protected]",
"role": "admin",
"iat": 1716158400,
"exp": 1716162000
}The payload contains claims — statements about the user. Standard registered claims:
| Claim | Meaning |
|---|---|
sub | Subject — the user ID |
iss | Issuer — who created the token |
aud | Audience — who the token is intended for |
exp | Expiration — Unix timestamp when the token expires |
iat | Issued at — Unix timestamp when the token was created |
nbf | Not before — token is invalid before this time |
jti | JWT ID — a unique identifier for the token |
Applications add custom claims too — like role, permissions, and tenant_id. The payload is Base64URL-encoded, not encrypted — anyone can read it. Never put passwords or secrets in a JWT payload.
HS256 vs RS256 — Which Algorithm?
HS256 (HMAC-SHA256) uses a single shared secret key for both signing and verification. Simple to set up, but every service that verifies the token must know the secret.
RS256 (RSA-SHA256) uses a private key to sign and a public key to verify. The issuer keeps the private key secret; anyone can verify tokens using the published public key. This is better for distributed systems where multiple services need to verify tokens.
Use HS256 for simple single-server setups. Use RS256 for microservices, third-party integrations, or any system where verification happens outside the auth server.
Debugging Common JWT Errors
401 Unauthorized — token expired
Paste your token into the JWT Decoder and check the exp claim. If it shows "Expired", request a new token — usually by using your refresh token or logging in again.
401 Unauthorized — invalid signature
The token was signed with a different secret than what the server is using to verify it. Common causes: the wrong secret key in your environment variables, a mismatched key between environments (dev vs prod), or the token was tampered with in transit.
Token not being sent
Check that you are including the token in every request: Authorization: Bearer YOUR_TOKEN. The word "Bearer" is required — a common mistake is sending just the token without it.
nbf — token not yet valid
Some tokens have a "not before" (nbf) claim. If the server clock is slightly ahead of the issuer's clock, the token may be rejected as "not yet valid". Add a small clock skew tolerance in your verification logic.
How to Decode a JWT in JavaScript
// Quick decode (no verification)
function decodeJWT(token) {
const payload = token.split('.')[1];
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decoded);
}
// For production — use a library that verifies the signature
// Node.js: npm install jsonwebtoken
const jwt = require('jsonwebtoken');
const decoded = jwt.verify(token, process.env.JWT_SECRET);Try it now: JWT Decoder — paste any JWT to inspect its header, payload claims and expiry status instantly.