-
Notifications
You must be signed in to change notification settings - Fork 0
feat(auth): multi-user — persistent sessions, password/SSO signup, fail-closed isolation #252
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
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
dafaa85
feat(auth): persistent cookie sessions via @supabase/ssr (Phase 2)
BigSimmo 0f94aa8
feat(auth): password + Google/Microsoft SSO + signup UI (Phase 3)
BigSimmo 3a3c7f3
feat(auth): fail-closed owner scoping for retrieval (Phase 4)
BigSimmo e59650d
docs(auth): Supabase multi-user config checklist (Phase 1, user-applied)
BigSimmo a88d8d4
docs(auth): DB owner-RLS + private storage already in place (verified…
BigSimmo eebebd5
style(docs): prettier multi-user-auth-setup
BigSimmo c55674c
docs(auth): correct order-of-operations note (RLS already in place)
BigSimmo c25143c
docs(auth): add concrete project URL + publishable key to Phase 1 che…
BigSimmo 5f05d36
fix(auth): surface auth callback error after async session load
BigSimmo 7372664
Merge branch 'main' into claude/multiuser-auth
BigSimmo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| # Multi-user auth — Supabase configuration checklist (you apply) | ||
|
|
||
| The **code** for multi-user (persistent cookie sessions, magic link + password + | ||
| SSO, per-user isolation) lands via the `claude/multiuser-auth` branch. The | ||
| **live Supabase configuration** below is done by you in the dashboard / provider | ||
| consoles — Claude does not change the live Auth config. Target project: | ||
| `Clinical KB Database` (`sjrfecxgysukkwxsowpy`). | ||
|
|
||
| > **Order matters:** do **not** enable open signup on live until the fail-closed | ||
| > owner-scoping hardening on this branch has merged (the DB owner-RLS + private | ||
| > storage backstop is already in place — see §7). Validate the whole flow in a | ||
| > **staging** project first. | ||
|
|
||
| ## 1. Auth → Providers | ||
|
|
||
| - **Email**: enable **Confirm email** (verifies ownership; blocks throwaway | ||
| signups). Enable **Email OTP** (magic link — already used) **and** **Password**. | ||
| - **Google**: create an OAuth client in Google Cloud Console → add the Supabase | ||
| callback `https://sjrfecxgysukkwxsowpy.supabase.co/auth/v1/callback` as an | ||
| authorized redirect URI → paste client ID/secret into Supabase → enable. | ||
| - **Azure (Microsoft)**: register an app in Azure AD (Entra ID) with the same | ||
| Supabase callback as a redirect URI → paste client ID/secret + tenant → | ||
| enable the **Azure** provider. | ||
|
|
||
| ## 2. Auth → Sign in / Providers → "Allow new users to sign up" | ||
|
|
||
| - Turn **ON** (open public signup, per decision). Each new account starts as an | ||
| empty private silo — a new user cannot see anyone else's data. | ||
|
|
||
| ## 3. Auth → URL Configuration | ||
|
|
||
| - **Site URL**: the production origin (e.g. `https://app.example.com`). | ||
| - **Redirect URLs** (allowlist): add the app's callback for every environment: | ||
| - `https://app.example.com/auth/callback` | ||
| - `http://localhost:<port>/auth/callback` (local dev) | ||
| - the app routes magic link, OAuth, and confirmation returns through | ||
| `/auth/callback` (see `src/app/auth/callback/route.ts`). | ||
|
|
||
| ## 4. Auth → SMTP (production email) | ||
|
|
||
| - Configure **custom SMTP** (Resend / SendGrid / SES / Postmark). The built-in | ||
| Supabase email is dev-only (~a few/hour) and will bottleneck magic-link + | ||
| confirmation mail for real users. | ||
|
|
||
| ## 5. Auth → Attack protection (recommended for open signup) | ||
|
|
||
| - Enable **CAPTCHA** (hCaptcha or Cloudflare Turnstile) to stop bot signups. | ||
| - Keep the default Auth **rate limits**. | ||
| - **Cost note:** every signed-in user can drive OpenAI / RAG spend — budget for | ||
| it and consider per-owner rate limits (the app already has `consumeApiRateLimit` | ||
| buckets keyed by owner). | ||
|
|
||
| ## 6. App environment variables | ||
|
|
||
| Already used by the app; ensure they are set per environment. Concrete values | ||
| for **this** project (retrieved read-only from the live project 2026-07-03): | ||
|
|
||
| - `NEXT_PUBLIC_SUPABASE_URL` = `https://sjrfecxgysukkwxsowpy.supabase.co` | ||
| - `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` = `sb_publishable_TgAfWIQDozYC_reOI-d5cw_FLYPnqOa` | ||
| (modern publishable key — public by design, safe in the browser; a legacy anon | ||
| JWT is also still active for compatibility) | ||
| - `SUPABASE_SERVICE_ROLE_KEY` (server-only; never exposed to the client — not | ||
| reproduced here; copy it from the Supabase dashboard → Project Settings → API) | ||
|
|
||
| The **Supabase OAuth callback** to authorize in the Google Cloud / Azure AD app | ||
| registrations (§1) is `https://sjrfecxgysukkwxsowpy.supabase.co/auth/v1/callback`. | ||
| OAuth client secrets live in **Supabase**, not in app env. | ||
|
|
||
| ## 7. Database RLS + storage — already in place (verified against live 2026-07-03) | ||
|
|
||
| The DB-level per-user backstop the plan anticipated **already exists on the live | ||
| project**, so no broad RLS migration is required: | ||
|
|
||
| - Every owner-scoped **user-data** table (documents + children, `rag_queries`, | ||
| `rag_query_misses`, `rag_retrieval_logs`, `import_batches`, `rag_aliases`, | ||
| `storage_cleanup_jobs`, `document_*`) has RLS enabled **and** an `authenticated` | ||
| owner-read policy: `owner_id = (select auth.uid())`. | ||
| - Registry tables (`clinical_registry_records`, `_sources`) and internal tables | ||
| (`api_rate_limits`, `audit_logs`, `rag_response_cache`) are RLS-enabled and | ||
| **service-role-only** (fully server-mediated — intentional). | ||
| - Both storage buckets (`clinical-documents`, `clinical-images`) are **private**; | ||
| file access is via server-minted signed URLs after an owner check. No direct | ||
| client storage access is enabled (so no per-user folder policy is needed unless | ||
| client-direct storage reads are ever added). | ||
|
|
||
| Combined with the app-layer **fail-closed owner scoping** shipped on this branch, | ||
| per-user isolation is enforced at both layers. | ||
|
|
||
| **Two residual, low-priority items (out of scope for multi-user, no action needed | ||
| to launch):** | ||
|
|
||
| - `rag_visual_eval_cases` (an internal eval table) has RLS **disabled**, but it | ||
| has **no anon/authenticated grant** so it is effectively service-role-only. It | ||
| is also **not in `supabase/schema.sql`** (untracked live-only drift) — fixing it | ||
| properly means codifying the table first, a separate schema-hygiene task. | ||
| - Registry tables are service-role-only by design; add `authenticated` owner-read | ||
| policies only if you later introduce client-side registry reads. | ||
|
|
||
| ## Verification (staging, after the above) | ||
|
|
||
| 1. Sign up with **email + password** → receive + click the confirmation link → | ||
| land signed in. | ||
| 2. **Magic link** → email link → signed in. | ||
| 3. **Google** and **Microsoft** SSO → signed in. | ||
| 4. **Hard-refresh** the page → still signed in (persistent cookie session). | ||
| 5. **Isolation:** sign in as user A, upload a document, sign out; sign in as | ||
| user B → B sees none of A's documents, registry, or search results. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| import { createSupabaseServerClient } from "@/lib/supabase/server"; | ||
|
|
||
| export const runtime = "nodejs"; | ||
| export const dynamic = "force-dynamic"; | ||
|
|
||
| // Handles the PKCE code return for OAuth (Google/Microsoft), email-confirmation, | ||
| // and magic-link sign-in. Exchanges `?code=` for a session, writing the session | ||
| // cookies via the cookie-aware server client, then redirects into the app. | ||
| export async function GET(request: Request) { | ||
| const { searchParams, origin } = new URL(request.url); | ||
| const code = searchParams.get("code"); | ||
| const errorDescription = searchParams.get("error_description") ?? searchParams.get("error"); | ||
| // Only honour same-origin relative redirects to avoid an open-redirect. | ||
| const rawNext = searchParams.get("next") ?? "/"; | ||
| const next = rawNext.startsWith("/") && !rawNext.startsWith("//") ? rawNext : "/"; | ||
|
|
||
| const failure = (reason: string) => NextResponse.redirect(`${origin}/?auth_error=${encodeURIComponent(reason)}`); | ||
|
|
||
| if (errorDescription) { | ||
| return failure(errorDescription); | ||
| } | ||
| if (!code) { | ||
| return failure("missing_auth_code"); | ||
| } | ||
|
|
||
| const supabase = await createSupabaseServerClient(); | ||
| if (!supabase) { | ||
| return failure("auth_unconfigured"); | ||
| } | ||
|
|
||
| const { error } = await supabase.auth.exchangeCodeForSession(code); | ||
| if (error) { | ||
| return failure(error.message); | ||
| } | ||
| return NextResponse.redirect(`${origin}${next}`); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 magic-link or signup-confirmation emails opened in a different browser/profile/device than the one that requested the email, this code-only callback has no
sb-...-code-verifiercookie, soexchangeCodeForSession(code)fails and the user is redirected back with an auth error. The previous hash-token flow did not require same-browser state; to keep email auth reliable, add a token-hash/verifyOtpconfirmation path or make the configured email templates explicitly produce links this route can verify without the verifier cookie.Useful? React with 👍 / 👎.