Pular para o conteúdo

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.

Atualizado em Ver como Markdown
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 });
		}
	},
};