Skip to content

fix(sso): check providers array instead of provider for SSO auth#1833

Merged
Dalanir merged 2 commits into
mainfrom
fix-sso-translation-auth
Mar 19, 2026
Merged

fix(sso): check providers array instead of provider for SSO auth#1833
Dalanir merged 2 commits into
mainfrom
fix-sso-translation-auth

Conversation

@Dalanir
Copy link
Copy Markdown
Contributor

@Dalanir Dalanir commented Mar 19, 2026

Summary

  • Root cause: app_metadata.provider (singular) is rewritten by Supabase Auth on every login based on the oldest/primary identity. After an account merge where the original user has an email identity, Supabase resets provider='email' even when the user logs in via SSO — causing sso_auth_required on subsequent logins.
  • Fix: Check app_metadata.providers[] (array) instead. This is cumulative — Supabase adds sso:X once and never removes it, so the check is stable regardless of which provider was used last.

Test plan

  • First SSO login on a domain where user has an existing email/password account → merge happens → redirect to login
  • Second SSO login → should reach dashboard without sso_auth_required
  • SSO login on a domain without a pre-existing account → provisioned normally
  • Email/password login (when SSO not enforced) → unaffected

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced SSO authentication to properly handle users with multiple linked providers (checks both singular and array provider declarations).
    • Expanded SSO detection to recognize both exact "sso" and "sso:*" provider formats.
    • Improved provider validation and expanded logging to include the full providers list for clearer troubleshooting.

…dation

app_metadata.provider (singular) is rewritten by Supabase Auth on every login
based on the oldest/primary identity — after an account merge where the original
user has an email identity, Supabase resets provider='email' even when the user
logs in via SSO. providers[] (plural) is cumulative and retains sso:X once set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 19, 2026

📝 Walkthrough

Walkthrough

Updated SSO detection and validation to read multiple providers from app_metadata.providers and treat both 'sso' and 'sso:*' values as SSO. Provisioning and enforcement now allow SSO when either the singular provider or any entry in providers matches; logging and downstream trusted-provider use were adjusted accordingly.

Changes

Cohort / File(s) Summary
Provisioning handler
supabase/functions/_backend/private/sso/provision-user.ts
Derive userProviders from user.app_metadata?.providers ?? []; accept SSO when provider === 'sso' or any providers entry starts with sso:; include providers in rejection logs; pass userProvider ?? '' into getTrustedSsoProviders.
Enforcement check
supabase/functions/_backend/private/sso/check-enforcement.ts
Extract providers (default []) from JWT app_metadata and introduce isSsoProvider predicate; compute isSsoAuth true if singular provider or any providers entry matches SSO pattern; log providers in success path.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰🌿 I nibbled through app_metadata arrays,
Finding 'sso' in winding ways.
Providers lined up, each one checked true,
I hop, I log — trusted names anew. 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description provides a clear root cause explanation and fix, but the test plan section uses unchecked checkboxes without confirmation of test execution, and required checklist items are not completed. Complete the test plan by confirming which scenarios were tested and their outcomes. Fill out the backend linting and testing checklist items to indicate that code style and E2E test coverage requirements have been met.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: shifting SSO authentication validation from checking a single provider field to checking the providers array.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-sso-translation-auth
📝 Coding Plan
  • Generate coding plan for human review comments

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
Copy Markdown
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

🧹 Nitpick comments (1)
supabase/functions/_backend/private/sso/provision-user.ts (1)

127-127: Passing empty string to getTrustedSsoProviders may add '' to trusted providers.

When userProvider is undefined, this passes '' to getTrustedSsoProviders. Looking at that function (lines 45-61), the predicate isTrustedSsoProvider includes provider === userProvider, which would evaluate to '' === ''true, causing an empty string to be added to the trusted providers set.

While this won't cause runtime failures (the line 121 check would reject cases where both userProvider and userProviders are empty/non-SSO), the resulting ['', 'sso:...'] array is semantically incorrect and could match unintended rows in database queries.

♻️ Proposed fix: guard against empty userProvider
-    const trustedSsoProviders = getTrustedSsoProviders(userProvider ?? '', userIdentities)
+    const trustedSsoProviders = getTrustedSsoProviders(userProvider || null, userIdentities)

And update the function signature to handle null:

-function getTrustedSsoProviders(userProvider: string, userIdentities: any[]): string[] {
+function getTrustedSsoProviders(userProvider: string | null, userIdentities: any[]): string[] {
   const trustedProviders = new Set<string>()
-  const isTrustedSsoProvider = (provider: string) => provider === 'sso' || provider.startsWith('sso:') || provider === userProvider
+  const isTrustedSsoProvider = (provider: string) => provider === 'sso' || provider.startsWith('sso:') || (userProvider && provider === userProvider)

-  if (isTrustedSsoProvider(userProvider)) {
+  if (userProvider && isTrustedSsoProvider(userProvider)) {
     trustedProviders.add(userProvider)
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/private/sso/provision-user.ts` at line 127, The
call to getTrustedSsoProviders currently passes userProvider ?? '' which allows
an empty string to be treated as a trusted provider; change the caller to pass
userProvider (or null/undefined) directly instead of coercing to '' and update
getTrustedSsoProviders signature/logic so its predicate (isTrustedSsoProvider)
explicitly guards for a non-empty userProvider (e.g., check provider ===
userProvider only when userProvider is truthy) to prevent '' being added to the
trusted providers set; refer to the symbols userProvider,
getTrustedSsoProviders, and isTrustedSsoProvider when making the change.
🤖 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/provision-user.ts`:
- Around line 119-124: The enforcement check only inspects
claims?.app_metadata?.provider and treats 'email' as non-SSO; update the logic
in check-enforcement.ts to mirror provision-user.ts by checking both
app_metadata.provider and app_metadata.providers (array). Implement an
isSsoProvider helper (e.g., p => p === 'sso' || p.startsWith('sso:')) and set
isSsoAuth to true if either provider passes isSsoProvider or any entry in
providers[] passes isSsoProvider, then use that boolean for enforcement
decisions (replace the current provider/isSsoAuth logic).

---

Nitpick comments:
In `@supabase/functions/_backend/private/sso/provision-user.ts`:
- Line 127: The call to getTrustedSsoProviders currently passes userProvider ??
'' which allows an empty string to be treated as a trusted provider; change the
caller to pass userProvider (or null/undefined) directly instead of coercing to
'' and update getTrustedSsoProviders signature/logic so its predicate
(isTrustedSsoProvider) explicitly guards for a non-empty userProvider (e.g.,
check provider === userProvider only when userProvider is truthy) to prevent ''
being added to the trusted providers set; refer to the symbols userProvider,
getTrustedSsoProviders, and isTrustedSsoProvider when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b320606a-6f66-4748-9cfe-6634ef670545

📥 Commits

Reviewing files that changed from the base of the PR and between 67a65c9 and b2317c8.

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

Comment thread supabase/functions/_backend/private/sso/provision-user.ts
…er.ts

Same root cause: after account merge Supabase resets app_metadata.provider to
'email' while providers[] retains sso:X. Without this fix, SSO-merged users
would be incorrectly blocked by the enforcement check on email/password login
attempts, even though they authenticated via SSO.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
supabase/functions/_backend/private/sso/check-enforcement.ts (1)

33-39: Good fix for the SSO detection stability issue.

The approach of checking both provider (singular) and providers[] (array) correctly addresses the Supabase account-merge behavior described in the PR. The logic maintains backward compatibility by still checking the singular provider while adding the cumulative array check.

One optional hardening: the providers array elements are assumed to be strings. If Supabase ever stores a non-string value, startsWith() would throw. Consider filtering for type safety:

💡 Optional defensive typing
-  const providers: string[] = Array.isArray(claims?.app_metadata?.providers) ? claims.app_metadata.providers as string[] : []
+  const providers: string[] = Array.isArray(claims?.app_metadata?.providers)
+    ? (claims.app_metadata.providers as unknown[]).filter((p): p is string => typeof p === 'string')
+    : []
🤖 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 33
- 39, The current SSO detection uses providers array elements directly which can
throw if a non-string appears; update the logic around provider, providers,
isSsoProvider, and isSsoAuth to defensively coerce/filter providers to strings
before testing—e.g., ensure providers is filtered/ mapped to string values (or
skip non-strings) and then apply isSsoProvider and providers.some(isSsoProvider)
so startsWith is only called on strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@supabase/functions/_backend/private/sso/check-enforcement.ts`:
- Around line 33-39: The current SSO detection uses providers array elements
directly which can throw if a non-string appears; update the logic around
provider, providers, isSsoProvider, and isSsoAuth to defensively coerce/filter
providers to strings before testing—e.g., ensure providers is filtered/ mapped
to string values (or skip non-strings) and then apply isSsoProvider and
providers.some(isSsoProvider) so startsWith is only called on strings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: de34d671-97df-464f-9923-60408ec80374

📥 Commits

Reviewing files that changed from the base of the PR and between b2317c8 and eab9300.

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

@Dalanir Dalanir merged commit a269c51 into main Mar 19, 2026
13 checks passed
@Dalanir Dalanir deleted the fix-sso-translation-auth branch March 19, 2026 16:54
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant