Skip to content

Validate JSON web tokens (JWT)

Extract a JWT from the Authorization header, verify its HMAC-SHA256 signature using the WebCrypto API, and validate its claims.

JavaScript
export default {
async fetch(request) {
const HMAC_SECRET = "mysecretsymmetrickey"; // Change this to your secret key
const CLOCK_TOLERANCE = 30; // Tolerance for clock skew in seconds
// Extract JWT token from "Authorization: Bearer" header
function getJWTToken(request) {
const authorizationHeader = request.headers.get("Authorization");
if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) {
return authorizationHeader.substring(7, authorizationHeader.length);
}
return null;
}
// Convert a base64url string to a Uint8Array
function base64urlToUint8Array(base64url) {
const base64 = base64url
.replace(/-/g, "+")
.replace(/_/g, "/")
.padEnd(base64url.length + ((4 - (base64url.length % 4)) % 4), "=");
const binary = atob(base64);
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
}
// Decode a base64url-encoded JWT segment to a JSON object
function decodeSegment(s) {
const bytes = base64urlToUint8Array(s);
return JSON.parse(new TextDecoder().decode(bytes));
}
// Validate JWT token structure, cryptographic signature, and claims.
async function validateJWT(token) {
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("Invalid JWT format");
}
const [headerB64, payloadB64, signatureB64] = parts;
const decodedHeader = decodeSegment(headerB64);
const decodedPayload = decodeSegment(payloadB64);
// Verify the algorithm.
// This prevents algorithm-confusion attacks where an attacker changes the "alg" header to bypass signature verification.
// Never branch on the token's alg header to decide how to verify.
// Always enforce the algorithm you expect.
if (decodedHeader.alg !== "HS256") {
throw new Error("Unsupported algorithm");
}
// Verify the cryptographic signature.
// Import the shared secret as an HMAC-SHA256 key, then verify that the signature matches "header.payload".
// If this fails, the token was tampered with or signed with a different key.
const signature = base64urlToUint8Array(signatureB64);
const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(HMAC_SECRET),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"],
);
const valid = await crypto.subtle.verify(
{ name: "HMAC" },
key,
signature,
data,
);
if (!valid) {
throw new Error("Invalid signature");
}
// Validate standard claims.
// The claims a JWT must contain depends on your use case.
const now = Math.floor(Date.now() / 1000);
// exp (expiration): Reject tokens that have expired.
// A small clock tolerance accounts for clock skew between servers.
if (typeof decodedPayload.exp !== "number") {
throw new Error("Missing exp claim");
}
if (decodedPayload.exp < now - CLOCK_TOLERANCE) {
throw new Error("JWT has expired");
}
// iat (issued at): Ensure the token declares when it was created.
if (typeof decodedPayload.iat !== "number") {
throw new Error("Missing iat claim");
}
// Additional claims per RFC 7519 (https://www.rfc-editor.org/rfc/rfc7519.html):
// - nbf (not before): Reject tokens used before their valid start time.
// if (decodedPayload.nbf > now + CLOCK_TOLERANCE) throw new Error("JWT is not yet valid");
// - iss (issuer): Ensure the token was issued by a trusted authority.
// if (decodedPayload.iss !== "https://auth.example.com") throw new Error("Invalid issuer");
// - aud (audience): Ensure the token is intended for your service.
// if (decodedPayload.aud !== "my-api") throw new Error("Invalid audience");
// - sub (subject): Identify the principal the token refers to.
// - jti (JWT ID): Unique token identifier, useful for preventing replay attacks.
return true;
}
// Execute the function to extract JWT token
const jwtToken = getJWTToken(request);
// If the token is not provided, serve 401 Forbidden
if (!jwtToken) {
return new Response("Missing JWT token", { status: 401 });
}
try {
// If the token is valid, the validateJWT function will not error and serve the actual response
// An example of a valid token that will expire in 2033 is "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNjI0OTkyMDAwLCJleHAiOjIwMDExMjAwMDB9._qgQ_TMrGfYgOoA8HtTZwEGoj8zAPWxsz8CT1jEAGzo"
await validateJWT(jwtToken);
return fetch(request);
} catch {
return new Response("Invalid JWT token", { status: 401 });
}
},
};