-
Notifications
You must be signed in to change notification settings - Fork 465
feat: add hybrid jwt verification #4721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
39176e3
00c30af
2258e81
55d59c9
4ab6860
964f3bc
067e8ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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,18 +106,53 @@ function getAuthToken(req: Request) { | |
| return token; | ||
| } | ||
|
|
||
| async function verifyJWT(jwt: string): Promise<boolean> { | ||
| async function isValidLegacyJWT(jwtSecret: string, jwt: string): Promise<boolean> { | ||
| 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<boolean> { | ||
| try { | ||
| if (!jwks) { | ||
| jwks = jose.createRemoteJWKSet(new URL(jwksUrl)); | ||
| } | ||
| await jose.jwtVerify(jwt, jwks); | ||
|
Comment on lines
+122
to
+137
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: wc -l internal/functions/serve/templates/main.tsRepository: supabase/cli Length of output: 102 🏁 Script executed: cat -n internal/functions/serve/templates/main.ts | head -150Repository: supabase/cli Length of output: 5779 🌐 Web query:
💡 Result: Key rotation behavior differences
Practical summary for rotation
Sources: Citations:
🏁 Script executed: cat -n internal/functions/serve/templates/main.ts | sed -n '131,165p'Repository: supabase/cli Length of output: 1384 🏁 Script executed: # Check if jwks variable is reassigned anywhere else in the file
rg "jwks\s*=" internal/functions/serve/templates/main.ts -nRepository: supabase/cli Length of output: 139 Don't freeze asymmetric verification to the startup JWKS snapshot. If 🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Not really true, the previous one also freeze the |
||
| } 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<boolean> { | ||
| 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<boolean> { | ||
| if (importMapPath) { | ||
|
|
@@ -159,7 +195,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); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.