Skip to content
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

Enable JWT verification when serving all functions #1005

Merged
merged 9 commits into from
Apr 10, 2023
42 changes: 32 additions & 10 deletions internal/functions/serve/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package serve

import (
"context"
_ "embed"
"errors"
"fmt"
"os"
Expand All @@ -23,6 +24,11 @@ const (
customDockerImportMapPath = "/home/deno/import_map.json"
)

var (
//go:embed templates/main.ts
mainFuncEmbed string
)

func ParseEnvFile(envFilePath string, fsys afero.Fs) ([]string, error) {
env := []string{}
if len(envFilePath) == 0 {
Expand Down Expand Up @@ -251,6 +257,8 @@ func runServeAll(ctx context.Context, envFilePath string, noVerifyJWT *bool, imp
return nil
}

// TODO: Support per-function config before we default to using edge-runtime for
// serving individual functions.
func ServeFunctions(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, fsys afero.Fs) error {
// 1. Load default values
if envFilePath == "" {
Expand Down Expand Up @@ -278,6 +286,8 @@ func ServeFunctions(ctx context.Context, envFilePath string, noVerifyJWT *bool,
"SUPABASE_ANON_KEY=" + utils.Config.Auth.AnonKey,
"SUPABASE_SERVICE_ROLE_KEY=" + utils.Config.Auth.ServiceRoleKey,
"SUPABASE_DB_URL=postgresql://postgres:postgres@" + utils.DbId + ":5432/postgres",
"SUPABASE_INTERNAL_FUNCTIONS_PATH=" + relayFuncDir,
fmt.Sprintf("SUPABASE_INTERNAL_HOST_PORT=%d", utils.Config.Api.Port),
}
verifyJWTEnv := "VERIFY_JWT=true"
if noVerifyJWT != nil {
Expand All @@ -296,22 +306,34 @@ func ServeFunctions(ctx context.Context, envFilePath string, noVerifyJWT *bool,
dockerImportMapPath := relayFuncDir + "/import_map.json"
if importMapPath != "" {
binds = append(binds, filepath.Join(cwd, importMapPath)+":"+dockerImportMapPath+":ro,z")
env = append(env, "SUPABASE_INTERNAL_IMPORT_MAP_PATH="+dockerImportMapPath)
}

// 4. Start container
fmt.Println("Serving " + utils.Bold(utils.FunctionsDir))
cmd := []string{"start", "--dir", relayFuncDir, "-p", "8081"}
if importMapPath != "" {
cmd = append(cmd, "--import-map", dockerImportMapPath)
}
if viper.GetBool("DEBUG") {
cmd = append(cmd, "--verbose")
fmt.Println("Setting up Edge Functions runtime...")

var cmdString string
{
cmd := []string{"edge-runtime", "start", "--main-service", "/home/deno/main", "-p", "8081"}
if importMapPath != "" {
cmd = append(cmd, "--import-map", dockerImportMapPath)
}
if viper.GetBool("DEBUG") {
cmd = append(cmd, "--verbose")
}
cmdString = strings.Join(cmd, " ")
}

entrypoint := []string{"sh", "-c", `mkdir /home/deno/main && cat <<'EOF' > /home/deno/main/index.ts && ` + cmdString + `
` + mainFuncEmbed + `
EOF
`}
_, err = utils.DockerStart(
ctx,
container.Config{
Image: utils.EdgeRuntimeImage,
Env: append(env, userEnv...),
Cmd: cmd,
Image: utils.EdgeRuntimeImage,
Env: append(env, userEnv...),
Entrypoint: entrypoint,
},
start.WithSyslogConfig(container.HostConfig{
Binds: binds,
Expand Down
105 changes: 105 additions & 0 deletions internal/functions/serve/templates/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import * as jose from "https://deno.land/x/[email protected]/index.ts";

const JWT_SECRET = Deno.env.get("JWT_SECRET")!;
const VERIFY_JWT = Deno.env.get("VERIFY_JWT") === "true";
const HOST_PORT = Deno.env.get("SUPABASE_INTERNAL_HOST_PORT")!;
// OS stuff - we don't want to expose these to the functions.
const EXCLUDED_ENVS = ["HOME", "HOSTNAME", "PATH", "PWD"];
const FUNCTIONS_PATH = Deno.env.get("SUPABASE_INTERNAL_FUNCTIONS_PATH")!;
const IMPORT_MAP_PATH = Deno.env.get("SUPABASE_INTERNAL_IMPORT_MAP_PATH");

function getAuthToken(req: Request) {
const authHeader = req.headers.get("authorization");
if (!authHeader) {
throw new Error("Missing authorization header");
}
const [bearer, token] = authHeader.split(" ");
if (bearer !== "Bearer") {
throw new Error(`Auth header is not 'Bearer {token}'`);
}
return token;
}

async function verifyJWT(jwt: string): Promise<boolean> {
const encoder = new TextEncoder();
const secretKey = encoder.encode(JWT_SECRET);
try {
await jose.jwtVerify(jwt, secretKey);
} catch (err) {
console.error(err);
return false;
}
return true;
}

serve(async (req: Request) => {
if (req.method !== "OPTIONS" && VERIFY_JWT) {
try {
const token = getAuthToken(req);
const isValidJWT = await verifyJWT(token);

if (!isValidJWT) {
return new Response(
JSON.stringify({ msg: "Invalid JWT" }),
{ status: 401, headers: { "Content-Type": "application/json" } },
);
}
} catch (e) {
console.error(e);
return new Response(
JSON.stringify({ msg: e.toString() }),
{ status: 401, headers: { "Content-Type": "application/json" } },
);
}
}

const url = new URL(req.url);
const { pathname } = url;
const pathParts = pathname.split("/");
const serviceName = pathParts[1];

if (!serviceName || serviceName === "") {
const error = { msg: "missing function name in request" };
return new Response(
JSON.stringify(error),
{ status: 400, headers: { "Content-Type": "application/json" } },
);
}

const servicePath = `${FUNCTIONS_PATH}/${serviceName}`;
console.error(`serving the request with ${servicePath}`);

const memoryLimitMb = 150;
const workerTimeoutMs = 1 * 60 * 1000;
const noModuleCache = false;
const envVarsObj = Deno.env.toObject();
const envVars = Object.entries(envVarsObj)
.filter(([name, _]) =>
!EXCLUDED_ENVS.includes(name) && !name.startsWith("SUPABASE_INTERNAL_")
);
try {
const worker = await EdgeRuntime.userWorkers.create({
servicePath,
memoryLimitMb,
workerTimeoutMs,
noModuleCache,
importMapPath: IMPORT_MAP_PATH,
envVars,
});
return await worker.fetch(req);
} catch (e) {
console.error(e);
const error = { msg: e.toString() };
return new Response(
JSON.stringify(error),
{ status: 500, headers: { "Content-Type": "application/json" } },
);
}
}, {
onListen: () => {
console.log(
`Serving functions on http://localhost:${HOST_PORT}/functions/v1/<function-name>`,
);
},
});
2 changes: 1 addition & 1 deletion internal/utils/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const (
StudioImage = "supabase/studio:20230330-99fed3d"
DenoRelayImage = "supabase/deno-relay:v1.6.0"
ImageProxyImage = "darthsim/imgproxy:v3.8.0"
EdgeRuntimeImage = "supabase/edge-runtime:v1.1.7"
EdgeRuntimeImage = "supabase/edge-runtime:v1.2.12"
VectorImage = "timberio/vector:0.28.1-alpine"
// Update initial schemas in internal/utils/templates/initial_schemas when
// updating any one of these.
Expand Down