Skip to content

Commit aaf21e4

Browse files
Yostracursoragent
andcommitted
fix: use correct supabase auth
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 58b4848 commit aaf21e4

3 files changed

Lines changed: 139 additions & 0 deletions

File tree

packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { runMigrationsFromContent } from '../../database/migrate.ts'
33
import { VERSION } from '../../version.ts'
44
import { embeddedMigrations } from '../../database/migrations-embedded.ts'
55
import { parseSchemaComment } from '../schemaComment.ts'
6+
import { authenticateWithSupabase } from '../managementAuth.ts'
67
import postgres from 'postgres'
78

89
// Name of the vault secret used to authenticate callers of this function.
@@ -123,6 +124,11 @@ Deno.serve(async (req) => {
123124
}
124125
}
125126

127+
const supabaseAuthError = await authenticateWithSupabase(MGMT_API_BASE, projectRef, accessToken)
128+
if (supabaseAuthError) {
129+
return new Response('Unauthorized', { status: 401 })
130+
}
131+
126132
// Handle GET requests for status
127133
if (req.method === 'GET') {
128134
const dbUrl = Deno.env.get('SUPABASE_DB_URL')
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Verifies a management access token can operate on a specific project, mirroring
2+
// how the install script targets the project ref directly via the Management API.
3+
4+
export async function canAccessProject(
5+
mgmtApiBase: string,
6+
projectRef: string,
7+
accessToken: string
8+
): Promise<boolean> {
9+
const response = await fetch(`${mgmtApiBase}/v1/projects/${projectRef}`, {
10+
method: 'GET',
11+
headers: {
12+
Authorization: `Bearer ${accessToken}`,
13+
'Content-Type': 'application/json',
14+
},
15+
})
16+
return response.ok
17+
}
18+
19+
// Fallback for branch deployments, whose ref is exposed via the branches endpoint
20+
// rather than /v1/projects/{ref}.
21+
export async function canAccessBranch(
22+
mgmtApiBase: string,
23+
projectRef: string,
24+
accessToken: string
25+
): Promise<boolean> {
26+
const response = await fetch(`${mgmtApiBase}/v1/branches/${projectRef}`, {
27+
method: 'GET',
28+
headers: {
29+
Authorization: `Bearer ${accessToken}`,
30+
'Content-Type': 'application/json',
31+
},
32+
})
33+
return response.ok
34+
}
35+
36+
// Returns null when the token may operate on projectRef, otherwise an error string.
37+
export async function authenticateWithSupabase(
38+
mgmtApiBase: string,
39+
projectRef: string,
40+
accessToken: string | null
41+
): Promise<string | null> {
42+
if (!accessToken) {
43+
return 'Unauthorized: Invalid credentials'
44+
}
45+
46+
if (await canAccessProject(mgmtApiBase, projectRef, accessToken)) {
47+
return null
48+
}
49+
50+
if (await canAccessBranch(mgmtApiBase, projectRef, accessToken)) {
51+
return null
52+
}
53+
54+
return 'Unauthorized: Invalid credentials'
55+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { describe, it, expect, afterEach, vi } from 'vitest'
2+
import { authenticateWithSupabase } from '../../supabase/managementAuth'
3+
4+
const MGMT_API_BASE = 'https://api.supabase.com'
5+
const PROJECT_REF = 'lgiudgtkdioaxbczoqhq'
6+
const VALID_TOKEN = 'sbp_valid_token'
7+
8+
// Stubs global fetch so response.ok is driven by the requested URL.
9+
function mockFetchByUrl(isOk: (url: string) => boolean) {
10+
const mockFetch = vi.fn().mockImplementation((url: string) => Promise.resolve({ ok: isOk(url) }))
11+
global.fetch = mockFetch
12+
return mockFetch
13+
}
14+
15+
describe('authenticateWithSupabase', () => {
16+
afterEach(() => {
17+
vi.restoreAllMocks()
18+
})
19+
20+
it('rejects a missing token without calling the management API', async () => {
21+
const mockFetch = mockFetchByUrl(() => true)
22+
23+
const error = await authenticateWithSupabase(MGMT_API_BASE, PROJECT_REF, null)
24+
25+
expect(error).toBe('Unauthorized: Invalid credentials')
26+
expect(mockFetch).not.toHaveBeenCalled()
27+
})
28+
29+
it('rejects a fabricated token (the reported attack)', async () => {
30+
// A bogus token gets a 401 from the management API, so response.ok is false.
31+
const mockFetch = mockFetchByUrl(() => false)
32+
33+
const error = await authenticateWithSupabase(MGMT_API_BASE, PROJECT_REF, 'fake_attacker_token')
34+
35+
expect(error).toBe('Unauthorized: Invalid credentials')
36+
// Both the project and branch checks are attempted before rejecting.
37+
expect(mockFetch).toHaveBeenCalledTimes(2)
38+
})
39+
40+
it('accepts a token that can access the project', async () => {
41+
mockFetchByUrl((url) => url.endsWith(`/v1/projects/${PROJECT_REF}`))
42+
43+
const error = await authenticateWithSupabase(MGMT_API_BASE, PROJECT_REF, VALID_TOKEN)
44+
45+
expect(error).toBeNull()
46+
})
47+
48+
it('accepts a branch ref reachable via /v1/branches as a fallback', async () => {
49+
const mockFetch = mockFetchByUrl((url) => url.includes('/v1/branches/'))
50+
51+
const error = await authenticateWithSupabase(MGMT_API_BASE, PROJECT_REF, VALID_TOKEN)
52+
53+
expect(error).toBeNull()
54+
// Direct project check ran first and failed, then the branch check passed.
55+
expect(mockFetch).toHaveBeenCalledTimes(2)
56+
expect(mockFetch).toHaveBeenLastCalledWith(
57+
`${MGMT_API_BASE}/v1/branches/${PROJECT_REF}`,
58+
expect.objectContaining({
59+
method: 'GET',
60+
headers: expect.objectContaining({ Authorization: `Bearer ${VALID_TOKEN}` }),
61+
})
62+
)
63+
})
64+
65+
it('queries the exact project ref with the bearer token', async () => {
66+
const mockFetch = mockFetchByUrl(() => true)
67+
68+
await authenticateWithSupabase(MGMT_API_BASE, PROJECT_REF, VALID_TOKEN)
69+
70+
expect(mockFetch).toHaveBeenCalledWith(
71+
`${MGMT_API_BASE}/v1/projects/${PROJECT_REF}`,
72+
expect.objectContaining({
73+
method: 'GET',
74+
headers: expect.objectContaining({ Authorization: `Bearer ${VALID_TOKEN}` }),
75+
})
76+
)
77+
})
78+
})

0 commit comments

Comments
 (0)