Skip to content

Protect real RAG search and answer APIs with authenticated owner scope#22

Merged
BigSimmo merged 7 commits into
mainfrom
fix/private-rag-api-auth
Jun 13, 2026
Merged

Protect real RAG search and answer APIs with authenticated owner scope#22
BigSimmo merged 7 commits into
mainfrom
fix/private-rag-api-auth

Conversation

@BigSimmo

Copy link
Copy Markdown
Owner

Summary

  • Require an authenticated user before real-mode RAG search, answer, and answer-stream requests can retrieve or generate from indexed content.
  • Pass the authenticated owner ID through search, related-document retrieval, answer generation, search coalescing, and touched search telemetry.
  • Preserve demo-mode anonymous behaviour.
  • Add focused regression tests for anonymous rejection, owner scoping, and demo-mode compatibility.

Issues addressed

  • PLAN-001: auth-gate and owner-scope real-mode /api/search
  • PLAN-002: auth-gate and owner-scope real-mode /api/answer and /api/answer/stream
  • Partial PLAN-004: owner-scope touched search telemetry only

Files changed

File Reason
src/app/api/search/route.ts Require an authenticated owner in real mode, pass ownerId into retrieval/related-document lookup, scope coalescing by owner, and write touched telemetry with owner ID.
src/app/api/answer/route.ts Require an authenticated owner in real mode and pass ownerId into answer generation.
src/app/api/answer/stream/route.ts Require an authenticated owner before opening the real-mode answer stream and pass ownerId into streamed answer generation.
tests/private-rag-access.test.ts Add regression coverage for anonymous rejection, authenticated owner propagation, and demo-mode anonymous compatibility.

Behaviour changes

  • Anonymous real-mode /api/search requests now return 401 before retrieval.
  • Anonymous real-mode /api/answer requests now return 401 before generation.
  • Anonymous real-mode /api/answer/stream requests now return 401 before stream retrieval/generation starts.
  • Authenticated real-mode search and answer requests are scoped to the authenticated user ID.
  • Demo-mode search and answer behaviour remains anonymous.

Tests added or updated

  • Added tests/private-rag-access.test.ts covering:
    • unauthenticated real search rejection
    • authenticated real search owner scoping
    • demo search remaining anonymous
    • unauthenticated real answer rejection
    • authenticated real answer owner scoping
    • demo answer remaining anonymous
    • unauthenticated real answer-stream rejection
    • authenticated real answer-stream owner scoping

Checks run

Command Result Notes
npm run lint Not run No local checkout/runtime is available through the GitHub connector.
npm run typecheck Not run No local checkout/runtime is available through the GitHub connector.
npm run test Not run No local checkout/runtime is available through the GitHub connector.
npm run build Not run No local checkout/runtime is available through the GitHub connector.

Manual QA

Check Result Notes
Anonymous real-mode API calls return 401 Not run Requires a running local app or deployed preview. Reviewer steps below.
Signed-in dashboard search and answer still work Not run Requires browser/runtime access and Supabase auth.
Demo mode remains anonymous Not run Requires runtime access.
No auth headers/tokens/secrets in logs Not run Requires runtime/log access.

Suggested reviewer steps:

  1. In real non-demo mode, call POST /api/search without auth and expect 401.
  2. Call POST /api/answer without auth and expect 401.
  3. Call POST /api/answer/stream without auth and expect 401 before SSE retrieval starts.
  4. Sign in and confirm dashboard search and answer stream still work.
  5. Enable demo mode and confirm search/answer still work without sign-in.

Risk and rollback

Risk:

  • Medium-high. This intentionally changes real-mode RAG API access from anonymous to authenticated and may affect external callers or scripts that call these endpoints without auth.

Rollback:

  • Revert this PR. No migrations, package changes, cache changes, or config changes were introduced.

Out of scope

  • SQL/RPC guard against future unscoped RAG calls
  • Broad telemetry redaction redesign
  • Upload validation or streaming hardening
  • Dialog accessibility
  • Invalid document page query handling
  • API DTO redaction
  • UI/CSS/design-system changes
  • Large component refactors
  • Dependency or package updates
  • CI workflow changes

