-
Notifications
You must be signed in to change notification settings - Fork 88
Phase 4c: Intercept non-SSO logins for enforced domains #2798
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,6 +45,10 @@ jwt_expiry = 604800 | |
| # Allow/disallow new user signups to your project. | ||
| enable_signup = true | ||
|
|
||
| [auth.hook.custom_access_token] | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for dev environment - will have to configure this via the supabase dashboard in production |
||
| enabled = true | ||
| uri = "pg-functions://postgres/public/check_sso_requirement" | ||
|
|
||
| [auth.email] | ||
| # Allow/disallow new user signups via email to your project. | ||
| enable_signup = true | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| -- Access token hook that blocks social login for users whose email domain | ||
| -- matches an SSO-enforcing tenant's configured domain. | ||
| -- | ||
| -- When enforce_sso is true and the user's email domain appears in | ||
| -- auth.sso_domains for that tenant's provider, the hook refuses to mint | ||
| -- a token — returning an error that tells the frontend to redirect to SSO. | ||
| -- | ||
| -- Users whose email domain does NOT match (e.g. contractors, partners) | ||
| -- are not blocked here; their grants wil be handled separately | ||
| -- (manually removed at first, perhaps automatically filtered later). | ||
|
|
||
| begin; | ||
|
|
||
| -- Add enforce_sso column to tenants. When true, SSO is required for | ||
| -- users whose email domain matches the tenant's SSO provider domains. | ||
| alter table public.tenants | ||
| add column if not exists enforce_sso boolean not null default false; | ||
|
|
||
| comment on column public.tenants.enforce_sso is | ||
| 'When true, users whose email domain matches this tenant''s SSO provider ' | ||
| 'domains must authenticate via SSO. Social login is blocked for these users.'; | ||
|
|
||
| create or replace function public.check_sso_requirement(event jsonb) | ||
| returns jsonb | ||
| language plpgsql | ||
| stable | ||
| security definer | ||
| set search_path to '' | ||
| as $$ | ||
| declare | ||
| target_user_id uuid; | ||
| user_email text; | ||
| user_domain text; | ||
| user_is_sso boolean; | ||
| begin | ||
| target_user_id = (event->>'user_id')::uuid; | ||
|
|
||
| select u.email, u.is_sso_user | ||
| into user_email, user_is_sso | ||
| from auth.users u | ||
| where u.id = target_user_id; | ||
|
|
||
| -- SSO users pass through unconditionally. | ||
| if user_is_sso then | ||
| return event; | ||
| end if; | ||
|
|
||
| user_domain = split_part(user_email, '@', 2); | ||
|
|
||
| -- Check whether this user's email domain matches an SSO-enforcing tenant. | ||
| -- If so, block token issuance — the user should be logging in via SSO. | ||
| if exists ( | ||
| select 1 | ||
| from auth.sso_domains sd | ||
| join public.tenants t on t.sso_provider_id = sd.sso_provider_id | ||
| where t.enforce_sso = true | ||
| and sd.domain = user_domain | ||
| ) then | ||
| return jsonb_build_object( | ||
| 'error', jsonb_build_object( | ||
| 'http_code', 403, | ||
| 'message', 'sso_required:' || user_domain | ||
| ) | ||
| ); | ||
| end if; | ||
|
|
||
| return event; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've removed the exception handler so exceptions will bubble up to the caller - is this the right approach? is there any specific monitoring we need to set up or are all exceptions like readily visible? (where would i look for these anyway?) |
||
| end; | ||
| $$; | ||
|
|
||
| -- The hook is invoked by GoTrue as the supabase_auth_admin role. | ||
| grant usage on schema public to supabase_auth_admin; | ||
| grant execute on function public.check_sso_requirement(jsonb) to supabase_auth_admin; | ||
|
|
||
| -- The function reads from these tables. | ||
| grant select on public.tenants to supabase_auth_admin; | ||
| grant select on auth.users to supabase_auth_admin; | ||
| grant select on auth.sso_domains to supabase_auth_admin; | ||
|
|
||
| -- Anon and authenticated roles have execute privileges by default - revoke them. | ||
| -- check_sso_requirement is exclusively for supabase_auth_admin. | ||
| revoke execute on function public.check_sso_requirement(jsonb) from authenticated, anon; | ||
|
|
||
| commit; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 100% claude generated script for setting up mocksaml in a dev environment. it works on my machine... :) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| #!/usr/bin/env bash | ||
| # Start local Supabase with SAML enabled via MockSAML. | ||
| # | ||
| # `supabase stop && supabase start` recreates the auth container from scratch, | ||
| # stripping any env vars injected by setup-mocksaml. This script runs that | ||
| # cycle and then re-injects the SAML env vars and ensures the MockSAML provider | ||
| # is registered — no need to rerun the full skill. | ||
| # | ||
| # Usage: ./supabase/start-with-saml.sh [--reset] | ||
| # --reset Run `supabase db reset` instead of stop/start (wipes DB data) | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| # Generate a SAML signing key (PKCS#1). | ||
| # GoTrue needs this to sign outgoing SAMLRequests. | ||
| SAML_KEY=$(openssl genrsa -traditional 2048 2>/dev/null \ | ||
| | grep -v "^-----" \ | ||
| | tr -d '\n') | ||
|
|
||
| RESET=${1:-} | ||
| PSQL="psql postgresql://postgres:postgres@localhost:5432/postgres" | ||
|
|
||
| # Detect docker prefix (direct or via Lima VM) | ||
| if docker ps &>/dev/null; then | ||
| DOCKER="docker" | ||
| elif limactl shell tiger docker ps &>/dev/null 2>&1; then | ||
| DOCKER="limactl shell tiger docker" | ||
| else | ||
| echo "Error: cannot reach Docker daemon. Start Docker or your Lima VM first." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # ── Step 1: Start Supabase ──────────────────────────────────────────────────── | ||
|
|
||
| if [[ "$RESET" == "--reset" ]]; then | ||
| echo "--- Running supabase db reset ---" | ||
| supabase db reset | ||
| else | ||
| echo "--- Running supabase stop/start ---" | ||
| supabase stop | ||
| # Remove the auth container if it still exists — supabase stop doesn't remove | ||
| # containers that were manually recreated by this script (not tracked by | ||
| # docker-compose), which causes a name conflict on the next start. | ||
| $DOCKER rm -f supabase_auth_flow &>/dev/null || true | ||
| supabase start | ||
| fi | ||
|
|
||
| # ── Step 2: Inject SAML env vars (skipped if already present) ──────────────── | ||
|
|
||
| if $DOCKER exec supabase_auth_flow env 2>/dev/null | grep -q "GOTRUE_SAML_ENABLED=true"; then | ||
| echo "SAML env vars already present — skipping container recreation." | ||
| else | ||
| echo "--- Injecting SAML env vars into auth container ---" | ||
|
|
||
| IMAGE=$($DOCKER inspect supabase_auth_flow --format '{{.Config.Image}}') | ||
| NETWORK=$($DOCKER inspect supabase_auth_flow --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}') | ||
|
|
||
| $DOCKER inspect supabase_auth_flow --format '{{range .Config.Env}}{{println .}}{{end}}' \ | ||
| | grep -v -E '^(PATH=|API_EXTERNAL_URL=)' \ | ||
| > /tmp/auth_env.txt | ||
| echo "GOTRUE_SAML_ENABLED=true" >> /tmp/auth_env.txt | ||
| echo "GOTRUE_SAML_PRIVATE_KEY=$SAML_KEY" >> /tmp/auth_env.txt | ||
| echo "API_EXTERNAL_URL=http://127.0.0.1:5431/auth/v1" >> /tmp/auth_env.txt | ||
|
|
||
| if [[ "$DOCKER" == limactl* ]]; then | ||
| limactl copy /tmp/auth_env.txt tiger:/tmp/auth_env.txt | ||
| fi | ||
|
|
||
| $DOCKER stop supabase_auth_flow && $DOCKER rm supabase_auth_flow | ||
| $DOCKER run -d \ | ||
| --name supabase_auth_flow \ | ||
| --network "$NETWORK" \ | ||
| --restart always \ | ||
| --env-file /tmp/auth_env.txt \ | ||
| "$IMAGE" auth | ||
|
|
||
| sleep 3 | ||
| if $DOCKER logs supabase_auth_flow --tail 5 2>&1 | grep -q "GoTrue API started"; then | ||
| echo "GoTrue is running with SAML enabled." | ||
| else | ||
| echo "Warning: GoTrue may not have started cleanly. Check logs:" | ||
| $DOCKER logs supabase_auth_flow --tail 20 | ||
| exit 1 | ||
| fi | ||
| fi | ||
|
|
||
| # ── Step 3: Ensure MockSAML provider is registered ─────────────────────────── | ||
|
|
||
| PROVIDER_ID=$($PSQL -t -c "SELECT id FROM auth.sso_providers LIMIT 1;" 2>/dev/null | tr -d ' \n') | ||
|
|
||
| if [[ -n "$PROVIDER_ID" ]]; then | ||
| echo "MockSAML provider already registered (id: $PROVIDER_ID)." | ||
| else | ||
| echo "--- Registering MockSAML provider ---" | ||
|
|
||
| # Extract the sb_secret_... key from Kong config — Kong translates it to | ||
| # the service_role JWT when used as an apikey header. | ||
| SERVICE_ROLE_KEY=$($DOCKER exec supabase_kong_flow cat /home/kong/kong.yml \ | ||
| | grep -o "sb_secret_[A-Za-z0-9_-]*" | head -1 || true) | ||
|
|
||
| if [[ -z "$SERVICE_ROLE_KEY" ]]; then | ||
| echo "Error: could not determine service role key from Kong config." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| read -r -p "Email domain to associate with MockSAML [example.com]: " DOMAIN | ||
| DOMAIN="${DOMAIN:-example.com}" | ||
|
|
||
| RESPONSE=$(curl -s -X POST 'http://127.0.0.1:5431/auth/v1/admin/sso/providers' \ | ||
| -H "apikey: $SERVICE_ROLE_KEY" \ | ||
| -H 'Content-Type: application/json' \ | ||
| -d "{\"type\":\"saml\",\"metadata_url\":\"https://mocksaml.com/api/saml/metadata\",\"domains\":[\"$DOMAIN\"]}") | ||
|
|
||
| PROVIDER_ID=$(echo "$RESPONSE" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])" 2>/dev/null || true) | ||
|
|
||
| if [[ -z "$PROVIDER_ID" ]]; then | ||
| echo "Error: failed to register provider. Response: $RESPONSE" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "MockSAML provider registered (id: $PROVIDER_ID, domain: $DOMAIN)." | ||
| fi | ||
|
|
||
| # ── Step 4: Link provider to a tenant ──────────────────────────────────────── | ||
|
|
||
| LINKED_TENANT=$($PSQL -t -c "SELECT tenant FROM public.tenants WHERE sso_provider_id = '$PROVIDER_ID' LIMIT 1;" 2>/dev/null | tr -d ' \n') | ||
|
|
||
| if [[ -n "$LINKED_TENANT" ]]; then | ||
| echo "Provider already linked to tenant: $LINKED_TENANT" | ||
| else | ||
| DEFAULT_TENANT=$($PSQL -t -c "SELECT tenant FROM public.tenants WHERE tenant NOT LIKE 'ops.%' ORDER BY tenant LIMIT 1;" 2>/dev/null | tr -d ' \n') | ||
|
|
||
| echo "" | ||
| echo "Available tenants:" | ||
| $PSQL -t -c "SELECT tenant FROM public.tenants WHERE tenant NOT LIKE 'ops.%' ORDER BY tenant;" 2>/dev/null | tr -d ' ' | grep -v '^$' | ||
| echo "" | ||
| read -r -p "Tenant to link to MockSAML provider [$DEFAULT_TENANT]: " TENANT | ||
| TENANT="${TENANT:-$DEFAULT_TENANT}" | ||
|
|
||
| # Normalise: ensure trailing slash | ||
| TENANT="${TENANT%/}/" | ||
|
|
||
| $PSQL -c "UPDATE public.tenants SET sso_provider_id = '$PROVIDER_ID' WHERE tenant = '$TENANT';" | ||
| echo "Linked provider to tenant: $TENANT" | ||
| fi | ||
|
|
||
| echo "" | ||
| echo "--- Setup complete ---" | ||
| echo " Provider ID : $PROVIDER_ID" | ||
| echo " SSO login : curl -s -X POST 'http://127.0.0.1:5431/auth/v1/sso' -H 'Content-Type: application/json' -d '{\"provider_id\":\"$PROVIDER_ID\"}'" |
Uh oh!
There was an error while loading. Please reload this page.
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.
will probably remove this skill in favor of the
start-with-saml.shscript below