diff --git a/internal/functions/serve/templates/main.ts b/internal/functions/serve/templates/main.ts index f6676c163..3ef644338 100644 --- a/internal/functions/serve/templates/main.ts +++ b/internal/functions/serve/templates/main.ts @@ -1,7 +1,7 @@ import { STATUS_CODE, STATUS_TEXT } from "https://deno.land/std/http/status.ts"; import * as posix from "https://deno.land/std/path/posix/mod.ts"; -import * as jose from "https://deno.land/x/jose@v4.13.1/index.ts"; +import * as jose from "jsr:@panva/jose@6"; const SB_SPECIFIC_ERROR_CODE = { BootError: @@ -29,8 +29,9 @@ const SB_SPECIFIC_ERROR_REASON = { // OS stuff - we don't want to expose these to the functions. const EXCLUDED_ENVS = ["HOME", "HOSTNAME", "PATH", "PWD"]; -const JWT_SECRET = Deno.env.get("SUPABASE_INTERNAL_JWT_SECRET")!; const HOST_PORT = Deno.env.get("SUPABASE_INTERNAL_HOST_PORT")!; +const JWT_SECRET = Deno.env.get("SUPABASE_INTERNAL_JWT_SECRET")!; +const JWKS_ENDPOINT = Deno.env.get("SUPABASE_URL")! + "/auth/v1/.well-known/jwks.json"; const DEBUG = Deno.env.get("SUPABASE_INTERNAL_DEBUG") === "true"; const FUNCTIONS_CONFIG_STRING = Deno.env.get( "SUPABASE_INTERNAL_FUNCTIONS_CONFIG", @@ -104,18 +105,53 @@ function getAuthToken(req: Request) { return token; } -async function verifyJWT(jwt: string): Promise { +async function isValidLegacyJWT(jwtSecret: string, jwt: string): Promise { const encoder = new TextEncoder(); - const secretKey = encoder.encode(JWT_SECRET); + const secretKey = encoder.encode(jwtSecret); try { await jose.jwtVerify(jwt, secretKey); } catch (e) { - console.error(e); + console.error('Symmetric Legacy JWT verification error', e); + return false; + } + return true; +} + +// Lazy-loading JWKs +let jwks = null; +async function isValidJWT(jwksUrl: string, jwt: string): Promise { + try { + if (!jwks) { + jwks = jose.createRemoteJWKSet(new URL(jwksUrl)); + } + await jose.jwtVerify(jwt, jwks); + } catch (e) { + console.error('Asymmetric JWT verification error', e); return false; } return true; } +/** + * Applies hybrid JWT verification, using JWK as primary and Legacy Secret as fallback. + * Use only during 'New JWT Keys' migration period, while `JWT_SECRET` is still available. + */ +export async function verifyHybridJWT(jwtSecret: string, jwksUrl: string, jwt: string): Promise { + const { alg: jwtAlgorithm } = jose.decodeProtectedHeader(jwt) + + if (jwtAlgorithm === 'HS256') { + console.log(`Legacy token type detected, attempting ${jwtAlgorithm} verification.`) + + return await isValidLegacyJWT(jwtSecret, jwt) + } + + if (jwtAlgorithm === 'ES256' || jwtAlgorithm === 'RS256') { + return await isValidJWT(jwksUrl, jwt) + } + + return false; +} + // Ref: https://docs.deno.com/examples/checking_file_existence/ async function shouldUsePackageJsonDiscovery({ entrypointPath, importMapPath }: FunctionConfig): Promise { if (importMapPath) { @@ -158,7 +194,7 @@ Deno.serve({ if (req.method !== "OPTIONS" && functionsConfig[functionName].verifyJWT) { try { const token = getAuthToken(req); - const isValidJWT = await verifyJWT(token); + const isValidJWT = await verifyHybridJWT(JWT_SECRET, JWKS_ENDPOINT, token); if (!isValidJWT) { return getResponse({ msg: "Invalid JWT" }, STATUS_CODE.Unauthorized);