Lock down OAuth used codes table (#641)
* Lock down OAuth used codes table * test(db): lock oauth_used_codes lockdown contract + reload PostgREST cache Add a pg-real test asserting anon/authenticated are denied SELECT/INSERT on public.oauth_used_codes while the privileged (service-role) connection can still read it, per the project's requirement that RLS changes ship a *.pg.test.ts. Also append NOTIFY pgrst, 'reload schema' so PostgREST picks up the privilege change immediately (migration rule 8). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
5f79a74a2e
commit
5aa449ad3c
@@ -0,0 +1,9 @@
|
||||
-- OAuth authorization-code replay tracking is internal-only. The token
|
||||
-- endpoint uses a service-role client, so browser-facing API roles must not
|
||||
-- access this table.
|
||||
ALTER TABLE public.oauth_used_codes ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
REVOKE ALL ON TABLE public.oauth_used_codes FROM anon, authenticated;
|
||||
|
||||
-- Privilege change: tell PostgREST to reload its schema/role cache.
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getClient } from '@/tests/pg/setup'
|
||||
|
||||
/**
|
||||
* Migration 20260602145425_lock_down_oauth_used_codes.sql enables RLS on
|
||||
* public.oauth_used_codes and REVOKEs all privileges from the browser-facing
|
||||
* `anon` and `authenticated` roles. The OAuth token endpoint reaches this
|
||||
* replay-tracking table only through a service-role client (which bypasses
|
||||
* both RLS and the revoke), so the API roles must be denied. These tests lock
|
||||
* that contract in so a future migration cannot silently re-expose the table.
|
||||
*/
|
||||
describe('oauth_used_codes lockdown (pg)', () => {
|
||||
async function expectDenied(role: 'anon' | 'authenticated', sql: string) {
|
||||
const client = await getClient()
|
||||
try {
|
||||
await client.query('BEGIN')
|
||||
await client.query(`SET LOCAL ROLE ${role}`)
|
||||
await expect(client.query(sql)).rejects.toThrow(/permission denied/i)
|
||||
} finally {
|
||||
await client.query('ROLLBACK').catch(() => {})
|
||||
client.release()
|
||||
}
|
||||
}
|
||||
|
||||
it('denies SELECT to the anon role', async () => {
|
||||
await expectDenied('anon', 'SELECT * FROM public.oauth_used_codes LIMIT 1')
|
||||
})
|
||||
|
||||
it('denies SELECT to the authenticated role', async () => {
|
||||
await expectDenied('authenticated', 'SELECT * FROM public.oauth_used_codes LIMIT 1')
|
||||
})
|
||||
|
||||
it('denies INSERT to the authenticated role', async () => {
|
||||
await expectDenied(
|
||||
'authenticated',
|
||||
`INSERT INTO public.oauth_used_codes (code_hash) VALUES ('lockdown-test')`,
|
||||
)
|
||||
})
|
||||
|
||||
it('still allows the privileged (service-role / owner) connection to read', async () => {
|
||||
// The app reaches this table through a service-role client, which bypasses
|
||||
// RLS and the anon/authenticated revoke. Model that with the default
|
||||
// privileged pg connection (table owner) the pg-real harness uses.
|
||||
const client = await getClient()
|
||||
try {
|
||||
const res = await client.query<{ n: number }>(
|
||||
'SELECT count(*)::int AS n FROM public.oauth_used_codes',
|
||||
)
|
||||
expect(res.rows[0]!.n).toBeGreaterThanOrEqual(0)
|
||||
} finally {
|
||||
client.release()
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user