JWT and Modern Web Authentication
· Cosyslabs
JSON Web Tokens (JWT) are a compact, self-contained format for transmitting claims between parties as a signed JSON object. A JWT carries its own metadata — expiry time, issuer, subject — and can be verified without a database lookup. Understanding JWT structure, algorithms, and attack vectors is essential for implementing secure authentication in web applications.
JWT Structure
A JWT consists of three Base64URL-encoded parts separated by dots:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfMTIzIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzE2MDAwMDAwLCJleHAiOjE3MTYwMDM2MDB9.signature
Header (algorithm and token type):
{
"alg": "RS256",
"typ": "JWT"
}
Payload (claims):
{
"sub": "usr_123", // subject (user ID)
"name": "Alice Chen",
"email": "[email protected]",
"roles": ["admin"],
"iat": 1716000000, // issued at (Unix timestamp)
"exp": 1716003600, // expires at (1 hour later)
"iss": "https://auth.example.com", // issuer
"aud": "https://api.example.com" // audience
}
Signature: cryptographic proof that the header and payload were not tampered with.
Signing Algorithms
Choose the right algorithm based on your architecture:
| Algorithm | Type | Use Case |
|---|---|---|
| HS256 | HMAC-SHA256 (symmetric) | Monolith where one server signs and verifies |
| RS256 | RSA-SHA256 (asymmetric) | Microservices — auth server signs, others verify with public key |
| ES256 | ECDSA-P256 (asymmetric) | Like RS256 but shorter signatures, better performance |
| none | No signature | Never use — allows any payload to pass verification |
// Node.js — jsonwebtoken library
import jwt from "jsonwebtoken";
// HS256 — symmetric
const token = jwt.sign(
{ sub: "usr_123", roles: ["admin"] },
process.env.JWT_SECRET,
{ algorithm: "HS256", expiresIn: "15m", issuer: "https://auth.example.com" }
);
// RS256 — asymmetric (sign with private key)
const token = jwt.sign(
{ sub: "usr_123" },
fs.readFileSync("private.pem"),
{ algorithm: "RS256", expiresIn: "15m" }
);
// Verify (RS256 — use public key; attackers don't have it)
try {
const payload = jwt.verify(token, fs.readFileSync("public.pem"), {
algorithms: ["RS256"], // NEVER allow ["RS256", "none"]
issuer: "https://auth.example.com",
audience: "https://api.example.com",
});
// payload.sub is the user ID
} catch (err) {
if (err.name === "TokenExpiredError") {
// Token expired — send 401 and ask client to refresh
}
// Other errors: invalid signature, wrong issuer, etc.
}
Verifying JWTs Without a Library
Understanding verification helps when debugging:
function decodeJwt(token) {
const [header, payload, signature] = token.split(".");
function base64UrlDecode(str) {
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
return JSON.parse(atob(padded));
}
return {
header: base64UrlDecode(header),
payload: base64UrlDecode(payload),
// Note: only decoding, not verifying the signature
};
}
// NEVER trust a decoded payload without verifying the signature server-side
Refresh Token Pattern
Access tokens should be short-lived (15 minutes). Use refresh tokens for session continuity:
1. Login → server returns { accessToken (15min), refreshToken (7 days) }
2. Client stores refreshToken in httpOnly cookie (not localStorage)
3. Client includes accessToken in Authorization header for API calls
4. When accessToken expires (401), client calls POST /auth/refresh
5. Server validates refreshToken, issues new accessToken (and rotates refreshToken)
6. On logout: server invalidates the refreshToken in the database
// Express refresh endpoint
app.post("/auth/refresh", async (req, res) => {
const refreshToken = req.cookies.refresh_token;
if (!refreshToken) return res.status(401).json({ error: "No refresh token" });
// Look up refresh token in database (allows revocation)
const stored = await db.refreshToken.findUnique({ where: { token: refreshToken } });
if (!stored || stored.revokedAt) return res.status(401).json({ error: "Invalid token" });
if (stored.expiresAt < new Date()) return res.status(401).json({ error: "Expired" });
// Rotate: delete old token, issue new one
await db.refreshToken.delete({ where: { id: stored.id } });
const newRefreshToken = crypto.randomBytes(32).toString("hex");
await db.refreshToken.create({
data: {
token: newRefreshToken,
userId: stored.userId,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
});
const accessToken = jwt.sign({ sub: stored.userId }, process.env.JWT_SECRET, {
expiresIn: "15m",
});
res.cookie("refresh_token", newRefreshToken, { httpOnly: true, secure: true, sameSite: "Strict" });
res.json({ accessToken });
});
JWT Security Vulnerabilities
Algorithm confusion attack: An attacker changes alg from RS256 to HS256 and signs the token using the public key as the HMAC secret. Fix: always specify allowed algorithms explicitly.
none algorithm: Setting alg: "none" removes the signature. Some libraries accepted this by default. Fix: reject tokens with alg: "none".
Sensitive data in payload: The payload is Base64-encoded, not encrypted. Anyone with the token can decode the payload. Never include passwords, credit card numbers, or secrets in JWT claims.
Missing expiry: A JWT with no exp claim is valid forever. Always set expiresIn.
Storing in localStorage: XSS can steal tokens from localStorage. Store refresh tokens in httpOnly cookies; keep short-lived access tokens in memory.
OAuth 2.0 and OpenID Connect
OAuth 2.0 is an authorization framework; OpenID Connect (OIDC) adds authentication on top:
Authorization Code Flow:
1. App redirects user to Auth Server: GET /authorize?response_type=code&client_id=...
2. User authenticates at Auth Server
3. Auth Server redirects back: GET /callback?code=AUTH_CODE
4. App exchanges code: POST /token { code, client_secret, ... }
5. Auth Server returns { access_token, refresh_token, id_token }
6. id_token is a JWT containing user identity claims
The id_token (OIDC) contains who the user is. The access_token (OAuth 2.0) grants access to resources — it may or may not be a JWT depending on the provider.
Tools
- JWT Decoder — decode and inspect JWT headers and payloads
- JWT Verifier — verify JWT signatures with a public key or secret
- HMAC Generator — generate HMAC-SHA256 signatures for HS256 tokens