Skip to content

feat: implement SSO activation and enforcement workflow#1725

Merged
Dalanir merged 71 commits intomainfrom
feat/enterprise-sso-activation
Mar 3, 2026
Merged

feat: implement SSO activation and enforcement workflow#1725
Dalanir merged 71 commits intomainfrom
feat/enterprise-sso-activation

Conversation

@Dalanir
Copy link
Contributor

@Dalanir Dalanir commented Feb 28, 2026

Summary (AI generated)

  • Fixed SSO domain check endpoint authentication to work during login flow
  • Added SSO provider status transition management (pending → verified → active → disabled)
  • Implemented comprehensive SSO configuration UI with activation controls
  • Added copy buttons for DNS TXT and SAML metadata
  • Implemented Enforce SSO toggle for domain-wide authentication requirements
  • Added Enterprise plan gating for all SSO features
  • Added English translation keys for SSO management actions

Motivation (AI generated)

Enterprise customers need a complete SSO lifecycle management workflow. Previously, SSO providers could be created but there was no way to:

  1. Activate a verified provider
  2. Enforce SSO for all domain users
  3. Easily copy SAML metadata for IdP configuration
  4. Manage provider status transitions

Additionally, the `/check-domain` endpoint required authentication, which prevented it from being called during the login flow when users are unauthenticated.

Business Impact (AI generated)

This enables Capgo to offer a complete Enterprise SSO solution, which:

  • Increases Enterprise conversions: Full-featured SSO is a requirement for many enterprise security policies
  • Reduces support burden: Self-service DNS verification and metadata display eliminates configuration support tickets
  • Enables upselling: SSO is gated behind Enterprise plan, driving plan upgrades
  • Improves security posture: Enforce SSO toggle prevents shadow IT and password-based bypasses
  • Competitive parity: Matches SSO offerings from competitors like Vercel, Netlify, and Auth0

Expected impact: Increased Enterprise plan adoption and reduced time-to-activation for SSO customers.

Test Plan (AI generated)

Backend Tests

  • Verify `/check-domain` endpoint works without authentication
  • Test with authenticated user (should still work)
  • Test with anonymous user (should work via SECURITY DEFINER)
  • Verify status transitions: pending_verification → verified → active
  • Test invalid transitions are rejected (e.g., pending → active)
  • Verify `dns_verification_token` is preserved in POST response

Frontend Tests

  • Verify SSO section hidden for non-Enterprise plans
  • Test "Add Provider" button in empty state
  • Test DNS TXT copy button copies correct value
  • Test SAML metadata copy buttons (ACS URL, Entity ID, Certificate)
  • Test Activate button on verified provider
  • Test Deactivate button on active provider
  • Test Enforce SSO toggle on active provider
  • Verify SAML metadata section is collapsible and closed by default
  • Test translations display correctly in English

Integration Tests

  • Full flow: Create provider → Verify DNS → Activate → Enforce
  • Test SSO login when enforce_sso is enabled
  • Test password login is blocked when SSO is enforced
  • Verify error handling for failed status transitions

Generated with AI

Summary by CodeRabbit

  • New Features

    • Full enterprise SSO: SAML provider management, DNS TXT verification, enforceable SSO policies, user provisioning, and SSO callback handling.
    • Login overhaul: guided multi-step sign-in (email-first domain check), explicit SSO path, password and 2FA steps.
    • Admin UI: SSO configuration panel in organization settings (Enterprise gated).
  • Localization

    • Added SSO translations across many languages and a "continue with SSO" option.
  • Tests

    • New integration and end-to-end tests for SSO flows and DNS verification.

Dalanir and others added 25 commits February 23, 2026 20:07
Add 38 SSO-related translation keys across all supported languages:
- continue-with-sso (login flow)
- sso-configuration, sso-add-provider, sso-new-provider, etc. (configuration)
- sso-domain, sso-metadata-url (form fields)
- sso-dns-verification-* (DNS verification flow)
- sso-status-* (status badges)
- sso-error-* (error messages)
- sso-no-providers (empty states)

Languages: en, de, es, fr, hi, id, it, ja, ko, pl, pt-br, ru, tr, vi, zh-cn

All translations follow semantic grouping with alphabetical ordering within groups.
{domain} placeholder preserved in sso-delete-confirm for all languages.

Generated with AI
Fix 4 issues identified during manual QA verification:

1. SSO redirect preserves 'to' query parameter (login.vue)
   - handleSsoLogin() now appends ?to= param to /sso-callback redirect
   - Ensures users land on originally requested protected route

2. Public route mismatch resolved (sso-enforcement.ts)
   - Changed /forgot-password → /forgot_password in PUBLIC_ROUTES
   - Matches actual route definition

3. Auto-provisioning integrated (sso-callback.vue)
   - Import and call useSSOProvisioning() after code exchange
   - Ensures user provisioned on first SSO login

