From 39176e3c4e81b25167928e5f2bd4e4fa4f5f6410 Mon Sep 17 00:00:00 2001 From: kallebysantos Date: Wed, 14 Jan 2026 11:33:56 +0000 Subject: [PATCH 1/5] 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 60ea51271..bb8a2efa8 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", @@ -105,13 +106,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; @@ -162,7 +178,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 00c30afced4f8fe19527e59a4d19a03a17cbcdc3 Mon Sep 17 00:00:00 2001 From: Kalleby Santos Date: Wed, 18 Feb 2026 15:31:05 +0000 Subject: [PATCH 2/5] 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 bb8a2efa8..467a0132c 100644 --- a/internal/functions/serve/templates/main.ts +++ b/internal/functions/serve/templates/main.ts @@ -106,9 +106,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) { @@ -120,10 +120,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) { @@ -133,6 +133,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) { @@ -175,15 +195,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); From 2258e814978a60dbb7be969ed1f28be8996316f1 Mon Sep 17 00:00:00 2001 From: Kalleby Santos Date: Mon, 9 Mar 2026 18:11:11 +0000 Subject: [PATCH 3/5] feat: passing down JWKs as internal env - It reduces functions bootime, since there's no need to fetch JWK on fly --- internal/functions/serve/serve.go | 2 ++ internal/functions/serve/templates/main.ts | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/functions/serve/serve.go b/internal/functions/serve/serve.go index 730e626da..ba3346413 100644 --- a/internal/functions/serve/serve.go +++ b/internal/functions/serve/serve.go @@ -126,12 +126,14 @@ func ServeFunctions(ctx context.Context, envFilePath string, noVerifyJWT *bool, if err != nil { return err } + jwks, _ := utils.Config.Auth.ResolveJWKS(ctx) env = append(env, fmt.Sprintf("SUPABASE_URL=http://%s:8000", utils.KongAliases[0]), "SUPABASE_ANON_KEY="+utils.Config.Auth.AnonKey.Value, "SUPABASE_SERVICE_ROLE_KEY="+utils.Config.Auth.ServiceRoleKey.Value, "SUPABASE_DB_URL="+dbUrl, "SUPABASE_INTERNAL_JWT_SECRET="+utils.Config.Auth.JwtSecret.Value, + "SUPABASE_INTERNAL_JWKS="+jwks, fmt.Sprintf("SUPABASE_INTERNAL_HOST_PORT=%d", utils.Config.Api.Port), ) if viper.GetBool("DEBUG") { diff --git a/internal/functions/serve/templates/main.ts b/internal/functions/serve/templates/main.ts index 467a0132c..ee6ede3c8 100644 --- a/internal/functions/serve/templates/main.ts +++ b/internal/functions/serve/templates/main.ts @@ -119,10 +119,19 @@ async function isValidLegacyJWT(jwtSecret: string, jwt: string): Promise { + try { + // using injected JWKS from cli + return jose.createLocalJWKSet(JSON.parse(Deno.env.get('SUPABASE_INTERNAL_JWKS'))); + } catch (error) { + return null + } +})(); + async function isValidJWT(jwksUrl: string, jwt: string): Promise { try { if (!jwks) { + // Loading from remote-url on fly jwks = jose.createRemoteJWKSet(new URL(jwksUrl)); } await jose.jwtVerify(jwt, jwks); From 55d59c9a1c02ddc0de83761852df6fd338456bae Mon Sep 17 00:00:00 2001 From: Kalleby Santos Date: Tue, 10 Mar 2026 01:02:37 +0000 Subject: [PATCH 4/5] stamp: using URL object instead of string concatenation --- internal/functions/serve/templates/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/functions/serve/templates/main.ts b/internal/functions/serve/templates/main.ts index ee6ede3c8..f9a5febac 100644 --- a/internal/functions/serve/templates/main.ts +++ b/internal/functions/serve/templates/main.ts @@ -31,7 +31,7 @@ const EXCLUDED_ENVS = ["HOME", "HOSTNAME", "PATH", "PWD"]; 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 JWKS_ENDPOINT = new URL('/auth/v1/.well-known/jwks.json', Deno.env.get("SUPABASE_URL")!) const DEBUG = Deno.env.get("SUPABASE_INTERNAL_DEBUG") === "true"; const FUNCTIONS_CONFIG_STRING = Deno.env.get( "SUPABASE_INTERNAL_FUNCTIONS_CONFIG", From 964f3bccc8ba18383949c2a58234884a5345e6c2 Mon Sep 17 00:00:00 2001 From: Kalleby Santos Date: Tue, 10 Mar 2026 01:21:42 +0000 Subject: [PATCH 5/5] stamp: codegen --- pkg/api/types.gen.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/api/types.gen.go b/pkg/api/types.gen.go index d995161a5..1e95e93da 100644 --- a/pkg/api/types.gen.go +++ b/pkg/api/types.gen.go @@ -3397,10 +3397,13 @@ type RealtimeConfigResponse struct { // MaxPresenceEventsPerSecond Sets maximum number of presence events per second rate limit MaxPresenceEventsPerSecond nullable.Nullable[int] `json:"max_presence_events_per_second"` + // PresenceEnabled Whether to enable presence + PresenceEnabled bool `json:"presence_enabled"` + // PrivateOnly Whether to only allow private channels PrivateOnly nullable.Nullable[bool] `json:"private_only"` - // Suspend Whether to suspend realtime + // Suspend Disables the Realtime service for this project when true. Set to false to re-enable it. Suspend nullable.Nullable[bool] `json:"suspend"` } @@ -4152,10 +4155,13 @@ type UpdateRealtimeConfigBody struct { // MaxPresenceEventsPerSecond Sets maximum number of presence events per second rate limit MaxPresenceEventsPerSecond *int `json:"max_presence_events_per_second,omitempty"` + // PresenceEnabled Whether to enable presence + PresenceEnabled *bool `json:"presence_enabled,omitempty"` + // PrivateOnly Whether to only allow private channels PrivateOnly *bool `json:"private_only,omitempty"` - // Suspend Whether to suspend realtime + // Suspend Disables the Realtime service for this project when true. Set to false to re-enable it. Suspend *bool `json:"suspend,omitempty"` }