From edf51dfd98b6091ae23e1bb7c7bd8d7cb5e59670 Mon Sep 17 00:00:00 2001 From: Atharv Gaur <161715841+Athaxv@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:26:29 +0530 Subject: [PATCH] fix(self-hosted): add hybrid JWT verification for edge functions docker template --- docker/volumes/functions/main/index.ts | 93 +++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 9 deletions(-) diff --git a/docker/volumes/functions/main/index.ts b/docker/volumes/functions/main/index.ts index b593527d687..ebe2061c4ba 100644 --- a/docker/volumes/functions/main/index.ts +++ b/docker/volumes/functions/main/index.ts @@ -3,8 +3,31 @@ import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts' console.log('main function started') const JWT_SECRET = Deno.env.get('JWT_SECRET') +const SUPABASE_URL = Deno.env.get('SUPABASE_URL') const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true' +// Create JWKS for ES256/RS256 tokens (newer tokens) +let SUPABASE_JWT_KEYS: ReturnType | null = null +if (SUPABASE_URL) { + try { + SUPABASE_JWT_KEYS = jose.createRemoteJWKSet( + new URL('/auth/v1/.well-known/jwks.json', SUPABASE_URL) + ) + } catch (e) { + console.error('Failed to fetch JWKS from SUPABASE_URL:', e) + } +} + +/** + * Extract JWT token from Authorization header + * + * Parses the Authorization header to extract the Bearer token. + * Expects format: "Bearer " + * + * @param req - The HTTP request object + * @returns The JWT token string + * @throws Error if Authorization header is missing or malformed + */ function getAuthToken(req: Request) { const authHeader = req.headers.get('authorization') if (!authHeader) { @@ -17,23 +40,75 @@ function getAuthToken(req: Request) { return token } -async function verifyJWT(jwt: string): Promise { - const encoder = new TextEncoder() - const secretKey = encoder.encode(JWT_SECRET) - try { - await jose.jwtVerify(jwt, secretKey) - } catch (err) { - console.error(err) +async function isValidLegacyJWT(jwt: string): Promise { + if (!JWT_SECRET) { + console.error('JWT_SECRET not available for HS256 token verification') return false } - return true + + const encoder = new TextEncoder(); + const secretKey = encoder.encode(JWT_SECRET) + + try { + await jose.jwtVerify(jwt, secretKey); + } catch (e) { + console.error('Symmetric Legacy JWT verification error', e); + return false; + } + return true; +} + +async function isValidJWT(jwt: string): Promise { + if (!SUPABASE_JWT_KEYS) { + console.error('JWKS not available for ES256/RS256 token verification') + return false + } + + try { + await jose.jwtVerify(jwt, SUPABASE_JWT_KEYS) + } catch (e) { + console.error('Asymmetric JWT verification error', e); + return false + } + + return true; +} + +/** + * Verify JWT token, handling both legacy (HS256) and newer (ES256/RS256) algorithms + * + * This function automatically detects the algorithm used in the token and applies + * the appropriate verification method: + * - HS256: Uses JWT_SECRET (symmetric key) + * - ES256/RS256: Uses JWKS endpoint (asymmetric public keys) + * + * This fix ensures compatibility with both legacy tokens and newer asymmetric tokens, + * resolving the "Key for the ES256 algorithm must be of type CryptoKey" error. + * + * @param jwt - The JWT token string to verify + * @returns Promise resolving to true if verification succeeds, false otherwise + */ +async function isValidHybridJWT(jwt: string): Promise { + const { alg: jwtAlgorithm } = jose.decodeProtectedHeader(jwt) + + if (jwtAlgorithm === 'HS256') { + console.log(`Legacy token type detected, attempting ${jwtAlgorithm} verification.`) + + return await isValidLegacyJWT(jwt) + } + + if (jwtAlgorithm === 'ES256' || jwtAlgorithm === 'RS256') { + return await isValidJWT(jwt) + } + + return false; } Deno.serve(async (req: Request) => { if (req.method !== 'OPTIONS' && VERIFY_JWT) { try { const token = getAuthToken(req) - const isValidJWT = await verifyJWT(token) + const isValidJWT = await isValidHybridJWT(token); if (!isValidJWT) { return new Response(JSON.stringify({ msg: 'Invalid JWT' }), {