4. Test API compatibility (sso*.test.ts)
   - Replace it.concurrent() → it() (Vitest doesn't support concurrent)
   - All 7 SSO tests now pass

All changes verified with lint, typecheck, and test suite.

Generated with AI
Enable SSO provider CRUD testing locally without real Supabase Management API token.
Mock mode activates when SUPABASE_MANAGEMENT_API_TOKEN is missing or placeholder.
Generates fake provider IDs and returns realistic mock responses for all CRUD operations.

Production behavior unchanged - real API calls when tokens are configured.

- Add isLocalDevMode() detection for missing/placeholder tokens
- Add generateMockProviderId() for unique mock IDs
- Add mockManagementAPICall() with POST/GET/PATCH/DELETE mocks
- Update .env with SSO Management API env var documentation
- Document mock mode behavior in learnings notepad

Fixes #issue-local-dev-sso-500-error
- Replace recentlyCreatedProvider with pendingVerificationProvider
- Show DNS instructions for ANY pending_verification provider, not just newly created
- Prioritize recently created provider first, then any pending provider
- Block now persists across page refreshes until DNS is verified
- Fixes issue where DNS instructions disappeared after page reload
The check-domain endpoint is called during login flow when users are unauthenticated. Removed middlewareAuth and added SECURITY DEFINER to the SQL function to allow anonymous access while maintaining security through RLS.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Enhanced SSO provider management:
- Added status field to update schema
- Implemented status transition validation (pending_verification → verified → active → disabled)
- Preserved dns_verification_token in POST response for UI display

This enables the SSO activation workflow for Enterprise customers.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Added comprehensive SSO configuration UI:
- Copy buttons for DNS TXT and SAML metadata
- Add Provider button in empty state
- Activate/Deactivate/Enforce SSO action buttons
- Collapsible SAML metadata section (closed by default)
- Enterprise plan gating for all SSO features

Enables full SSO lifecycle management for Enterprise organizations.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Added English translation keys for SSO management:
- Activate/deactivate/reactivate actions
- Enforce SSO toggle and tooltip
- Success/error messages for all operations

Supports the new SSO activation UI.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements end-to-end SSO: adds frontend UI/components and composables, refactors login into a multi-step SSO-aware flow, introduces many backend serverless SSO endpoints and utilities, DB migrations/types for sso_providers, tests, i18n entries across locales, and related env/config updates.

Changes

Cohort / File(s) Summary
Translations
messages/*.json
messages/de.json, messages/en.json, messages/es.json, messages/fr.json, messages/hi.json, messages/id.json, messages/it.json, messages/ja.json, messages/ko.json, messages/pl.json, messages/pt-br.json, messages/ru.json, messages/tr.json, messages/vi.json, messages/zh-cn.json
Added comprehensive sso-* localization keys and continue-with-sso across many locales.
Login UI & Flow
src/pages/login.vue, src/pages/sso-callback.vue, playwright/e2e/sso-login.spec.ts
Refactored login into email → credentials/SSO → 2FA multi-step flow; added SSO callback page and Playwright e2e tests for two-step SSO login and back navigation.
SSO Management Frontend
src/components/organizations/SsoConfiguration.vue, src/pages/settings/organization/Security.vue, src/components.d.ts, src/auto-imports.d.ts
New SsoConfiguration component, type declarations, and integration into Security page (enterprise-gated).
Composables & Routing
src/composables/useSSORouting.ts, src/composables/useSSOProvisioning.ts
New composables for domain checks, SSO routing/sign-in, and server-side provisioning helper.
Client Enforcement
src/modules/sso-enforcement.ts
New client-side navigation guard module enforcing SSO rules with short TTL caching and server check.
Routing Types
src/route-map.d.ts
Added /sso-callback route typing and file mapping.
Backend SSO Endpoints
supabase/functions/_backend/private/sso/*.ts
check-domain.ts, check-enforcement.ts, providers.ts, verify-dns.ts, prelink.ts, prelink-internal.ts, provision-user.ts
Added 7 Hono-based serverless endpoints for domain checks, enforcement, provider CRUD, DNS verification, prelinking, internal prelink, and provisioning (auth, validation, plan gating, logging).
Backend Utilities
supabase/functions/_backend/utils/*.ts
supabase-management.ts, dns-verification.ts, plan-gating.ts, supabase.types.ts
Management API client for Supabase SSO provider lifecycle, Cloudflare DoH DNS TXT verifier, plan-gating helpers, and updated Supabase types.
DB Migrations & Types
supabase/migrations/*.sql, src/types/supabase.types.ts, supabase/functions/_backend/utils/supabase.types.ts
Added sso_providers table, triggers, SECURITY DEFINER check_domain_sso, grants, and TypeScript type updates; added channel_permission_overrides types.
Backend Route Registration & Env
supabase/functions/private/index.ts, supabase/functions/.env, supabase/functions/.env.example
Registered SSO endpoints in private index and added SUPABASE_MANAGEMENT_API_TOKEN and SUPABASE_PROJECT_REF env vars (and examples).
Tests
tests/*.ts, playwright/e2e/sso-login.spec.ts
Added integration tests for SSO endpoints and DNS verification; Playwright e2e for login flow.
Config & Ignore
.claude/settings.local.json, .gitignore
Removed local Claude permissions list entry; added AI-agent directories (.sisyphus/, .claude/) to .gitignore.
Type Augmentations
src/components.d.ts, src/auto-imports.d.ts
Exposed SsoConfiguration component and auto-imported composables useSSOProvisioning and useSSORouting.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser
    participant Frontend as App (Login UI)
    participant Backend as API (/private/sso)
    participant DB as Database
    participant Auth as Supabase Auth / Mgmt
    participant IdP as Identity Provider

    User->>Browser: enter email
    Browser->>Frontend: submit email
    Frontend->>Backend: POST /private/sso/check-domain { email }
    Backend->>DB: call check_domain_sso(domain)
    DB-->>Backend: { has_sso, provider_id, org_id }
    Backend-->>Frontend: { has_sso }
    alt has_sso == true
        Frontend->>Auth: signInWithSSO(domain)
        Auth-->>IdP: redirect to IdP
        IdP-->>Auth: SAML assertion
        Auth->>Frontend: redirect to /sso-callback
        Frontend->>Backend: (optional) POST /private/sso/provision-user
        Backend->>DB: insert org_user if needed
    else
        Frontend->>Auth: signInWithPassword
    end
    Frontend->>User: navigate to dashboard
Loading
sequenceDiagram
    participant Admin
    participant UI as SsoConfiguration
    participant Backend as API (/private/sso/providers)
    participant Mgmt as Supabase Management API
    participant DB as Database
    participant DNS as Cloudflare DoH

    Admin->>UI: create provider (domain, metadata)
    UI->>Backend: POST /private/sso/providers
    Backend->>Mgmt: create provider on Supabase Management API
    Mgmt-->>Backend: { provider_id }
    Backend->>DB: insert sso_providers (pending_verification + dns token)
    Backend-->>UI: provider created, show DNS TXT to add

    Admin->>DNS: add TXT record _capgo-sso.domain
    UI->>Backend: POST /private/sso/verify-dns { provider_id }
    Backend->>DNS: verifyDnsTxtRecord(domain, token)
    DNS-->>Backend: records & verified
    Backend->>DB: update sso_providers status → verified
    Backend-->>UI: verification success
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • riderx

Poem

🐰
I hopped through DNS and tiny tokens bright,
I nudged providers into verified light,
With hoppity hops I tied domains to keys,
Now logins flow smooth as a breeze — carrots please!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The pull request description provides a detailed AI-generated summary covering motivation, business impact, and test plans, but lacks verification that the author has completed the manual checklist items for code style, documentation, and testing. Verify that the author has completed the manual checklist items (linting, documentation updates, E2E test coverage, manual testing) before merge.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'feat: implement SSO activation and enforcement workflow' accurately describes the main change, which is the addition of a complete SSO lifecycle management system with activation controls and enforcement.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/enterprise-sso-activation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/login.vue (1)

481-516: ⚠️ Potential issue | 🟠 Major

Don’t force SSO-only login just because a provider exists.

On Line 481 onward, hasSso hides the password form entirely. That blocks password login even when SSO exists but enforcement is disabled.

💡 Minimal fallback option
               <div v-if="hasSso" class="space-y-5">
@@
                 <div class="text-center">
+                  <p
+                    class="font-medium text-orange-500 transition-all duration-200 cursor-pointer hover:text-orange-600 hover:underline"
+                    `@click`="hasSso = false"
+                  >
+                    {{ t('sso-dismiss') }}
+                  </p>
                   <p class="font-medium text-orange-500 transition-all duration-200 cursor-pointer hover:text-orange-600 hover:underline" `@click`="goBackToEmail()">
                     ← {{ t('go-back') }}
                   </p>
                 </div>
               </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/login.vue` around lines 481 - 516, The template currently uses
v-if="hasSso" to completely hide the password form when any SSO provider exists;
change the condition so the password form is only hidden when SSO is actually
enforced (e.g., use v-if="hasSso && isSsoEnforced" or similar boolean), and for
non-enforced SSO render the SSO prompt (with handleSsoLogin and goBackToEmail)
above or alongside the password FormKit (handlePasswordSubmit) so users can
still sign in with a password when enforcement is disabled. Ensure you add or
use the enforcement flag (isSsoEnforced / ssoEnforced) in the component
state/props and update the v-if/v-else logic accordingly.
🟡 Minor comments (8)
supabase/functions/.env.example-35-35 (1)

35-35: ⚠️ Potential issue | 🟡 Minor

Remove the extra blank line flagged by dotenv-linter.

Line 35 triggers ExtraBlankLine; trimming it will keep .env.example lint-clean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/.env.example` at line 35, Remove the extra blank/empty
line in the .env.example file (the stray extra blank line flagged by
dotenv-linter) so there are no consecutive blank lines; ensure the file ends
with a single newline character and re-run dotenv-linter to confirm the
ExtraBlankLine warning is resolved.
.sisyphus/notepads/enterprise-sso/decisions.md-20-20 (1)

20-20: ⚠️ Potential issue | 🟡 Minor

Clarify token exposure policy to avoid contradictory guidance.

Line 20 says dns_verification_token is not returned by list endpoints, while the PR objective says token preservation in responses. Please make this explicit (e.g., “preserved only for specific provider/detail responses”) so future changes don’t regress behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.sisyphus/notepads/enterprise-sso/decisions.md at line 20, The current note
contradicts itself about dns_verification_token exposure—update the decision
text to explicitly state the token exposure policy: that dns_verification_token
is not returned by LIST endpoints but is preserved and returned only in specific
endpoints (e.g., provider-specific or detail GET responses) as described in the
PR objective; reference the token name dns_verification_token and the LIST
endpoint wording so reviewers can verify consistency and add a short example
sentence like “dns_verification_token: not returned in LIST responses; returned
only in provider/detail GET responses” to prevent future regressions.
messages/tr.json-1357-1357 (1)

1357-1357: ⚠️ Potential issue | 🟡 Minor

Fix typo in SSO configuration description.

Line 1357 uses “Teklu”; this should be “Tekli” for correct Turkish wording.

✏️ Proposed fix
-  "sso-configuration-description": "Kuruluşunuz için SAML 2.0 Teklu Oturum Açma özelliğini yapılandırın",
+  "sso-configuration-description": "Kuruluşunuz için SAML 2.0 Tekli Oturum Açma özelliğini yapılandırın",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@messages/tr.json` at line 1357, The string value for the localization key
"sso-configuration-description" contains a typo ("Teklu")—update the value to
use the correct Turkish word "Tekli" so the entry reads: Kuruluşunuz için SAML
2.0 Tekli Oturum Açma özelliğini yapılandırın; locate the key
"sso-configuration-description" in the messages/tr.json and replace only the
misspelled word.
messages/pt-br.json-1383-1383 (1)

1383-1383: ⚠️ Potential issue | 🟡 Minor

Correct PT-BR article typo in SSO empty-state description.

Line 1383 uses “un provedor”; in PT-BR this should be “um provedor”.

✏️ Proposed fix
-  "sso-no-providers-description": "Adicione un provedor SSO para habilitar o Logon Único em sua organização",
+  "sso-no-providers-description": "Adicione um provedor SSO para habilitar o Logon Único em sua organização",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@messages/pt-br.json` at line 1383, Fix the PT-BR string for key
"sso-no-providers-description": change the article "un" to the correct Brazilian
Portuguese "um" so the value reads "Adicione um provedor SSO para habilitar o
Logon Único em sua organização".
.sisyphus/notepads/enterprise-sso/learnings.md-627-627 (1)

627-627: ⚠️ Potential issue | 🟡 Minor

Fix list indentation to satisfy markdownlint MD005.

Line 627 has one extra leading space compared to sibling list items.

🔧 Suggested fix
-    - Impact: public-route allowlist is inconsistent; current behavior is mostly masked by `!session` early-return, but route config is incorrect.
+   - Impact: public-route allowlist is inconsistent; current behavior is mostly masked by `!session` early-return, but route config is incorrect.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.sisyphus/notepads/enterprise-sso/learnings.md at line 627, The list item
"Impact: public-route allowlist is inconsistent; current behavior is mostly
masked by `!session` early-return, but route config is incorrect." has one extra
leading space breaking MD005; edit the Markdown so this list line's indentation
matches its sibling list items (remove the extra leading space) to restore
consistent list indentation and re-run markdownlint to verify the MD005 warning
is resolved.
supabase/functions/_backend/private/sso/check-domain.ts-26-29 (1)

26-29: ⚠️ Potential issue | 🟡 Minor

Normalize extracted domain before lookup.

Line 26 uses raw domain casing from the email. Mixed-case domains can miss SSO matches on case-sensitive comparisons.

🔧 Suggested fix
-  const domain = email.split('@')[1]
+  const domain = email.split('@')[1]?.trim().toLowerCase()
   if (!domain) {
     return quickError(400, 'invalid_email', 'Email must contain a domain')
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/private/sso/check-domain.ts` around lines 26 -
29, The extracted domain (variable domain from email.split in check-domain) must
be normalized before SSO lookup to avoid case-sensitive misses; trim whitespace
and convert to lowercase (and apply any IDN/punycode normalization if your app
supports internationalized domains) and then use that normalized value in your
lookup logic instead of the raw domain; keep the same invalid-domain early
return using quickError('invalid_email') when no domain exists.
.sisyphus/plans/enterprise-sso.md-345-356 (1)

345-356: ⚠️ Potential issue | 🟡 Minor

Task 3 endpoint auth requirements conflict with themselves.

This section says to use middlewareAuth and also says the endpoint is public for login and should not require user permission checks. Please resolve this contradiction so implementation guidance is unambiguous.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.sisyphus/plans/enterprise-sso.md around lines 345 - 356, The auth
instructions for POST /private/sso/check-domain conflict: remove the requirement
to enforce authenticated user permissions and instead make the endpoint publicly
callable for login flows; implement it using the optional/lenient auth
middleware (e.g., middlewareAuthOptional or equivalent) or no auth middleware
while retaining any global rate-limiting/abuse protections, ensure you do not
perform user permission checks in the handler for check-domain, and keep the
sensitive provider fields omitted when returning has_sso/provider_id/org_id.
.sisyphus/plans/enterprise-sso.md-130-171 (1)

130-171: ⚠️ Potential issue | 🟡 Minor

Add language identifiers to fenced code blocks (MD040).

Multiple fenced blocks are missing language tags (for example text, bash, sql), which triggers markdownlint warnings.

Also applies to: 269-284, 328-335, 375-393, 434-449, 504-523, 570-580, 623-641, 680-694, 742-761, 807-822, 864-871, 915-932, 981-998, 1037-1045, 1084-1099, 1141-1158, 1198-1205, 1250-1257, 1298-1305, 1344-1351, 1395-1403

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.sisyphus/plans/enterprise-sso.md around lines 130 - 171, The markdown has
multiple fenced code blocks without language identifiers (MD040); update each
triple-backtick block (e.g., the Wave plan block and the other blocks called
out) to include an appropriate language tag such as text, bash, or sql so
markdownlint stops reporting MD040; search for all fenced blocks in
enterprise-sso.md (including the Wave 1..FINAL plan block and the other ranges
noted) and add the correct language token for each block based on its contents
(use "text" for plain ASCII diagrams, "bash" for shell snippets, "sql" for
queries, etc.).
🧹 Nitpick comments (10)
supabase/functions/_backend/private/sso/verify-dns.ts (1)

19-22: Add explicit authType handling before continuing request flow.

Line 19 only checks presence of auth. Explicit branching on JWT vs API key keeps this endpoint aligned with your backend auth contract and avoids ambiguous behavior across callers.

Based on learnings: Check c.get('auth')?.authType to determine authentication type ('apikey' vs 'jwt') in backend endpoints.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/private/sso/verify-dns.ts` around lines 19 - 22,
The current code only checks for presence of auth via c.get('auth') before
continuing; update the handler to read c.get('auth')?.authType and explicitly
branch on allowed types ('apikey' and 'jwt'), e.g., allow request to proceed for
'apikey' and 'jwt' and call quickError(401, 'not_authorized', 'Not authorized')
for missing or any other authType; reference the existing auth retrieval
(c.get('auth')) and the quickError helper when implementing this explicit
branching so the endpoint enforces the backend auth contract.
src/auto-imports.d.ts (1)

248-249: Use ~/ alias for the new composable import types.

These new type imports use relative paths on Line 248 and Line 249, which conflicts with the frontend import-path guideline.

Suggested patch
-  const useSSOProvisioning: typeof import('./composables/useSSOProvisioning').useSSOProvisioning
-  const useSSORouting: typeof import('./composables/useSSORouting').useSSORouting
+  const useSSOProvisioning: typeof import('~/composables/useSSOProvisioning').useSSOProvisioning
+  const useSSORouting: typeof import('~/composables/useSSORouting').useSSORouting

As per coding guidelines "Use path alias ~/ to reference src/ directory in imports".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/auto-imports.d.ts` around lines 248 - 249, The generated type imports for
useSSOProvisioning and useSSORouting currently use relative paths; update their
import type references to use the project path alias by changing the module
specifiers to start with "~/", e.g. import types from
"~/composables/useSSOProvisioning" and "~/composables/useSSORouting" so the
declarations for useSSOProvisioning and useSSORouting in auto-imports.d.ts
follow the frontend import-path guideline.
src/pages/settings/organization/Security.vue (1)

1402-1407: Hardcoded strings should use i18n for consistency.

The SSO Configuration section title and description are hardcoded in English, while other sections on this page use the t() function for internationalization. Consider using i18n keys for consistency with the rest of the codebase.

♻️ Proposed fix to use i18n
               <h3 class="text-lg font-semibold dark:text-white text-slate-800">
-                  SSO Configuration
+                  {{ t('sso-configuration') }}
               </h3>
               <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
-                  Configure SAML 2.0 Single Sign-On for your organization. Requires Enterprise plan.
+                  {{ t('sso-configuration-description') }}
               </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/settings/organization/Security.vue` around lines 1402 - 1407,
Replace the hardcoded English strings in the SSO section by using the i18n
translation function: update the <h3> element content ("SSO Configuration") and
the following <p> description ("Configure SAML 2.0 Single Sign-On for your
organization. Requires Enterprise plan.") to call t() with new i18n keys (e.g.
t('settings.sso.title') and t('settings.sso.description')); add those keys to
the locale files with the corresponding English text and any other locales, and
keep the existing class attributes and markup (the <h3> and <p> in Security.vue)
untouched aside from replacing the inner text with the t() calls.
tests/sso.test.ts (1)

50-117: Consider using it.concurrent() for parallel test execution.

Per coding guidelines, tests within the same file should use it.concurrent() for parallel execution. Since these tests use isolated test data (unique org ID, customer ID), they should be safe to run concurrently.

♻️ Proposed fix to use concurrent tests
 describe('[POST] /private/sso/check-domain', () => {
-  it('should return has_sso=false for non-SSO domain', async () => {
+  it.concurrent('should return has_sso=false for non-SSO domain', async () => {
     // ...
   })

-  it('should return 400 for invalid email', async () => {
+  it.concurrent('should return 400 for invalid email', async () => {
     // ...
   })
 })

 describe('[POST] /private/sso/check-enforcement', () => {
-  it('should return allowed=true for password auth when no SSO is configured', async () => {
+  it.concurrent('should return allowed=true for password auth when no SSO is configured', async () => {
     // ...
   })
 })

 describe('[GET] /private/sso/providers/:orgId', () => {
-  it('should return empty array for org with no SSO providers', async () => {
+  it.concurrent('should return empty array for org with no SSO providers', async () => {
     // ...
   })

-  it('should return 401 without authentication', async () => {
+  it.concurrent('should return 401 without authentication', async () => {
     // ...
   })
 })

As per coding guidelines: "ALL test files run in parallel; use it.concurrent() instead of it() to run tests within the same file in parallel for faster CI/CD".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sso.test.ts` around lines 50 - 117, The tests in this file use
synchronous it() calls but should use Jest's it.concurrent() to allow parallel
execution; update each test invocation (e.g., the cases with titles "should
return has_sso=false for non-SSO domain", "should return 400 for invalid email",
"should return allowed=true for password auth when no SSO is configured",
"should return empty array for org with no SSO providers", and "should return
401 without authentication") to use it.concurrent(), keeping their bodies
unchanged (they call fetchWithRetry/getEndpointUrl and reference SSO_TEST_ORG_ID
and authHeaders), so tests remain isolated but run in parallel.
tests/sso-verify-dns.test.ts (1)

12-36: Consider using it.concurrent() for parallel test execution.

Per coding guidelines, test files run in parallel, and it.concurrent() should be used instead of it() to run tests within the same file in parallel for faster CI/CD.

♻️ Proposed fix to use concurrent tests
 describe('[POST] /private/sso/verify-dns', () => {
-  it('should return 404 for non-existent provider', async () => {
+  it.concurrent('should return 404 for non-existent provider', async () => {
     const response = await fetchWithRetry(getEndpointUrl('/private/sso/verify-dns'), {
       method: 'POST',
       headers: authHeaders,
       body: JSON.stringify({ provider_id: randomUUID() }),
     })

     expect(response.status).toBe(404)
     const data = await response.json() as { error: string }
     expect(data.error).toBe('provider_not_found')
   })

-  it('should return 401 without authentication', async () => {
+  it.concurrent('should return 401 without authentication', async () => {
     const response = await fetchWithRetry(getEndpointUrl('/private/sso/verify-dns'), {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
       },
       body: JSON.stringify({ provider_id: randomUUID() }),
     })

     expect(response.status).toBe(401)
     const data = await response.json() as { error: string }
     expect(data.error).toBe('no_jwt_apikey_or_subkey')
   })
 })

As per coding guidelines: "ALL test files run in parallel; use it.concurrent() instead of it() to run tests within the same file in parallel for faster CI/CD".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/sso-verify-dns.test.ts` around lines 12 - 36, Replace the two serial
tests in this file so they run concurrently: change the calls to it() for the
tests named 'should return 404 for non-existent provider' and 'should return 401
without authentication' to use it.concurrent(), leaving the test bodies
(fetchWithRetry, getEndpointUrl, authHeaders, assertions) unchanged so the same
setup and assertions are executed in parallel.
playwright/e2e/sso-login.spec.ts (1)

32-32: Fragile selector for back button.

The back button is selected using page.locator('p').filter({ hasText: '←' }), which is fragile and inconsistent with the data-test attribute pattern used for other elements in this test. If the arrow character changes or the element type changes, this test will break silently.

♻️ Proposed fix to use data-test attribute
-    await page.locator('p').filter({ hasText: '←' }).first().click()
+    await page.locator('[data-test="back-to-email"]').click()

This requires adding the data-test="back-to-email" attribute to the back button in the login component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@playwright/e2e/sso-login.spec.ts` at line 32, Replace the fragile arrow-based
locator ("page.locator('p').filter({ hasText: '←' }).first()") with a stable
data-test selector and add data-test="back-to-email" to the back button in the
login component; update the test to click the locator that targets
[data-test="back-to-email"] (use the same selector pattern as other tests) so
the test no longer depends on the arrow character or element type.
src/pages/sso-callback.vue (1)

58-82: Hardcoded strings should use i18n.

Multiple strings in the template are hardcoded in English. For consistency with the rest of the codebase and to support internationalization, these should use the t() function.

♻️ Proposed fix to use i18n
         <h1 class="text-2xl font-bold text-gray-900 dark:text-white">
-          SSO Authentication
+          {{ t('sso-authentication') }}
         </h1>

         <div v-if="isLoading" class="mt-6 space-y-4">
           <!-- loader -->
           <p class="text-gray-700 dark:text-gray-300">
-            Completing sign in...
+            {{ t('sso-completing-sign-in') }}
           </p>
         </div>

         <div v-else class="mt-6 space-y-4">
           <p class="font-medium text-red-600">
             {{ errorMessage }}
           </p>
           <p class="text-gray-700 dark:text-gray-300">
-            Please try again or contact your administrator.
+            {{ t('sso-error-contact-admin') }}
           </p>
           <router-link
             to="/login"
             class="..."
           >
-            Back to Login
+            {{ t('back-to-login') }}
           </router-link>
         </div>

You'll also need to add const { t } = useI18n() to the script setup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/sso-callback.vue` around lines 58 - 82, Replace hardcoded English
strings in the SSO callback template with i18n calls and add the i18n composable
in the script setup: add const { t } = useI18n() in the script setup, then
change "SSO Authentication" to t('sso.title'), "Completing sign in..." to
t('sso.completingSignIn'), the static "Please try again or contact your
administrator." to t('sso.retryOrContactAdmin'), and the router-link text "Back
to Login" to t('auth.backToLogin'); keep the dynamic errorMessage binding as-is
(it may contain server text) but wrap any other static labels around it with
t(...) as needed so all fixed strings use the i18n keys (e.g., sso.title,
sso.completingSignIn, sso.retryOrContactAdmin, auth.backToLogin).
supabase/functions/_backend/private/sso/check-enforcement.ts (1)

29-30: Use message (not context) in structured backend logs.

Several cloudlog(...) calls use context instead of the required message field shape, which breaks log consistency and filtering.

As per coding guidelines: “Use structured logging with cloudlog({ requestId: c.get('requestId'), message: '...' }) for all backend logging.”

Also applies to: 44-45, 52-53, 58-59, 73-74, 79-80, 92-93, 100-101, 105-106, 109-110

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/private/sso/check-enforcement.ts` around lines 29
- 30, Replace the incorrect `context` field in all cloudlog calls in
check-enforcement.ts with the required `message` field and ensure the requestId
is taken via c.get('requestId'); e.g., update each cloudlog call (the ones shown
around the return lines and at the other listed locations) to use cloudlog({
requestId: c.get('requestId'), message: 'check_enforcement - SSO auth always
allowed', email }) and similarly change the message text for the other cloudlog
invocations (lines referenced in the comment) so logs follow the structured
shape.
src/pages/login.vue (1)

445-447: Use DaisyUI interactive components for the new auth actions.

The newly added action controls are plain <button> elements; project guidelines require DaisyUI interactive components.

As per coding guidelines: “Use DaisyUI components (d-btn, d-input, d-card) for interactive elements in Vue components.”

Also applies to: 497-505, 543-547, 617-621

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/login.vue` around lines 445 - 447, The template uses plain <button>
elements for auth actions (seen around the v-if="!isDomainChecking" submit
button and also at the other noted ranges) but project guidelines require
DaisyUI interactive components; replace each plain button with the DaisyUI
component (d-btn) while preserving the same bindings and attributes
(v-if/v-else, type="submit", data-test values, click handlers, disabled states
and any reactive classes like isDomainChecking) and map existing class-based
styles to the appropriate d-btn props or classes to keep visual/behavioral
parity; do this for the button at v-if="!isDomainChecking" and the other
occurrences referenced (lines ~497-505, ~543-547, ~617-621).
supabase/functions/_backend/private/sso/providers.ts (1)

70-74: Branch explicitly on authType in these handlers.

These handlers only check that auth exists. Add an explicit c.get('auth')?.authType branch so JWT/API key flows are intentionally handled.

As per coding guidelines: “Check c.get('auth')?.authType to determine authentication type ('apikey' vs 'jwt') in backend endpoints.”

Also applies to: 123-126, 151-154, 238-241

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/private/sso/providers.ts` around lines 70 - 74,
The current handlers only verify presence of auth but must branch on
c.get('auth')?.authType; update the app.post('/') handler (and the other POST
handlers marked in the comment) to read const auth = c.get('auth') and then
switch or if/else on auth?.authType (e.g., 'apikey' vs 'jwt'), explicitly
allowing the intended flows and calling quickError(401, ...) when the authType
is missing or not permitted; ensure the branching logic is applied in the same
way in the other handlers referenced so API-key and JWT flows are handled
intentionally.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude/settings.local.json:
- Line 14: Replace the overly broad permission entry "Bash(grep:*)" with
least-privilege patterns: find the "Bash(grep:*)" entry and restrict it to the
exact file paths or regex patterns your code needs (for example, specific log or
config directories) or explicit commands rather than a wildcard; update the
allowlist to one or more scoped entries like Bash(grep:<allowed-path-or-regex>)
that only match the expected targets and remove the global wildcard.
- Around line 15-18: Remove the machine-specific absolute paths in
.claude/settings.local.json by replacing entries like "Bash(git -C
/Users/jordanlorho/Capgo/capgo log --oneline -15)" and the other "git -C
/Users/jordanlorho/Capgo/capgo ..." commands with repo-relative or generic
commands (for example "Bash(git -C . log --oneline -15)" or simply "Bash(git log
--oneline -15)"), or remove them entirely and keep truly local
permissions/config out of version control; ensure the changed strings in the
file match the originals so you can find and update each offending entry.

In @.sisyphus/boulder.json:
- Around line 2-8: The committed .sisyphus/boulder.json currently contains
machine-specific and sensitive data: the absolute path in the active_plan value
and raw session IDs in session_ids (and optionally a precise started_at
timestamp). Update the artifact to sanitize these fields: replace active_plan's
absolute path with a portable value (e.g., just the filename "enterprise-sso.md"
or null), remove or redact session_ids (set to an empty array or a placeholder
like ["REDACTED"]), and if desired clear or normalize started_at (set to null or
a non-identifying timestamp); ensure you modify the active_plan, session_ids,
and started_at keys in .sisyphus/boulder.json so no local paths or raw session
tokens are committed.

In `@src/components/organizations/SsoConfiguration.vue`:
- Around line 343-570: Replace raw HTML form controls and buttons with the
project's DaisyUI primitives: swap <input v-model="newDomain"> and <input
v-model="newMetadataUrl"> for d-input components (preserve v-model,
type/placeholder, :disabled="isSubmitting" and classes/aria attributes), wrap
provider blocks and the DNS-verification panel in d-card, and replace all
<button> elements (create, cancel, verifyDns, updateProviderStatus,
toggleEnforceSso checkbox wrapper, deleteProvider, dismiss) with d-btn (preserve
disabled state checks like :disabled="isSubmitting" or :disabled="isVerifying
=== provider.id", click handlers addProvider, verifyDns(provider.id),
updateProviderStatus(provider.id, ...), toggleEnforceSso(provider),
deleteProvider(provider), recentlyCreatedId = null, and keep Spinner/Icon
components and title/tooltip text). Ensure d-input/d-btn support the same
classes/props (loading state, size, icon slots) so existing bindings (newDomain,
newMetadataUrl, isSubmitting, isVerifying, pendingVerificationProvider,
providers) and accessibility attributes remain unchanged.
- Around line 329-574: The component currently renders SSO controls without
verifying the stricter backend permission; add a frontend check for the
org.manage_sso permission and gate UI/actions accordingly: create a
computed/prop (e.g., hasManageSso or canManageSso) and use it to wrap or guard
the Add Provider form (showAddForm), pendingVerificationProvider actions
(verifyDns), provider action buttons (calls to addProvider,
updateProviderStatus, deleteProvider, toggleEnforceSso) and the DNS copy/verify
controls so users without org.manage_sso cannot see or interact with these
controls; alternatively, update the parent Security.vue permission gate to check
for org.manage_sso instead of org.update_settings so SsoConfiguration.vue is
never mounted for insufficient-permission users. Include references to methods
addProvider, verifyDns, updateProviderStatus, deleteProvider, toggleEnforceSso
and reactive state pendingVerificationProvider/showAddForm when implementing the
guard.

In `@src/composables/useSSOProvisioning.ts`:
- Around line 70-93: The code in provisionUser currently only logs and returns
when data.has_sso && data.org_id is true, so it never actually provisions org
membership; replace the early return with a call to the backend provisioning
endpoint (using session.access_token) to pre-link the user to the returned
org_id/provider_id. Specifically, in the block where you inspect data.has_sso,
call a POST (or the existing server route for SSO provisioning) with body {
email, org_id: data.org_id, provider_id: data.provider_id } and await the
response, handle non-ok errors (log and continue/fail gracefully), and only
return after the provisioning request completes successfully or has been
handled; keep using the same headers pattern as the check-domain request and
ensure provisionUser processes the provisioning result instead of just logging.

In `@src/modules/sso-enforcement.ts`:
- Around line 84-94: The cache is being marked valid even when the enforcement
HTTP response is not ok; change the logic in the SSO enforcement flow so
setCacheValid() is only called when response.ok is true AND the parsed
SsoEnforcementResponse has data.allowed === true. Locate the block that checks
response.ok and data.allowed (uses response.ok, SsoEnforcementResponse,
clearSsoEnforcementCache(), supabase.auth.signOut(),
next('/login?sso_required=true')), and move the setCacheValid() call inside the
branch where response.ok && data.allowed are both satisfied so failed or non-OK
responses never set the cache.

In `@src/pages/sso-callback.vue`:
- Around line 40-41: The code uses route.query.to directly in router.replace
(redirectTo and router.replace) causing an open redirect; validate and sanitize
the parameter before using it: only accept internal paths (e.g., ensure
redirectTo is a relative path starting with a single '/' and not starting with
'//' or containing a scheme like 'http:'), or match it against a whitelist of
allowed paths, and fall back to '/dashboard' if validation fails. Update the
logic around route.query.to and router.replace to perform this check and only
call router.replace with a validated path.

In `@supabase/functions/_backend/private/sso/check-enforcement.ts`:
- Around line 83-106: Remove the special-case super-admin bypass in the SSO
enforcement flow: delete the isSuperAdmin check and the early return that
returns c.json({ allowed: true }) (the block referencing isSuperAdmin, cloudlog
'super admin bypass', and that return). Keep the existing role query and its
error handling (roleData/roleError and the PGRST116 check) but ensure that after
determining roleData there is no conditional that exempts org_super_admin from
SSO enforcement; always log the enforced SSO outcome and return c.json({
allowed: false, reason: 'sso_enforced' }) when SSO is enforced.

In `@supabase/functions/_backend/private/sso/prelink.ts`:
- Around line 66-72: The handler currently uses the service-role client via
supabaseAdmin(c) (see variable admin and calls to .from('sso_providers') and
later admin.auth.api.listUsers / admin.auth.getUserById), which bypasses RLS;
replace those database reads with the authenticated client (use the
request-bound Supabase client — e.g., the same client used for other user
requests) so the .from('sso_providers').select(...).eq('id',
provider_id).single() call is executed under RLS and not via supabaseAdmin(c).
For auth.admin-only operations (listUsers, getUserById) do not call admin.auth
from this user-facing endpoint; instead move those actions behind a separate
internal endpoint authenticated by an API secret or perform them only in backend
processes that use the service role. Ensure all references to admin for DB reads
are swapped to the authenticated client and that only the isolated internal
route uses supabaseAdmin for privileged auth operations.

In `@supabase/functions/_backend/private/sso/providers.ts`:
- Around line 262-273: The current flow deletes the database row before calling
deleteSSOProvider which risks cross-system inconsistency if external deletion
fails; change the order so you first call deleteSSOProvider(c,
provider.provider_id) (only when provider.provider_id exists), handle and
surface any error from that call (use quickError to return a 500 and include the
external error), and only after successful external deletion perform the
supabase .from('sso_providers').delete().eq('id', id) and then handle
deleteError as before; ensure you reference deleteSSOProvider,
provider.provider_id, id, quickError and the supabase delete call when making
the change.
- Around line 94-115: The external provider (created via createSSOProvider) can
be left orphaned if the DB insert (via
supabaseWithAuth().from('sso_providers').insert(...)) fails; modify the flow so
that if the insert returns error or no data you call the management API to
delete/rollback the created provider (e.g., a deleteSSOProvider or
managementProvider.delete using managementProvider.id) before calling
quickError, and ensure this cleanup runs in the error branch (and in any thrown
exception path) so the external provider is removed on DB failure; reference
createSSOProvider, managementProvider.id,
supabaseWithAuth(...).from('sso_providers').insert(...),
generateDnsVerificationToken and quickError when adding the rollback.

In `@supabase/functions/_backend/private/sso/verify-dns.ts`:
- Around line 64-70: The update always sets status: 'verified' which can
downgrade an already-active provider; modify the logic in verify-dns so you
first read the current row (from sso_providers by provider_id) or check its
status before updating, and only set status: 'verified' when the existing status
is not 'active' (but always update dns_verified_at). Use the same supabase
client and provider_id symbol to locate the code that currently calls .update({
status: 'verified', dns_verified_at: ... }). Ensure the update either omits the
status field if current status === 'active' or explicitly preserves the existing
status when performing the .update.

In `@supabase/functions/_backend/utils/dns-verification.ts`:
- Around line 31-36: The code allows an empty expectedToken to pass because
''.includes('') is true; update the validation to reject empty/whitespace tokens
and require exact matches when checking TXT records: ensure expectedToken is a
non-empty string (e.g., typeof expectedToken === 'string' &&
expectedToken.trim().length > 0) and replace loose checks that use
includes(expectedToken) with strict equality or a token-exact comparison (e.g.,
txtRecord === expectedToken or txtRecord.split('"').join('') === expectedToken)
wherever expectedToken is compared (the checks around expectedToken at the
initial validation block and the comparison at the TXT answers processing, lines
referencing expectedToken).
- Around line 46-51: The Cloudflare DoH fetch in dns-verification.ts currently
has no timeout; wrap the request in an AbortController, start a timer (e.g.,
3–5s) that calls controller.abort(), pass signal to the existing fetch call that
creates `response`, and clear the timer after fetch completes; also catch the
abort/timeout error (AbortError) and surface a clear, descriptive error so the
caller can handle timeouts from the DNS-over-HTTPS request.

In `@supabase/functions/_backend/utils/supabase-management.ts`:
- Around line 34-39: The current isLocalDevMode in supabase-management.ts treats
missing SUPABASE_MANAGEMENT_API_TOKEN or SUPABASE_PROJECT_REF as local-dev and
enables mock mode; change this to "fail closed": use getEnv to read token and
projectRef and if either is missing or empty throw a clear error (e.g., throw
new Error(...)) so misconfiguration surfaces, but still allow explicit local-dev
values to enable mock mode (keep the token === 'local-dev-token' || projectRef
=== 'local-dev-ref' check to return true); otherwise return false. Target the
isLocalDevMode function and getEnv usage when making this change.

In `@supabase/migrations/20260223000001_add_sso_providers.sql`:
- Around line 123-137: Make domain case-insensitive by using the citext type for
public.sso_providers.domain (or alternatively enforce uniqueness via a UNIQUE
index on lower(domain)); change the column definition from "domain text NOT NULL
UNIQUE" to "domain citext NOT NULL UNIQUE" (ensure the citext extension is
created earlier in the migration) and remove the redundant non-unique index
CREATE INDEX idx_sso_providers_domain ON public.sso_providers (domain) so you
only have the case-insensitive unique constraint on domain.

---

Outside diff comments:
In `@src/pages/login.vue`:
- Around line 481-516: The template currently uses v-if="hasSso" to completely
hide the password form when any SSO provider exists; change the condition so the
password form is only hidden when SSO is actually enforced (e.g., use
v-if="hasSso && isSsoEnforced" or similar boolean), and for non-enforced SSO
render the SSO prompt (with handleSsoLogin and goBackToEmail) above or alongside
the password FormKit (handlePasswordSubmit) so users can still sign in with a
password when enforcement is disabled. Ensure you add or use the enforcement
flag (isSsoEnforced / ssoEnforced) in the component state/props and update the
v-if/v-else logic accordingly.

---

Minor comments:
In @.sisyphus/notepads/enterprise-sso/decisions.md:
- Line 20: The current note contradicts itself about dns_verification_token
exposure—update the decision text to explicitly state the token exposure policy:
that dns_verification_token is not returned by LIST endpoints but is preserved
and returned only in specific endpoints (e.g., provider-specific or detail GET
responses) as described in the PR objective; reference the token name
dns_verification_token and the LIST endpoint wording so reviewers can verify
consistency and add a short example sentence like “dns_verification_token: not
returned in LIST responses; returned only in provider/detail GET responses” to
prevent future regressions.

In @.sisyphus/notepads/enterprise-sso/learnings.md:
- Line 627: The list item "Impact: public-route allowlist is inconsistent;
current behavior is mostly masked by `!session` early-return, but route config
is incorrect." has one extra leading space breaking MD005; edit the Markdown so
this list line's indentation matches its sibling list items (remove the extra
leading space) to restore consistent list indentation and re-run markdownlint to
verify the MD005 warning is resolved.

In @.sisyphus/plans/enterprise-sso.md:
- Around line 345-356: The auth instructions for POST /private/sso/check-domain
conflict: remove the requirement to enforce authenticated user permissions and
instead make the endpoint publicly callable for login flows; implement it using
the optional/lenient auth middleware (e.g., middlewareAuthOptional or
equivalent) or no auth middleware while retaining any global rate-limiting/abuse
protections, ensure you do not perform user permission checks in the handler for
check-domain, and keep the sensitive provider fields omitted when returning
has_sso/provider_id/org_id.
- Around line 130-171: The markdown has multiple fenced code blocks without
language identifiers (MD040); update each triple-backtick block (e.g., the Wave
plan block and the other blocks called out) to include an appropriate language
tag such as text, bash, or sql so markdownlint stops reporting MD040; search for
all fenced blocks in enterprise-sso.md (including the Wave 1..FINAL plan block
and the other ranges noted) and add the correct language token for each block
based on its contents (use "text" for plain ASCII diagrams, "bash" for shell
snippets, "sql" for queries, etc.).

In `@messages/pt-br.json`:
- Line 1383: Fix the PT-BR string for key "sso-no-providers-description": change
the article "un" to the correct Brazilian Portuguese "um" so the value reads
"Adicione um provedor SSO para habilitar o Logon Único em sua organização".

In `@messages/tr.json`:
- Line 1357: The string value for the localization key
"sso-configuration-description" contains a typo ("Teklu")—update the value to
use the correct Turkish word "Tekli" so the entry reads: Kuruluşunuz için SAML
2.0 Tekli Oturum Açma özelliğini yapılandırın; locate the key
"sso-configuration-description" in the messages/tr.json and replace only the
misspelled word.

In `@supabase/functions/_backend/private/sso/check-domain.ts`:
- Around line 26-29: The extracted domain (variable domain from email.split in
check-domain) must be normalized before SSO lookup to avoid case-sensitive
misses; trim whitespace and convert to lowercase (and apply any IDN/punycode
normalization if your app supports internationalized domains) and then use that
normalized value in your lookup logic instead of the raw domain; keep the same
invalid-domain early return using quickError('invalid_email') when no domain
exists.

In `@supabase/functions/.env.example`:
- Line 35: Remove the extra blank/empty line in the .env.example file (the stray
extra blank line flagged by dotenv-linter) so there are no consecutive blank
lines; ensure the file ends with a single newline character and re-run
dotenv-linter to confirm the ExtraBlankLine warning is resolved.

---

Nitpick comments:
In `@playwright/e2e/sso-login.spec.ts`:
- Line 32: Replace the fragile arrow-based locator ("page.locator('p').filter({
hasText: '←' }).first()") with a stable data-test selector and add
data-test="back-to-email" to the back button in the login component; update the
test to click the locator that targets [data-test="back-to-email"] (use the same
selector pattern as other tests) so the test no longer depends on the arrow
character or element type.

In `@src/auto-imports.d.ts`:
- Around line 248-249: The generated type imports for useSSOProvisioning and
useSSORouting currently use relative paths; update their import type references
to use the project path alias by changing the module specifiers to start with
"~/", e.g. import types from "~/composables/useSSOProvisioning" and
"~/composables/useSSORouting" so the declarations for useSSOProvisioning and
useSSORouting in auto-imports.d.ts follow the frontend import-path guideline.

In `@src/pages/login.vue`:
- Around line 445-447: The template uses plain <button> elements for auth
actions (seen around the v-if="!isDomainChecking" submit button and also at the
other noted ranges) but project guidelines require DaisyUI interactive
components; replace each plain button with the DaisyUI component (d-btn) while
preserving the same bindings and attributes (v-if/v-else, type="submit",
data-test values, click handlers, disabled states and any reactive classes like
isDomainChecking) and map existing class-based styles to the appropriate d-btn
props or classes to keep visual/behavioral parity; do this for the button at
v-if="!isDomainChecking" and the other occurrences referenced (lines ~497-505,
~543-547, ~617-621).

In `@src/pages/settings/organization/Security.vue`:
- Around line 1402-1407: Replace the hardcoded English strings in the SSO
section by using the i18n translation function: update the <h3> element content
("SSO Configuration") and the following <p> description ("Configure SAML 2.0
Single Sign-On for your organization. Requires Enterprise plan.") to call t()
with new i18n keys (e.g. t('settings.sso.title') and
t('settings.sso.description')); add those keys to the locale files with the
corresponding English text and any other locales, and keep the existing class
attributes and markup (the <h3> and <p> in Security.vue) untouched aside from
replacing the inner text with the t() calls.

In `@src/pages/sso-callback.vue`:
- Around line 58-82: Replace hardcoded English strings in the SSO callback
template with i18n calls and add the i18n composable in the script setup: add
const { t } = useI18n() in the script setup, then change "SSO Authentication" to
t('sso.title'), "Completing sign in..." to t('sso.completingSignIn'), the static
"Please try again or contact your administrator." to
t('sso.retryOrContactAdmin'), and the router-link text "Back to Login" to
t('auth.backToLogin'); keep the dynamic errorMessage binding as-is (it may
contain server text) but wrap any other static labels around it with t(...) as
needed so all fixed strings use the i18n keys (e.g., sso.title,
sso.completingSignIn, sso.retryOrContactAdmin, auth.backToLogin).

In `@supabase/functions/_backend/private/sso/check-enforcement.ts`:
- Around line 29-30: Replace the incorrect `context` field in all cloudlog calls
in check-enforcement.ts with the required `message` field and ensure the
requestId is taken via c.get('requestId'); e.g., update each cloudlog call (the
ones shown around the return lines and at the other listed locations) to use
cloudlog({ requestId: c.get('requestId'), message: 'check_enforcement - SSO auth
always allowed', email }) and similarly change the message text for the other
cloudlog invocations (lines referenced in the comment) so logs follow the
structured shape.

In `@supabase/functions/_backend/private/sso/providers.ts`:
- Around line 70-74: The current handlers only verify presence of auth but must
branch on c.get('auth')?.authType; update the app.post('/') handler (and the
other POST handlers marked in the comment) to read const auth = c.get('auth')
and then switch or if/else on auth?.authType (e.g., 'apikey' vs 'jwt'),
explicitly allowing the intended flows and calling quickError(401, ...) when the
authType is missing or not permitted; ensure the branching logic is applied in
the same way in the other handlers referenced so API-key and JWT flows are
handled intentionally.

In `@supabase/functions/_backend/private/sso/verify-dns.ts`:
- Around line 19-22: The current code only checks for presence of auth via
c.get('auth') before continuing; update the handler to read
c.get('auth')?.authType and explicitly branch on allowed types ('apikey' and
'jwt'), e.g., allow request to proceed for 'apikey' and 'jwt' and call
quickError(401, 'not_authorized', 'Not authorized') for missing or any other
authType; reference the existing auth retrieval (c.get('auth')) and the
quickError helper when implementing this explicit branching so the endpoint
enforces the backend auth contract.

In `@tests/sso-verify-dns.test.ts`:
- Around line 12-36: Replace the two serial tests in this file so they run
concurrently: change the calls to it() for the tests named 'should return 404
for non-existent provider' and 'should return 401 without authentication' to use
it.concurrent(), leaving the test bodies (fetchWithRetry, getEndpointUrl,
authHeaders, assertions) unchanged so the same setup and assertions are executed
in parallel.

In `@tests/sso.test.ts`:
- Around line 50-117: The tests in this file use synchronous it() calls but
should use Jest's it.concurrent() to allow parallel execution; update each test
invocation (e.g., the cases with titles "should return has_sso=false for non-SSO
domain", "should return 400 for invalid email", "should return allowed=true for
password auth when no SSO is configured", "should return empty array for org
with no SSO providers", and "should return 401 without authentication") to use
it.concurrent(), keeping their bodies unchanged (they call
fetchWithRetry/getEndpointUrl and reference SSO_TEST_ORG_ID and authHeaders), so
tests remain isolated but run in parallel.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee141cd and 5b6a41a.

📒 Files selected for processing (59)
  • .claude/settings.local.json
  • .sisyphus/boulder.json
  • .sisyphus/evidence/task-1-migration-applies.txt
  • .sisyphus/evidence/task-17-env-vars.txt
  • .sisyphus/evidence/task-2-module-exports.txt
  • .sisyphus/evidence/task-3-no-sso.txt
  • .sisyphus/evidence/task-4-dns-fail.txt
  • .sisyphus/evidence/task-5-create-provider.txt
  • .sisyphus/evidence/task-6-prelink-uuid.txt
  • .sisyphus/evidence/task-7-enforced-block.txt
  • .sisyphus/evidence/task-8-enterprise-pass.txt
  • .sisyphus/notepads/enterprise-sso/decisions.md
  • .sisyphus/notepads/enterprise-sso/issues.md
  • .sisyphus/notepads/enterprise-sso/learnings.md
  • .sisyphus/plans/enterprise-sso.md
  • messages/de.json
  • messages/en.json
  • messages/es.json
  • messages/fr.json
  • messages/hi.json
  • messages/id.json
  • messages/it.json
  • messages/ja.json
  • messages/ko.json
  • messages/pl.json
  • messages/pt-br.json
  • messages/ru.json
  • messages/tr.json
  • messages/vi.json
  • messages/zh-cn.json
  • playwright/e2e/sso-login.spec.ts
  • src/auto-imports.d.ts
  • src/components.d.ts
  • src/components/organizations/SsoConfiguration.vue
  • src/composables/useSSOProvisioning.ts
  • src/composables/useSSORouting.ts
  • src/modules/sso-enforcement.ts
  • src/pages/login.vue
  • src/pages/settings/organization/Security.vue
  • src/pages/sso-callback.vue
  • src/route-map.d.ts
  • src/types/supabase.types.ts
  • supabase/functions/.env
  • supabase/functions/.env.example
  • supabase/functions/_backend/private/sso/check-domain.ts
  • supabase/functions/_backend/private/sso/check-enforcement.ts
  • supabase/functions/_backend/private/sso/prelink.ts
  • supabase/functions/_backend/private/sso/providers.ts
  • supabase/functions/_backend/private/sso/verify-dns.ts
  • supabase/functions/_backend/utils/dns-verification.ts
  • supabase/functions/_backend/utils/plan-gating.ts
  • supabase/functions/_backend/utils/supabase-management.ts
  • supabase/functions/_backend/utils/supabase.types.ts
  • supabase/functions/private/index.ts
  • supabase/migrations/20260223000001_add_sso_providers.sql
  • supabase/migrations/20260228154639_fix_check_domain_sso_security.sql
  • supabase/seed.sql
  • tests/sso-verify-dns.test.ts
  • tests/sso.test.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@supabase/functions/_backend/private/sso/verify-dns.ts`:
- Around line 34-41: The code in verify-dns.ts uses supabaseWithAuth
(supabaseWithAuth(...)) to query and update the sso_providers table; replace
those calls with the approved PostgreSQL client by calling getPgClient() or
getDrizzleClient() from utils/pg.ts and reimplement the read and update logic
(the select/.single() read for provider by provider_id and subsequent update
statements) using parameterized pg queries or Drizzle methods so all DB access
(reads/updates against sso_providers using provider_id, dns_verification_token,
status, etc.) uses the pg/drizzle client instead of supabaseWithAuth. Ensure you
preserve the same error handling and return values when translating the supabase
.from(...).select(...) and update calls to the new client.
- Around line 78-81: There is a duplicate/out-of-scope check for updateError
after the block where updateError is declared; remove the dangling if
(updateError) { cloudlog(...); quickError(...) } that appears after the closing
brace of the provider.status === 'pending_verification' block so only the
original check inside that block remains (leave the existing cloudlog(...) and
quickError(...) within the if (provider.status === 'pending_verification')
branch and delete the extra check referencing updateError outside that scope).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b6a41a and 5ebb9ae.

📒 Files selected for processing (1)
  • supabase/functions/_backend/private/sso/verify-dns.ts

Dalanir added 7 commits March 2, 2026 17:31
Prevent enabling SSO enforcement when the provider status is not
'active'. Auto-reset enforce_sso to false when provider is disabled
to prevent stale enforcement blocking users.
Replace the errors array (containing raw user IDs) in the response
with an error_count. User IDs are now only logged server-side via
cloudlogErr, preventing information leakage in API responses.
Replace raw pg client for provider lookup with supabaseAdmin as
defense-in-depth. Raw pg is still used for the atomic conditional
status update where it is needed.
Add ALLOWED_ATTRIBUTE_KEYS whitelist (email, first_name, last_name,
display_name, groups, role, phone) and enforce a 256-char max on
values. Reject unknown keys and empty values to prevent injection
of arbitrary SAML attribute mappings.

Security: M2 finding from SSO audit.
Apply sanitizeProvider() to the POST creation response. The function
was already used on GET and PATCH responses but the POST handler
returned raw data including the dns_verification_token secret.

Security: L1 finding from SSO audit.
Add rate limiting (10 requests/minute per IP) to the unauthenticated
check-domain endpoint to prevent mass domain enumeration. Uses the
existing CacheHelper + getClientIP pattern from rate_limit.ts.
Returns 429 when limit is exceeded.

Security: M4 finding from SSO audit.
Show SSO config only for Enterprise plan orgs. Non-Enterprise orgs
see a placeholder with an upgrade button linking to the plans page.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (2)
supabase/functions/_backend/private/sso/prelink-internal.ts (1)

158-163: Remove redundant normalizedDomain redeclaration.

normalizedDomain is already defined and normalized at line 102. This inner redeclaration from the raw domain variable is unnecessary and slightly inconsistent (it omits the .trim() call).

♻️ Suggested fix
     for (const user of users) {
       const userEmail = user.email?.toLowerCase() ?? ''
-      const normalizedDomain = domain.toLowerCase()
       if (!userEmail.endsWith(`@${normalizedDomain}`)) {
         continue
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/private/sso/prelink-internal.ts` around lines 158
- 163, In the for-loop over users, remove the inner redeclaration "const
normalizedDomain = domain.toLowerCase()" and instead use the previously computed
normalizedDomain (the one normalized with .trim() earlier) when checking
userEmail.endsWith; this eliminates redundancy and ensures the same
trimmed/lowercased domain value is used consistently in the userEmail check
inside the loop.
supabase/functions/_backend/private/sso/providers.ts (1)

139-141: Use structured backend logging for rollback failures.

These console.error calls drop request context. Please log cleanup failures via structured backend logging with requestId.

🔧 Suggested fix
-import { version } from '../../utils/version.ts'
+import { cloudlogErr } from '../../utils/logging.ts'
+import { version } from '../../utils/version.ts'
@@
       await deleteSSOProvider(c, managementProvider.id).catch((cleanupError) => {
-        console.error('Failed to cleanup external SSO provider after DB insert failure:', cleanupError)
+        cloudlogErr({
+          requestId: c.get('requestId'),
+          message: 'Failed to cleanup external SSO provider after DB insert failure',
+          providerId: managementProvider.id,
+          error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
+        })
       })
@@
     await deleteSSOProvider(c, managementProvider.id).catch((cleanupError) => {
-      console.error('Failed to cleanup external SSO provider after exception:', cleanupError)
+      cloudlogErr({
+        requestId: c.get('requestId'),
+        message: 'Failed to cleanup external SSO provider after exception',
+        providerId: managementProvider.id,
+        error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
+      })
     })

As per coding guidelines: "Use structured logging with cloudlog({ requestId: c.get('requestId'), message: '...' }) for all backend logging."

Also applies to: 149-151

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/private/sso/providers.ts` around lines 139 - 141,
Replace the console.error rollback logs with structured backend logging using
cloudlog and the requestId from the context: when catching errors from
deleteSSOProvider(c, managementProvider.id) (and the similar handler around the
other cleanup at lines referenced), call cloudlog({ requestId:
c.get('requestId'), message: 'Failed to cleanup external SSO provider after DB
insert failure', err: cleanupError }) (or equivalent structured payload) so the
cleanup failure preserves request context and error details; update both
occurrences that currently use console.error to use cloudlog with
c.get('requestId').
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/composables/useSSOProvisioning.ts`:
- Around line 73-76: When the provisionResponse is not ok in
useSSOProvisioning.ts (the block handling /private/sso/provision-user), set the
composable's error.value to the parsed error (e.g., error.value = errorData || {
message: 'Provisioning failed', status: provisionResponse.status }) instead of
only console.error, and ensure you abort further success handling (return or
throw) so callers see the failure; apply the same change to the similar failure
handling around the provisionResponse at lines 82-84 so both failure branches
populate error.value with useful details.
- Around line 43-60: The current early return in useSSOProvisioning checks
org_membership from supabase (org_users query -> orgMembership) and returns
whenever the user belongs to any org, which is too broad; remove that
short-circuit (do not return when orgMembership is truthy) or refine it to only
skip when orgMembership.org_id matches the specific SSO target org id if you
have a resolved target id, and always proceed to call the backend provisioning
endpoint (the existing provisioning API call) so the server can decide
membership for the resolved provider org; update the logic around the supabase
.from('org_users') query, orgMembership handling, and the place where the
provisioning request is made in useSSOProvisioning accordingly.

In `@src/modules/sso-enforcement.ts`:
- Around line 89-99: The guard currently only enforces SSO when response.ok is
true, letting 4xx/5xx responses pass; update the check around fetch response
handling in the SSO enforcement function so that any non-ok response
(response.ok === false) is treated like a failure: call
clearSsoEnforcementCache(), await supabase.auth.signOut(), and return
next('/login?sso_required=true') instead of falling through; keep the existing
behavior of setCacheValid(userId) only when response.ok and data.allowed are
true and apply the same non-ok handling for the other similar response branch
that uses the same logic (refer to response, SsoEnforcementResponse,
setCacheValid, clearSsoEnforcementCache, supabase.auth.signOut, and next).

In `@src/pages/settings/organization/Security.vue`:
- Around line 1418-1422: Replace the hardcoded English UI strings in
Security.vue (e.g., "SSO Configuration", "Configure SAML 2.0 Single Sign-On for
your organization." and the CTA/labels around the subsequent blocks) with i18n
lookups using the t(...) helper (for example: t('settings.security.ssoTitle'),
t('settings.security.ssoDescription'), etc.), update the template bindings where
those strings are used (headers, paragraph text and CTA/button labels), and add
corresponding keys and translations to the locale JSON/YAML files so the new
keys exist for all supported languages.

In `@supabase/functions/_backend/private/sso/check-domain.ts`:
- Around line 22-23: The branch in check-domain.ts that returns false when
getClientIP() yields 'unknown' bypasses your rate-limiting; instead of returning
early, treat 'unknown' as a valid identifier for throttling (e.g., set ip =
'unknown' and proceed) or derive a fallback key (like a normalized Origin/Host
header or a global "unknown" bucket) so the request still passes through your
existing rate-limiting/throttling logic; update the code around getClientIP() in
check-domain.ts to remove the early return and ensure the rate limiter receives
a concrete key for the IP/fallback case.

In `@supabase/functions/_backend/private/sso/check-enforcement.ts`:
- Around line 17-35: The endpoint currently trusts client-supplied body fields
(email, auth_type) which allows spoofing; change the logic in check-enforcement
to ignore these request body fields for enforcement decisions and instead derive
the authenticated user’s attributes (email and auth_type) from the server-side
auth/session context (e.g., the authenticated user object available on c,
c.auth, or c.get('user')) before running validation or enforcement; keep
parseBody and bodySchema for optional input validation if needed but do not use
parsed email/auth_type for allow/deny decisions—use the server-derived values,
log them via requestId/cloudlog, and handle missing server-side identity with a
clear error path.

In `@supabase/functions/_backend/private/sso/prelink-internal.ts`:
- Around line 66-74: The fetch call in prelink-internal.ts currently has no
timeout and can hang; wrap the DELETE request to the GoTrue service in an
AbortController: create an AbortController, pass controller.signal to fetch (the
existing fetch invocation), set a short timeout (e.g., 5s) with setTimeout that
calls controller.abort(), and clear that timeout after fetch completes; also
catch the abort error in the surrounding try/catch so aborted requests are
treated as a timeout case rather than indefinite hangs.
- Around line 95-131: The request extracts org_id but never verifies it against
the provider; update the provider verification to ensure the provider belongs to
that org by including org_id in the lookup and/or validating the returned
provider's org_id: when calling
supabaseAdmin(...).from('sso_providers').select(...).eq('id', provider_id) add a
filter for org_id (e.g., .eq('org_id', org_id)) or retain the existing query and
after obtaining providerCheck assert providerCheck.org_id === org_id, logging a
clear error with requestId and returning a quickError like 'provider_not_found'
or 'org_mismatch' if it doesn't match; keep the existing domain/status checks
afterwards.

In `@supabase/functions/_backend/private/sso/provision-user.ts`:
- Around line 14-24: The auth block currently only verifies presence of auth and
userId but doesn't ensure the auth type is JWT; update the handler to explicitly
check auth.authType === 'jwt' (using c.get('auth')?.authType or the existing
auth variable) and return quickError(401, 'not_authorized', 'Not authorized') or
a similar 401 when the auth type is not 'jwt' before any provisioning logic
(i.e., immediately after acquiring auth and before using userId/requestId in
this file's provisioning code such as in provision-user.ts).
- Around line 92-103: The insert into org_users currently treats any insertError
as fatal; update the error handling around (admin as
any).from('org_users').insert(...) so that if insertError indicates a
unique-constraint/duplicate-membership (e.g., SQLSTATE '23505' or message
containing 'duplicate' depending on Supabase error shape) you treat it as a
successful idempotent outcome (do not call quickError), otherwise log with
cloudlogErr({ requestId, message: 'Failed to insert user into org_users',
userId, orgId: provider.org_id, error: insertError }) and return quickError(500,
'provision_failed', 'Failed to provision user to organization') as before; keep
the same context (userId, provider.org_id) in logs.

---

Nitpick comments:
In `@supabase/functions/_backend/private/sso/prelink-internal.ts`:
- Around line 158-163: In the for-loop over users, remove the inner
redeclaration "const normalizedDomain = domain.toLowerCase()" and instead use
the previously computed normalizedDomain (the one normalized with .trim()
earlier) when checking userEmail.endsWith; this eliminates redundancy and
ensures the same trimmed/lowercased domain value is used consistently in the
userEmail check inside the loop.

In `@supabase/functions/_backend/private/sso/providers.ts`:
- Around line 139-141: Replace the console.error rollback logs with structured
backend logging using cloudlog and the requestId from the context: when catching
errors from deleteSSOProvider(c, managementProvider.id) (and the similar handler
around the other cleanup at lines referenced), call cloudlog({ requestId:
c.get('requestId'), message: 'Failed to cleanup external SSO provider after DB
insert failure', err: cleanupError }) (or equivalent structured payload) so the
cleanup failure preserves request context and error details; update both
occurrences that currently use console.error to use cloudlog with
c.get('requestId').

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af50915 and 8c7441d.

📒 Files selected for processing (10)
  • src/composables/useSSOProvisioning.ts
  • src/modules/sso-enforcement.ts
  • src/pages/settings/organization/Security.vue
  • supabase/functions/_backend/private/sso/check-domain.ts
  • supabase/functions/_backend/private/sso/check-enforcement.ts
  • supabase/functions/_backend/private/sso/prelink-internal.ts
  • supabase/functions/_backend/private/sso/providers.ts
  • supabase/functions/_backend/private/sso/provision-user.ts
  • supabase/functions/_backend/private/sso/verify-dns.ts
  • supabase/functions/_backend/utils/supabase-management.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • supabase/functions/_backend/private/sso/verify-dns.ts

…oning

The client-side check returned early if the user belonged to ANY org,
preventing the server from adding them to the SSO provider's specific
org. Now always calls the backend, which checks membership against
the resolved target org and handles idempotency.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/composables/useSSOProvisioning.ts (1)

57-68: ⚠️ Potential issue | 🟡 Minor

Surface provisioning failures via error.value and stop success flow.

/private/sso/provision-user failures are only logged right now, so callers can’t react to a failed auto-provision attempt.

💡 Suggested change
         if (!provisionResponse.ok) {
           const errorData = await provisionResponse.json().catch(() => ({ error: 'Unknown error' }))
           console.error('SSO provisioning: provision request failed', provisionResponse.status, errorData)
+          error.value = typeof errorData?.error === 'string'
+            ? errorData.error
+            : 'Failed to provision organization membership'
+          return
         }
@@
       catch (provisionError) {
         console.error('SSO provisioning: provision request error', provisionError)
+        error.value = 'Failed to provision organization membership'
+        return
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/composables/useSSOProvisioning.ts` around lines 57 - 68, The provisioning
branch currently only logs failures; update the error handling in the provision
flow so callers can react: when provisionResponse.ok is false, set the
composable's error.value to a meaningful object/string (using the parsed
errorData) and abort the success path (return or throw) so provisionData isn't
treated as success; likewise, in the catch block assign error.value =
provisionError (or a normalized message) before rethrowing or returning. Locate
and modify the logic around provisionResponse, provisionData, and provisionError
in useSSOProvisioning to ensure error.value is populated on failures and the
success branch is not executed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/composables/useSSOProvisioning.ts`:
- Around line 31-35: The current users pre-check in useSSOProvisioning aborts
provisioning on userError (it logs and returns), which blocks valid SSO logins
on transient DB/RLS errors; change the logic so that when userError is truthy
you log the error (keep the console.error line), set a non-fatal message if
desired (e.g. error.value = 'Failed to verify user account' or clear it) but do
NOT return—allow the code to continue to the server-side provisioning path (keep
the subsequent server call/invoke path intact), referencing the existing
userError check and error.value assignment to locate where to remove the early
return and make the check non-blocking.

---

Duplicate comments:
In `@src/composables/useSSOProvisioning.ts`:
- Around line 57-68: The provisioning branch currently only logs failures;
update the error handling in the provision flow so callers can react: when
provisionResponse.ok is false, set the composable's error.value to a meaningful
object/string (using the parsed errorData) and abort the success path (return or
throw) so provisionData isn't treated as success; likewise, in the catch block
assign error.value = provisionError (or a normalized message) before rethrowing
or returning. Locate and modify the logic around provisionResponse,
provisionData, and provisionError in useSSOProvisioning to ensure error.value is
populated on failures and the success branch is not executed.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c7441d and dc9659d.

📒 Files selected for processing (1)
  • src/composables/useSSOProvisioning.ts

Dalanir added 17 commits March 2, 2026 18:50
Both failure branches (HTTP error and network error) now set
error.value with a meaningful message instead of only logging to
console. HTTP failures also return early to prevent success handling.
Previously 4xx/5xx responses from check-enforcement fell through
to next(), letting the user proceed. Now any non-ok HTTP status
clears cache, signs user out, and redirects to login.
Replace 4 hardcoded English strings in the SSO section of Security.vue
with vue-i18n t() calls. Add corresponding keys (sso-configuration,
sso-configuration-description, sso-enterprise-upgrade-description,
sso-upgrade-to-enterprise) to all 15 locale JSON files with English
fallback values for non-English locales.
When getClientIP() returns 'unknown', the rate limiter was returning
false (not limited), allowing unlimited requests from clients that
strip or spoof IP headers. Now 'unknown' is used as the rate-limit
cache key, so all such requests share a single throttled bucket.
…ctivation

# Conflicts:
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/hi.json
#	messages/id.json
#	messages/it.json
#	messages/ja.json
#	messages/ko.json
#	messages/pl.json
#	messages/pt-br.json
#	messages/ru.json
#	messages/tr.json
#	messages/vi.json
#	messages/zh-cn.json
#	src/pages/login.vue
#	supabase/functions/_backend/utils/hono.ts
Backend: ENABLE_SSO env var controls /sso/* routes via Hono middleware
in both Supabase functions and Cloudflare Workers. Returns 404 when off.

Frontend: VITE_ENABLE_SSO controls SSO enforcement module, domain
checking on login, SSO config section in Security settings, and
SSO callback page. All skip gracefully when disabled.
… column

- Add sso_enabled boolean (default false) to orgs table
- Update check_domain_sso RPC to check orgs.sso_enabled
- Update get_orgs_v7 to return sso_enabled for frontend
- Remove global SSO middleware from private/index.ts and cloudflare workers
- Remove ENABLE_SSO env var from .env.example and supabase.ts
- Frontend uses currentOrganization.sso_enabled instead of global ref
- SSO provider creation checks org-level sso_enabled flag
- Update SSO tests with sso_enabled: true on test org
- Add sso_enabled to TypeScript types
@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 3, 2026

@Dalanir Dalanir merged commit 4353ca8 into main Mar 3, 2026
15 checks passed
@Dalanir Dalanir deleted the feat/enterprise-sso-activation branch March 3, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant