Skip to content

Commit 35867ca

Browse files
feat: add hybrid jwt verification (#4721)
* feat: adding hybrid jwt verification Allows verify new JWTs as well legacy * stamp: detect algorithm before verify JWT It helps to reduce latency for Legacy token verifications, since it avoid unnecessary requests. * feat: passing down JWKs as internal env - It reduces functions bootime, since there's no need to fetch JWK on fly * stamp: using URL object instead of string concatenation * stamp: codegen --------- Co-authored-by: Andrew Valleteau <avallete@users.noreply.github.com>
1 parent f73cfdc commit 35867ca

2 files changed

Lines changed: 53 additions & 6 deletions

File tree

internal/functions/serve/serve.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,14 @@ func ServeFunctions(ctx context.Context, envFilePath string, noVerifyJWT *bool,
126126
if err != nil {
127127
return err
128128
}
129+
jwks, _ := utils.Config.Auth.ResolveJWKS(ctx)
129130
env = append(env,
130131
fmt.Sprintf("SUPABASE_URL=http://%s:8000", utils.KongAliases[0]),
131132
"SUPABASE_ANON_KEY="+utils.Config.Auth.AnonKey.Value,
132133
"SUPABASE_SERVICE_ROLE_KEY="+utils.Config.Auth.ServiceRoleKey.Value,
133134
"SUPABASE_DB_URL="+dbUrl,
134135
"SUPABASE_INTERNAL_JWT_SECRET="+utils.Config.Auth.JwtSecret.Value,
136+
"SUPABASE_INTERNAL_JWKS="+jwks,
135137
fmt.Sprintf("SUPABASE_INTERNAL_HOST_PORT=%d", utils.Config.Api.Port),
136138
)
137139
if viper.GetBool("DEBUG") {

internal/functions/serve/templates/main.ts

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { STATUS_CODE, STATUS_TEXT } from "https://deno.land/std/http/status.ts";
22
import * as posix from "https://deno.land/std/path/posix/mod.ts";
33

4-
import * as jose from "https://deno.land/x/jose@v4.13.1/index.ts";
4+
import * as jose from "jsr:@panva/jose@6";
55

66
const SB_SPECIFIC_ERROR_CODE = {
77
BootError:
@@ -29,8 +29,9 @@ const SB_SPECIFIC_ERROR_REASON = {
2929
// OS stuff - we don't want to expose these to the functions.
3030
const EXCLUDED_ENVS = ["HOME", "HOSTNAME", "PATH", "PWD"];
3131

32-
const JWT_SECRET = Deno.env.get("SUPABASE_INTERNAL_JWT_SECRET")!;
3332
const HOST_PORT = Deno.env.get("SUPABASE_INTERNAL_HOST_PORT")!;
33+
const JWT_SECRET = Deno.env.get("SUPABASE_INTERNAL_JWT_SECRET")!;
34+
const JWKS_ENDPOINT = new URL('/auth/v1/.well-known/jwks.json', Deno.env.get("SUPABASE_URL")!)
3435
const DEBUG = Deno.env.get("SUPABASE_INTERNAL_DEBUG") === "true";
3536
const FUNCTIONS_CONFIG_STRING = Deno.env.get(
3637
"SUPABASE_INTERNAL_FUNCTIONS_CONFIG",
@@ -105,18 +106,62 @@ function getAuthToken(req: Request) {
105106
return token;
106107
}
107108

108-
async function verifyJWT(jwt: string): Promise<boolean> {
109+
async function isValidLegacyJWT(jwtSecret: string, jwt: string): Promise<boolean> {
109110
const encoder = new TextEncoder();
110-
const secretKey = encoder.encode(JWT_SECRET);
111+
const secretKey = encoder.encode(jwtSecret);
111112
try {
112113
await jose.jwtVerify(jwt, secretKey);
113114
} catch (e) {
114-
console.error(e);
115+
console.error('Symmetric Legacy JWT verification error', e);
116+
return false;
117+
}
118+
return true;
119+
}
120+
121+
// Lazy-loading JWKs
122+
let jwks = (() => {
123+
try {
124+
// using injected JWKS from cli
125+
return jose.createLocalJWKSet(JSON.parse(Deno.env.get('SUPABASE_INTERNAL_JWKS')));
126+
} catch (error) {
127+
return null
128+
}
129+
})();
130+
131+
async function isValidJWT(jwksUrl: string, jwt: string): Promise<boolean> {
132+
try {
133+
if (!jwks) {
134+
// Loading from remote-url on fly
135+
jwks = jose.createRemoteJWKSet(new URL(jwksUrl));
136+
}
137+
await jose.jwtVerify(jwt, jwks);
138+
} catch (e) {
139+
console.error('Asymmetric JWT verification error', e);
115140
return false;
116141
}
117142
return true;
118143
}
119144

145+
/**
146+
* Applies hybrid JWT verification, using JWK as primary and Legacy Secret as fallback.
147+
* Use only during 'New JWT Keys' migration period, while `JWT_SECRET` is still available.
148+
*/
149+
export async function verifyHybridJWT(jwtSecret: string, jwksUrl: string, jwt: string): Promise<boolean> {
150+
const { alg: jwtAlgorithm } = jose.decodeProtectedHeader(jwt)
151+
152+
if (jwtAlgorithm === 'HS256') {
153+
console.log(`Legacy token type detected, attempting ${jwtAlgorithm} verification.`)
154+
155+
return await isValidLegacyJWT(jwtSecret, jwt)
156+
}
157+
158+
if (jwtAlgorithm === 'ES256' || jwtAlgorithm === 'RS256') {
159+
return await isValidJWT(jwksUrl, jwt)
160+
}
161+
162+
return false;
163+
}
164+
120165
// Ref: https://docs.deno.com/examples/checking_file_existence/
121166
async function shouldUsePackageJsonDiscovery({ entrypointPath, importMapPath }: FunctionConfig): Promise<boolean> {
122167
if (importMapPath) {
@@ -159,7 +204,7 @@ Deno.serve({
159204
if (req.method !== "OPTIONS" && functionsConfig[functionName].verifyJWT) {
160205
try {
161206
const token = getAuthToken(req);
162-
const isValidJWT = await verifyJWT(token);
207+
const isValidJWT = await verifyHybridJWT(JWT_SECRET, JWKS_ENDPOINT, token);
163208

164209
if (!isValidJWT) {
165210
return getResponse({ msg: "Invalid JWT" }, STATUS_CODE.Unauthorized);

0 commit comments

Comments
 (0)