-
Notifications
You must be signed in to change notification settings - Fork 489
feat(functions): add hybrid JWT verification to TS edge runtime #5560
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
base: develop
Are you sure you want to change the base?
Changes from 2 commits
fc5d8d9
b7fdcbd
8d9a640
3ab48f1
f66460e
c83bda6
80e5620
a211129
d239180
c134206
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 |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // Minimal ambient typings for the Deno-resolved jose import used by | ||
| // edge-runtime-main.ts. The bun workspace type-checker cannot resolve the | ||
| // `jsr:` specifier, so we describe only the surface that runtime script relies | ||
| // on; jose itself is loaded at runtime inside the edge runtime. | ||
| declare module "jsr:@panva/jose@6" { | ||
| type JwksResolver = (...args: ReadonlyArray<unknown>) => Promise<CryptoKey>; | ||
| export function decodeProtectedHeader(token: string): { readonly alg?: string }; | ||
| export function jwtVerify(jwt: string, key: Uint8Array | JwksResolver): Promise<unknown>; | ||
| export function createLocalJWKSet(jwks: { readonly keys: ReadonlyArray<unknown> }): JwksResolver; | ||
| export function createRemoteJWKSet(url: URL): JwksResolver; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,11 @@ | |
| import { join } from "node:path"; | ||
| import { afterAll, beforeAll, describe, expect, test } from "vitest"; | ||
| import { createStack, type StackHandle } from "../src/node.ts"; | ||
| import { generateJwt } from "../src/JwtGenerator.ts"; | ||
| import { setupTestTable } from "./helpers/e2e.ts"; | ||
|
|
||
| const STACK_E2E_TEST_TIMEOUT_MS = 5_000; | ||
| const JWT_SECRET = "super-secret-jwt-token-with-at-least-32-characters-long"; | ||
|
|
||
| describe("createStack e2e", () => { | ||
| let stack: StackHandle; | ||
|
|
@@ -22,7 +24,7 @@ | |
| stack = await createStack({ | ||
| projectDir, | ||
| functions: { noVerifyJwt: true }, | ||
| jwtSecret: "super-secret-jwt-token-with-at-least-32-characters-long", | ||
| jwtSecret: JWT_SECRET, | ||
| postgres: { dataDir }, | ||
| }); | ||
|
|
||
|
|
@@ -94,6 +96,33 @@ | |
| expect(await res.text()).toBe("later"); | ||
| }); | ||
|
|
||
| test("enforces hybrid JWT verification on Edge Functions", { timeout: 20_000 }, async () => { | ||
|
Contributor
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. This e2e name says “hybrid JWT verification,” but the scenario only covers missing credentials, forged HS256, valid HS256, and the |
||
| writeFunction(projectDir, "secure", "secure"); | ||
| // verify_jwt defaults to true, so this turns verification back on. | ||
| await stack.reloadFunctions(); | ||
|
avallete marked this conversation as resolved.
Outdated
|
||
| const url = `${stack.url}/functions/v1/secure`; | ||
|
|
||
| // Missing credentials are rejected before reaching the function. | ||
| const missing = await fetch(url); | ||
| expect(missing.status).toBe(401); | ||
|
Check failure on line 107 in packages/stack/tests/createStack.e2e.test.ts
|
||
|
|
||
| // A well-formed token signed with the wrong secret fails signature checks. | ||
| const forged = generateJwt("a-different-secret-at-least-32-characters-long", "anon"); | ||
| const forgedRes = await fetch(url, { headers: { Authorization: `Bearer ${forged}` } }); | ||
| expect(forgedRes.status).toBe(401); | ||
|
|
||
| // A valid HS256 token is accepted via the legacy/hybrid path. | ||
| const valid = generateJwt(JWT_SECRET, "anon"); | ||
| const bearerRes = await fetch(url, { headers: { Authorization: `Bearer ${valid}` } }); | ||
| expect(bearerRes.status).toBe(200); | ||
| expect(await bearerRes.text()).toBe("secure"); | ||
|
|
||
| // The apikey -> minted sb-api-key compatibility path is accepted too. | ||
| const apiKeyRes = await fetch(url, { headers: { apikey: stack.publishableKey } }); | ||
| expect(apiKeyRes.status).toBe(200); | ||
| expect(await apiKeyRes.text()).toBe("secure"); | ||
| }); | ||
|
|
||
| test( | ||
| "supports the auth signup and session golden path", | ||
| { timeout: STACK_E2E_TEST_TIMEOUT_MS }, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For projects that configure asymmetric auth inputs such as
auth.signing_keys_pathorauth.third_party(both are loaded by@supabase/config), this writes a JWKS derived only from the legacyjwtSecret. As a result, ES256/RS256 tokens signed by the configured local signing key or third-party provider are absent fromSUPABASE_JWKS; the fallback only asks the local GoTrue JWKS endpoint, not the configured provider/file, so Edge Functions reject tokens that the hybrid verifier is meant to support. Resolve the JWKS from the loaded project auth config rather than always callinggenerateJwks(stackConfig.jwtSecret).Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving this one open as a known limitation rather than fixing it in this PR, because the local stack doesn't yet support asymmetric signing locally:
StackBuilderpassesversion,port,siteUrl,jwtExpiry, andexternalUrlto the auth service, but notauth.signing_keys_pathorauth.third_party. So local GoTrue mints HS256 tokens signed withjwtSecret, andgenerateJwks(stackConfig.jwtSecret)is the correct local key set for them.signing_keys_path/third_partykeys intoSUPABASE_JWKSwouldn't match any locally-minted token today./auth/v1/.well-known/jwks.jsonfallback for keys the local auth service is aware of.Properly sourcing
SUPABASE_JWKSfrom the configured signing keys / third-party providers requires first plumbing those into the local auth service so it actually signs with them — that's a larger, separate change. Happy to file a follow-up issue if you'd like to track it.Generated by Claude Code