Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions apps/cli-go/internal/functions/serve/templates/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,40 @@ const functionsConfig: Record<string, FunctionConfig> = (() => {
}
})();

/* --- JWT verification --- */
export function extractBearerToken(rawToken: string) {
const tokenParts = rawToken.split(' ')
const [bearer, token] = tokenParts
if (bearer !== 'Bearer' || tokenParts.length !== 2) {
return null
}

return token
}

function getAuthToken(req: Request) {
const authHeader = req.headers.get("authorization");
if (!authHeader) {
const sbApiKeyCompatibilityToken = req.headers.get("sb-api-key")

// NOTE:(kallebysantos) Kong on legacy CLI stack pass it down as 'Bearer Token' format
const cleanSbApiKeyCompatibilityToken = sbApiKeyCompatibilityToken.replace('Bearer', '').trim()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick

sbApiKeyCompatibilityToken is string | null (headers.get returns null when absent), so .replace(...) throws if sb-api-key is missing. Masked today only because Kong always sets the header on this route but a request reaching the runtime without it (direct-to-runtime, tests) would 500. A ?? guard would match the ingress behavior.


if (!authHeader && !cleanSbApiKeyCompatibilityToken) {
throw new Error("Missing authorization header");
}
const [bearer, token] = authHeader.split(" ");
if (bearer !== "Bearer") {

// NOTE:(kallebysantos) Compatibility mode is triggered when all conditions match:
// - API proxy mints a temp token
// - Original bearer is not present or is ApiKey
const bearerToken = extractBearerToken(authHeader ?? '')
const token = (!bearerToken || bearerToken.startsWith('sb_'))
? cleanSbApiKeyCompatibilityToken
: bearerToken

if (!token) {
throw new Error(`Auth header is not 'Bearer {token}'`);
}

return token;
}

Expand Down Expand Up @@ -180,6 +205,19 @@ async function shouldUsePackageJsonDiscovery({ entrypointPath, importMapPath }:
return true
}

export function prepareUserRequest(req: Request): Request {
const clonedURL = new URL(req.url)
const forwardedHost = req.headers.get('x-forwarded-host')
clonedURL.hostname = forwardedHost ?? clonedURL.hostname
const clonedReq = new Request(clonedURL, req.clone())

// remove custom api headers
clonedReq.headers.delete('sb-api-key')
EdgeRuntime.applySupabaseTag(req, clonedReq)

return clonedReq
}

Deno.serve({
handler: async (req: Request) => {
const url = new URL(req.url);
Expand Down Expand Up @@ -279,7 +317,8 @@ Deno.serve({
staticPatterns,
});

return await worker.fetch(req);
const userReq = prepareUserRequest(req)
return await worker.fetch(userReq);
} catch (e) {
console.error(e);

Expand Down
4 changes: 2 additions & 2 deletions apps/cli-go/internal/start/templates/kong.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,10 @@ services:
config:
add:
headers:
- "Authorization: {{ .BearerToken }}"
- "sb-api-key: {{ .BearerToken }}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remark(not-blocking):

this mints on every /functions request regardless of verify_jwt. Harmless now that it goes to sb-api-key and is stripped before the worker (and only read when verify_jwt is on), but it's wider than necessary could scope it later. (Underlying expression is in https://github.com/supabase/cli/blob/develop/apps/cli-go/internal/start/start.go#L451-L463 )

replace:
headers:
- "Authorization: {{ .BearerToken }}"
- "sb-api-key: {{ .BearerToken }}"
# Management API endpoints
- name: well-known-oauth
_comment: "GoTrue: /.well-known/oauth-authorization-server -> http://auth:9999/.well-known/oauth-authorization-server"
Expand Down
19 changes: 14 additions & 5 deletions packages/stack/src/ApiProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,29 @@ export interface ProxyConfig {
readonly serviceRoleJwt: string;
}

function transformAuthorization(headers: Headers.Headers, config: ProxyConfig): Headers.Headers {
function transformAuthorization(
headers: Headers.Headers,
config: ProxyConfig,
useCustomHeader = false,
): Headers.Headers {
const auth = headers["authorization"];
const apikey = headers["apikey"];

const transformHeaderName = useCustomHeader ? "sb-api-key" : "authorization";
const transformPrefix = useCustomHeader ? "" : "Bearer ";

if (auth !== undefined && !auth.startsWith("Bearer sb_")) {
return headers;
}

if (apikey === config.publishableKey) {
return Headers.set(headers, "authorization", `Bearer ${config.anonJwt}`);
return Headers.set(headers, transformHeaderName, transformPrefix + config.anonJwt);
}
if (apikey === config.secretKey) {
return Headers.set(headers, "authorization", `Bearer ${config.serviceRoleJwt}`);
return Headers.set(headers, transformHeaderName, transformPrefix + config.serviceRoleJwt);
}
if (apikey !== undefined && apikey !== "") {
return Headers.set(headers, "authorization", apikey);
return Headers.set(headers, transformHeaderName, apikey);
}

return headers;
Expand Down Expand Up @@ -103,6 +110,7 @@ interface ProxyHandlerOptions {
readonly stripPrefix?: string;
readonly backendPath?: string;
readonly transformAuth?: boolean;
readonly transformAuthCustomHeader?: boolean;
readonly extraHeaders?: Record<string, string>;
}

Expand All @@ -126,7 +134,7 @@ function makeProxyHandler(

let outHeaders = req.headers;
if (opts.transformAuth === true) {
outHeaders = transformAuthorization(outHeaders, config);
outHeaders = transformAuthorization(outHeaders, config, opts.transformAuthCustomHeader);
}
outHeaders = addProxyHeaders(outHeaders, Option.getOrUndefined(req.remoteAddress));

Expand Down Expand Up @@ -254,6 +262,7 @@ export class ApiProxy extends Context.Service<
backendPort: config.edgeRuntimePort,
stripPrefix: "/functions/v1",
transformAuth: true,
transformAuthCustomHeader: true,
}),
),
HttpRouter.route(
Expand Down
27 changes: 21 additions & 6 deletions packages/stack/src/ApiProxy.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,13 +223,28 @@ describe("ApiProxy", () => {
expect(body.path).toBe("/users");
});

test("/functions/v1/test strips prefix and transforms auth", async () => {
const res = await fetch(`${proxyUrl}/functions/v1/test`, {
headers: { apikey: SECRET_KEY },
describe("/functions/v1/ test strips prefix and transforms auth", () => {
test("transforms to custom header", async () => {
const res = await fetch(`${proxyUrl}/functions/v1/test`, {
headers: { apikey: SECRET_KEY },
});
const body = (await res.json()) as { path: string; headers: Record<string, string> };
expect(body.path).toBe("/test");
expect(body.headers["sb-api-key"]).toBe(SERVICE_ROLE_JWT);
});

test("transforms to custom header without replacing original auth", async () => {
const res = await fetch(`${proxyUrl}/functions/v1/test`, {
headers: {
apikey: SECRET_KEY,
authorization: `Bearer ${SECRET_KEY}`,
},
});
const body = (await res.json()) as { path: string; headers: Record<string, string> };
expect(body.path).toBe("/test");
expect(body.headers["authorization"]).toBe(`Bearer ${SECRET_KEY}`);
expect(body.headers["sb-api-key"]).toBe(SERVICE_ROLE_JWT);
});
const body = (await res.json()) as { path: string; headers: Record<string, string> };
expect(body.path).toBe("/test");
expect(body.headers["authorization"]).toBe(`Bearer ${SERVICE_ROLE_JWT}`);
});

test("strips upstream content-encoding metadata from proxied function responses", async () => {
Expand Down
Loading