|
1 | 1 | import { STATUS_CODE, STATUS_TEXT } from "https://deno.land/std/http/status.ts"; |
2 | 2 | import * as posix from "https://deno.land/std/path/posix/mod.ts"; |
3 | 3 |
|
4 | | -import * as jose from "https://deno.land/x/jose@v4.13.1/index.ts"; |
| 4 | +import * as jose from "jsr:@panva/jose@6"; |
5 | 5 |
|
6 | 6 | const SB_SPECIFIC_ERROR_CODE = { |
7 | 7 | BootError: |
@@ -29,8 +29,9 @@ const SB_SPECIFIC_ERROR_REASON = { |
29 | 29 | // OS stuff - we don't want to expose these to the functions. |
30 | 30 | const EXCLUDED_ENVS = ["HOME", "HOSTNAME", "PATH", "PWD"]; |
31 | 31 |
|
32 | | -const JWT_SECRET = Deno.env.get("SUPABASE_INTERNAL_JWT_SECRET")!; |
33 | 32 | 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")!) |
34 | 35 | const DEBUG = Deno.env.get("SUPABASE_INTERNAL_DEBUG") === "true"; |
35 | 36 | const FUNCTIONS_CONFIG_STRING = Deno.env.get( |
36 | 37 | "SUPABASE_INTERNAL_FUNCTIONS_CONFIG", |
@@ -105,18 +106,62 @@ function getAuthToken(req: Request) { |
105 | 106 | return token; |
106 | 107 | } |
107 | 108 |
|
108 | | -async function verifyJWT(jwt: string): Promise<boolean> { |
| 109 | +async function isValidLegacyJWT(jwtSecret: string, jwt: string): Promise<boolean> { |
109 | 110 | const encoder = new TextEncoder(); |
110 | | - const secretKey = encoder.encode(JWT_SECRET); |
| 111 | + const secretKey = encoder.encode(jwtSecret); |
111 | 112 | try { |
112 | 113 | await jose.jwtVerify(jwt, secretKey); |
113 | 114 | } 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); |
115 | 140 | return false; |
116 | 141 | } |
117 | 142 | return true; |
118 | 143 | } |
119 | 144 |
|
| 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 | + |
120 | 165 | // Ref: https://docs.deno.com/examples/checking_file_existence/ |
121 | 166 | async function shouldUsePackageJsonDiscovery({ entrypointPath, importMapPath }: FunctionConfig): Promise<boolean> { |
122 | 167 | if (importMapPath) { |
@@ -159,7 +204,7 @@ Deno.serve({ |
159 | 204 | if (req.method !== "OPTIONS" && functionsConfig[functionName].verifyJWT) { |
160 | 205 | try { |
161 | 206 | const token = getAuthToken(req); |
162 | | - const isValidJWT = await verifyJWT(token); |
| 207 | + const isValidJWT = await verifyHybridJWT(JWT_SECRET, JWKS_ENDPOINT, token); |
163 | 208 |
|
164 | 209 | if (!isValidJWT) { |
165 | 210 | return getResponse({ msg: "Invalid JWT" }, STATUS_CODE.Unauthorized); |
|
0 commit comments