Derive CORS allowed origins from DB, remove ALLOWED_ORIGINS env var#42
Conversation
pstaylor-patrick
left a comment
There was a problem hiding this comment.
Code Review Summary
This PR removes the ALLOWED_ORIGINS env var and derives CORS allowed origins directly from the oauth_client.allowed_origin DB column, with a 5-minute in-memory cache. The refactor consolidates duplicated CORS logic into a shared lib/cors.ts module and moves CORS handling from Edge middleware into route handlers (necessary because Edge Runtime cannot use node-postgres).
What works well:
- Clean extraction of duplicated CORS logic into a single module with clear separation between preflight handling and per-response header injection
- Smart dual-mode design: strict per-client validation when
clientIdis known, broad registered-origin check for preflight/error paths - Proper
Vary: Originheader included (previously missing in some paths) - Good cleanup of env var references across
apphosting.yaml,firebase-secrets.sh,.env.example, andadd-client.ts - Preflight correctly returns 204 (not 200)
Top risks:
- Module-level cache has a minor race condition on concurrent cache-miss (P1)
- Error responses on token/userinfo endpoints use broad origin check instead of per-client check, leaking that an origin is registered for some client (P4, P5)
pnpm-lock.yamladded -- unclear if intentional or accidental (P6)- Unrelated
plans/migration-plan.mdincluded in the diff (P7)
Findings:
- P1 [warning] -- Cache race condition on concurrent refresh
- P2 [warning] -- Silent deny on unknown origins could use a comment
- P3 [suggestion] -- Fast-path cache check before per-client DB query
- P4 [warning] -- Token error responses use broad origin check
- P5 [warning] -- Userinfo error responses use broad origin check
- P6 [suggestion] -- pnpm-lock.yaml may be unintended
- P7 [suggestion] -- Unrelated migration plan in PR
No blockers found. The CORS logic is correct and the security posture is improved over the previous state (which had a userinfo OPTIONS handler that allowed any origin). Good to merge after addressing the warnings.
Store the in-flight DB promise so concurrent cache-miss requests share a single query instead of racing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Skip the per-client DB lookup when the origin is not in any registered client's allowed set. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pass clientId to addCorsHeaders on all error paths where available, so error responses only get CORS headers when the origin matches the specific requesting client. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
clientId is unavailable until the access token is validated, so pre-validation errors necessarily use the all-origins check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Project uses npm (package-lock.json). Remove duplicate lockfile. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move to a separate PR to keep this diff focused. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pstaylor-patrick
left a comment
There was a problem hiding this comment.
Re-review: All 7 original findings addressed
Verdict: APPROVE (posted as COMMENT because GitHub blocks self-approval) -- all original findings (P1-P7) have been properly fixed. One new minor suggestion below.
Verification of original findings
| # | Finding | Fix commit | Status |
|---|---|---|---|
| P1 | Cache race on concurrent refresh | a6d682b | Verified: concurrent callers now share a single DB query via pendingRefresh |
| P2 | Silent deny needs comment | 1e2c999 | Verified: comment at line 58 explains intent |
| P3 | Fast-path cache check before per-client query | b26939a | Verified: isAllowedOrigin() guard at line 77 short-circuits before DB hit |
| P4 | Token error responses use broad origin check | 4278cf7 | Verified: all addCorsHeaders calls in token route now include clientId where available |
| P5 | Userinfo error responses use broad origin check | fa83bb6 | Verified: comment at lines 18-19 documents that clientId is unavailable pre-validation |
| P6 | pnpm-lock.yaml unintended | e3b5e3f | Verified: no longer in diff |
| P7 | Unrelated migration plan | 9a6265d | Verified: no longer in diff |
New finding (non-blocking)
P1 [suggestion] -- getAllowedOrigins() has no .catch() on the pending promise. A transient DB failure would permanently poison pendingRefresh until process restart. See inline comment for suggested fix.
Summary
The CORS refactor is clean and well-structured. The shared lib/cors.ts module correctly handles preflight, per-client strict validation, and broad origin checks. The Vary: Origin header is properly set. The middleware cleanup is correct. No blockers remain -- good to merge.
Without this, a transient DB failure would poison the cached promise and all subsequent calls would receive the rejected result. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
lib/cors.ts— shared CORS utility that queriesoauth_client.allowed_originfrom the DB with a 5-minute in-memory cache, replacing theALLOWED_ORIGINSenv vartoken,userinfo,authorize), since Next.js Edge Runtime middleware can't usenode-postgrestokenOPTIONS was missing CORS headers entirely,userinfoOPTIONS was allowing any originALLOWED_ORIGINSfromapphosting.yaml,firebase-secrets.sh,.env.example, andadd-client.tsnext-steps outputMotivation
Every time a new OAuth client was registered, the
ALLOWED_ORIGINSenv var had to be manually updated and redeployed. The DB (oauth_client.allowed_origin) already has the source of truth — now CORS origins are derived from it automatically.Test plan
npx tsc --noEmitpasses (pre-existing.nextcache error is unrelated)ALLOWED_ORIGINSis no longer referenced anywhere in codebase🤖 Generated with Claude Code