From 0e4a217608067ef19800b877e13f1c6c1a6861b6 Mon Sep 17 00:00:00 2001 From: kallebysantos Date: Wed, 14 Jan 2026 11:33:56 +0000 Subject: [PATCH 1/2] feat: adding hybrid jwt verification Allows verify new JWTs as well legacy --- internal/functions/serve/templates/main.ts | 31 ++++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/internal/functions/serve/templates/main.ts b/internal/functions/serve/templates/main.ts index f6676c163..997ed1089 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,13 +105,28 @@ function getAuthToken(req: Request) { return token; } -async function verifyJWT(jwt: string): Promise { +async function verifyLegacyJWT(jwt: string): Promise { const encoder = new TextEncoder(); const secretKey = encoder.encode(JWT_SECRET); 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 verifyJWT(jwt: string): Promise { + try { + if (!jwks) { + jwks = jose.createRemoteJWKSet(new URL(JWKS_ENDPOINT)); + } + await jose.jwtVerify(jwt, jwks); + } catch (e) { + console.error('Asymmetric JWT verification error', e); return false; } return true; @@ -161,7 +177,12 @@ Deno.serve({ const isValidJWT = await verifyJWT(token); if (!isValidJWT) { - return getResponse({ msg: "Invalid JWT" }, STATUS_CODE.Unauthorized); + console.log('Asymmetric JWT verification failed; attempting legacy verification.') + const isValidLegacyJWT = await verifyLegacyJWT(token); + + if (!isValidLegacyJWT) { + return getResponse({ msg: "Invalid JWT" }, STATUS_CODE.Unauthorized); + } } } catch (e) { console.error(e); From 4c30f0a9f5599b687a5a5111a6988cd36d00498b Mon Sep 17 00:00:00 2001 From: Kalleby Santos Date: Wed, 18 Feb 2026 15:31:05 +0000 Subject: [PATCH 2/2] stamp: detect algorithm before verify JWT It helps to reduce latency for Legacy token verifications, since it avoid unnecessary requests. --- internal/functions/serve/templates/main.ts | 37 +++++++++++++++------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/internal/functions/serve/templates/main.ts b/internal/functions/serve/templates/main.ts index 997ed1089..3ef644338 100644 --- a/internal/functions/serve/templates/main.ts +++ b/internal/functions/serve/templates/main.ts @@ -105,9 +105,9 @@ function getAuthToken(req: Request) { return token; } -async function verifyLegacyJWT(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) { @@ -119,10 +119,10 @@ async function verifyLegacyJWT(jwt: string): Promise { // Lazy-loading JWKs let jwks = null; -async function verifyJWT(jwt: string): Promise { +async function isValidJWT(jwksUrl: string, jwt: string): Promise { try { if (!jwks) { - jwks = jose.createRemoteJWKSet(new URL(JWKS_ENDPOINT)); + jwks = jose.createRemoteJWKSet(new URL(jwksUrl)); } await jose.jwtVerify(jwt, jwks); } catch (e) { @@ -132,6 +132,26 @@ async function verifyJWT(jwt: string): Promise { 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) { @@ -174,15 +194,10 @@ 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) { - console.log('Asymmetric JWT verification failed; attempting legacy verification.') - const isValidLegacyJWT = await verifyLegacyJWT(token); - - if (!isValidLegacyJWT) { - return getResponse({ msg: "Invalid JWT" }, STATUS_CODE.Unauthorized); - } + return getResponse({ msg: "Invalid JWT" }, STATUS_CODE.Unauthorized); } } catch (e) { console.error(e);