Notes for reviewer

  • The branch was created and edited through the GitHub connector, not a local checkout, so local npm checks could not be run here.
  • scripts/eval-search-api.ts currently calls /api/search without an auth header; after this PR, real non-demo usage of that script may need an auth/local-no-auth follow-up if it is still used against private data.
  • The streaming route returns a plain JSON 401 before opening the stream for unauthenticated real-mode requests, matching the dashboard's existing pre-stream response.status === 401 handling.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 971ebe4bed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +578 to +579
const user = await serverAuth.requireAuthenticatedUser(request, supabase);
ownerId = user.id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update existing unauthenticated route tests

Adding this auth gate changes the real-mode contract, but the existing tests/private-access-routes.test.ts suite still calls /api/search, /api/answer, and /api/answer/stream without an Authorization header and asserts successful unauthenticated behavior (for example the tests around allows unauthenticated search and answer without owner scope, public rate limiting, coalescing, and streaming without auth). With this line in place those requests return 401 before the mocked RAG functions run, so the normal npm test suite will fail unless those tests are updated or converted to demo/local-no-auth/authenticated scenarios.

Useful? React with 👍 / 👎.

Comment on lines +577 to +579
supabase = createAdminClient();
const user = await serverAuth.requireAuthenticatedUser(request, supabase);
ownerId = user.id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass auth when running API search evals

This new real-mode auth requirement also breaks the existing npm run eval:search:api workflow outside demo/local-no-auth mode: scripts/eval-search-api.ts posts to /api/search with only Content-Type, so every eval case now receives 401 and --fail-on-threshold will fail before measuring retrieval quality. The API eval script needs a way to supply a bearer token or otherwise opt into the same authenticated/local owner flow as the other RAG eval scripts.

Useful? React with 👍 / 👎.

Comment on lines +577 to +579
supabase = createAdminClient();
const user = await serverAuth.requireAuthenticatedUser(request, supabase);
ownerId = user.id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate real-mode search on auth in the UI

After this auth check, the dashboard still enables the main search button in real mode whenever setup is ready because ClinicalDashboard passes realDataReady={canRunSearch} and canRunSearch does not include canUsePrivateApis. For an unsigned-in user with a configured real backend, submitting a search now just calls these endpoints without credentials, marks the session expired, and shows a 401 error instead of disabling the action or requiring sign-in first.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae29521c57

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return (
process.env.RAG_EVAL_API_AUTH_TOKEN?.trim() ||
process.env.RAG_EVAL_AUTH_TOKEN?.trim() ||
process.env.SUPABASE_ACCESS_TOKEN?.trim() ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid forwarding Supabase CLI tokens to the app

When a developer has the Supabase CLI configured in CI/local, SUPABASE_ACCESS_TOKEN is a personal access token for the Management API, not an Auth user JWT (Supabase docs describe it as a personal access token). This script now silently forwards that long-lived token as Authorization: Bearer to whichever --base-url is used and suppresses the no-token diagnostic, so real-mode evals still 401 while potentially exposing a management token to a preview/non-local URL. Please only auto-read the RAG-specific env vars/explicit flag, or otherwise require a real user session JWT.

Useful? React with 👍 / 👎.

}

supabase = createAdminClient();
const user = await serverAuth.requireAuthenticatedUser(request, supabase);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply rate limiting before auth validation

For requests that include any invalid Authorization: Bearer ... header, this call goes out to Supabase Auth and throws before the per-IP limiter at lines 581-582 is consumed, so those requests are never counted; the same auth-before-limit order was added to /api/answer and /api/answer/stream. A client can therefore send unlimited invalid-token attempts and force external auth validation on each request instead of receiving 429s from this endpoint. Move the cheap limiter before token validation, or add a separate auth-failure limiter, so authentication failures are throttled too.

Useful? React with 👍 / 👎.

@BigSimmo
BigSimmo merged commit fc07eb9 into main Jun 13, 2026
2 of 4 checks passed
@BigSimmo
BigSimmo deleted the fix/private-rag-api-auth branch June 13, 2026 07:55
Copilot AI added a commit that referenced this pull request Jul 22, 2026
BigSimmo added a commit that referenced this pull request Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant