Skip to content

fix(web): Support multiple IDPs of the same type#1177

Open
brendan-kellam wants to merge 8 commits into
mainfrom
bkellam/multi-idp
Open

fix(web): Support multiple IDPs of the same type#1177
brendan-kellam wants to merge 8 commits into
mainfrom
bkellam/multi-idp

Conversation

@brendan-kellam

@brendan-kellam brendan-kellam commented May 5, 2026

Copy link
Copy Markdown
Contributor

This PR adds support for multiple IDP providers of the same type. This is mostly useful in the scenario where you have code hosted in e.g., gitlab.com and a self-hosted gitlab instance and you want to use permission syncing. In this scenario, users need a mechanism of linking their account from gitlab.com and the self-hosted gitlab deployment.

Here's a example config:

"identityProviders": {
    "gitlab-self-hosted": {
        "provider": "gitlab",
        "purpose": "account_linking",
        "baseUrl": "https://gitlab.sourcebot.dev",
        "clientId": {
            "env": "GITLAB_SH_CLIENT_ID"
        },
        "clientSecret": {
            "env": "GITLAB_SH_CLIENT_SECRET"
        },
        "displayName": "GitLab Self Hosted"
    },
    "gitlab-dot-com": {
        "provider": "gitlab",
        "purpose": "account_linking",
        "clientId": {
            "env": "GITLAB_DOT_COM_CLIENT_ID"
        },
        "clientSecret": {
            "env": "GITLAB_DOT_COM_CLIENT_SECRET"
        },
        "displayName": "GitLab.com"
    }
}

TODOs:

  • Document the object form of identityProviders
  • Add additional UTs
  • Test back-compat scenario

Summary by CodeRabbit

  • New Features

    • Added optional custom display names for identity providers on login screens.
    • Identity provider configuration now supports both object map and array formats for greater flexibility.
    • Full support for multiple instances of the same identity provider type.
  • Bug Fixes

    • Resolved unexpected behavior when using multiple identity providers of the same type.

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d34752fb-fb66-4e12-9799-6d8c4ec2739b

📥 Commits

Reviewing files that changed from the base of the PR and between 1732ec4 and 501abec.

📒 Files selected for processing (20)
  • CHANGELOG.md
  • docs/snippets/schemas/v3/identityProvider.schema.mdx
  • docs/snippets/schemas/v3/index.schema.mdx
  • packages/backend/src/api.ts
  • packages/backend/src/ee/accountPermissionSyncer.ts
  • packages/backend/src/ee/repoPermissionSyncer.ts
  • packages/backend/src/ee/tokenRefresh.ts
  • packages/db/prisma/migrations/20260610025101_add_provider_id_and_type_to_account/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/schemas/src/v3/identityProvider.schema.ts
  • packages/schemas/src/v3/identityProvider.type.ts
  • packages/schemas/src/v3/index.schema.ts
  • packages/schemas/src/v3/index.type.ts
  • packages/shared/src/constants.ts
  • packages/shared/src/env.server.ts
  • packages/shared/src/index.server.ts
  • packages/web/src/app/(app)/settings/linked-accounts/page.tsx
  • packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts
  • packages/web/src/app/components/authMethodSelector.tsx
  • packages/web/src/app/login/components/loginForm.tsx
💤 Files with no reviewable changes (3)
  • packages/web/src/app/login/components/loginForm.tsx
  • packages/web/src/app/components/authMethodSelector.tsx
  • packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/shared/src/index.server.ts
  • CHANGELOG.md
  • docs/snippets/schemas/v3/identityProvider.schema.mdx
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/web/src/app/(app)/settings/linked-accounts/page.tsx
  • packages/backend/src/ee/repoPermissionSyncer.ts
  • packages/schemas/src/v3/identityProvider.type.ts
  • packages/shared/src/constants.ts
  • packages/schemas/src/v3/identityProvider.schema.ts
  • packages/db/prisma/schema.prisma
  • packages/backend/src/ee/tokenRefresh.ts
  • packages/shared/src/env.server.ts
  • packages/schemas/src/v3/index.type.ts
  • packages/backend/src/ee/accountPermissionSyncer.ts

Walkthrough

This PR refactors identity-provider handling to support multiple instances of the same type by replacing a single provider field with providerId and providerType. Database schema, config contracts, shared helpers, authentication wiring, backend sync flows, and frontend UI were all updated to use the new identity model.

Changes

Identity provider model and flow migration

Layer / File(s) Summary
Identity provider contracts and schema surface
packages/schemas/src/v3/index.type.ts, packages/schemas/src/v3/identityProvider.type.ts, packages/schemas/src/v3/identityProvider.schema.ts, schemas/v3/identityProvider.json, schemas/v3/index.json, docs/snippets/schemas/v3/identityProvider.schema.mdx
Type definitions, TypeScript schemas, and JSON schemas now allow object-keyed identityProviders (mapping config id to IdentityProviderConfig) and add an optional displayName field across all 11 provider types (GitHub, GitLab, Google, Okta, Keycloak, Microsoft Entra ID, GCP IAP, Bitbucket Cloud, Authentik, JumpCloud, Bitbucket Server) in both definitions and top-level oneOf entries.
Account persistence model and database migration
packages/db/prisma/schema.prisma, packages/db/prisma/migrations/20260610025101_add_provider_id_and_type_to_account/migration.sql
Account table replaces provider: String with providerId: String and adds new required providerType: String, updating the uniqueness constraint from (provider, providerAccountId) to (providerId, providerAccountId). Migration renames the column, adds and backfills providerType, and enforces it as NOT NULL.
Shared config normalization and helper exports
packages/shared/src/constants.ts, packages/shared/src/env.server.ts, packages/shared/src/index.server.ts
New exported types NormalizedIdentityProviders and NormalizedSourcebotConfig represent identity providers in canonical object form; loadConfig and resolveEnvironmentVariableOverridesFromConfig now accept/return normalized config; new doesIdpSupportPermissionSyncing(providerType) type-guard helper replaces hardcoded provider lists; normalizeIdentityProviders converts deprecated array form to object form and validates for duplicate synthesized ids.
Auth adapter and token persistence with provider remapping
packages/web/src/lib/encryptedPrismaAdapter.ts, packages/web/src/auth.ts
EncryptedPrismaAdapter now fully overrides linkAccount, getUserByAccount, and unlinkAccount to map auth.js account.provider into Prisma providerId and resolved providerType via a new resolveProviderType helper; auth.ts refactored to store NextAuth provider factories under __provider and construct provider objects with explicit type, id, and purpose metadata; issuer URL helpers updated to use getIssuerUrlForProviderId(providerId) instead of account-shape-based lookups.
Provider registry construction and NextAuth wiring
packages/web/src/ee/features/sso/sso.ts
getEEIdentityProviders now iterates config.identityProviders as an object map via Object.entries, passing configured provider id to all provider factory helpers (GitHub, GitLab, Google, Okta, Keycloak, Microsoft Entra ID, GCP IAP, Bitbucket Cloud, Bitbucket Server, Authentik, JumpCloud, GCP IAP); legacy env-var fallback now activates only when config is empty and supplies explicit id/type to factories; exported createAuthentikProvider signature updated to include id parameter.
Permission sync and account lookup refactor
packages/backend/src/api.ts, packages/backend/src/ee/accountPermissionSyncer.ts, packages/backend/src/ee/repoPermissionSyncer.ts
Account selection and permission-sync queries now filter by providerType in the supported identity providers list instead of provider; permission sync for GitHub, GitLab, and Bitbucket Cloud/Server now loads identity-provider config by providerId and uses idpConfig.baseUrl for OAuth client creation; repo lookups conditionally filter by external_codeHostUrl when account.issuerUrl is present; debug/error logging switched to use providerId and providerType.
OAuth token refresh with provider type dispatch
packages/backend/src/ee/tokenRefresh.ts
Token refresh flow now requires providerType to be supported (via type-guard), loads identity-provider config by providerId, and falls back to deprecated env credentials only when config is missing; refreshOAuthToken signature updated to accept (providerId, providerType, refreshToken) separately; token endpoint URL selection switched to dispatch exclusively on providerType instead of provider field; error and debug messages updated to reference providerId/providerType.
Frontend auth provider info and linked account type updates
packages/web/src/lib/utils.ts, packages/web/src/lib/identityProviders.ts, packages/web/src/ee/features/sso/actions.ts, packages/web/src/ee/features/sso/components/connectAccountsCard.tsx, packages/web/src/ee/features/sso/components/linkedAccountProviderCard.tsx, packages/web/src/app/(app)/settings/linked-accounts/page.tsx
getAuthProviderInfo now accepts providerType instead of providerId; IdentityProviderMetadata now exposes type and optional displayName instead of name; LinkedAccount type split into providerId and providerType with optional displayName; getLinkedAccounts refactored to fetch and populate providerId/providerType/displayName from normalized config, and unlinkLinkedAccountProvider now accepts providerId; list keys updated to use providerId; UI components updated to derive displayName from linkedAccount.displayName or getAuthProviderInfo(linkedAccount.providerType).
Login form and auth method selector type-based behavior
packages/web/src/app/components/authMethodSelector.tsx, packages/web/src/app/login/components/loginForm.tsx, packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts
AuthMethodSelector now filters OAuth vs special-method providers using provider.type instead of provider.id; LoginForm now resolves analytics event names from provider type via useIdentityProviders hook; permission sync status query updated to constrain providerType using PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS instead of hardcoded provider list.
Changelog entry for fix
CHANGELOG.md
Added entry under ## [Unreleased]### Fixed documenting that multiple identity providers of the same type no longer cause unexpected behavior.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • sourcebot-dev/sourcebot#729: Modifies accountPermissionSyncer.ts permission-sync base URL handling, now tied to identity provider idpConfig.baseUrl and providerType in this PR.
  • sourcebot-dev/sourcebot#849: Modifies SSO provider construction in sso.ts including createAuthentikProvider factory signature, also updated in this PR.
  • sourcebot-dev/sourcebot#934: Introduces Bitbucket Server as a supported provider; this PR adds optional displayName field to that provider's config schema.

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(web): Support multiple IDPs of the same type' directly aligns with the PR's main objective of enabling multiple identity providers of the same type, which is thoroughly documented in the PR summary and demonstrated through extensive schema, database, and backend changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 bkellam/multi-idp

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.

@brendan-kellam brendan-kellam force-pushed the bkellam/multi-idp branch 2 times, most recently from 32010eb to cc9dcc0 Compare May 5, 2026 22:53
@brendan-kellam brendan-kellam marked this pull request as ready for review May 5, 2026 23:07
@brendan-kellam brendan-kellam changed the title wip: multi-idp support fix(web): Support multiple IDPs of the same type May 5, 2026
@brendan-kellam

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@brendan-kellam brendan-kellam changed the base branch from v5 to main June 10, 2026 02:58
@brendan-kellam brendan-kellam changed the base branch from main to v5 June 10, 2026 02:58
@brendan-kellam brendan-kellam changed the base branch from v5 to main June 10, 2026 03:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
packages/backend/src/ee/tokenRefresh.ts (1)

154-257: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use providerId when building the GitLab refresh callback.

The refresh path still hardcodes redirect_uri to /api/auth/callback/gitlab. After this PR, secondary GitLab providers authorize against /api/auth/callback/<providerId>, and GitLab expects refreshes to reuse that same URI. Custom-id GitLab accounts will stop refreshing once their access token expires.

Proposed fix
 const refreshOAuthToken = async (
     providerId: string,
     providerType: SupportedProviderType,
     refreshToken: string,
 ): Promise<OAuthTokenResponse | null> => {
@@
-                const result = await tryRefreshToken(providerType, refreshToken, envCredentials);
+                const result = await tryRefreshToken(providerId, providerType, refreshToken, envCredentials);
@@
-        const result = await tryRefreshToken(providerType, refreshToken, { clientId, clientSecret, baseUrl });
+        const result = await tryRefreshToken(providerId, providerType, refreshToken, { clientId, clientSecret, baseUrl });
@@
 const tryRefreshToken = async (
+    providerId: string,
     providerType: SupportedProviderType,
     refreshToken: string,
     credentials: ProviderCredentials,
 ): Promise<OAuthTokenResponse | null> => {
@@
     if (providerType === 'gitlab') {
-        bodyParams.redirect_uri = new URL('/api/auth/callback/gitlab', env.AUTH_URL).toString();
+        bodyParams.redirect_uri = new URL(`/api/auth/callback/${providerId}`, env.AUTH_URL).toString();
     }

Based on the PR objective and stack context that auth callbacks now use configured provider ids.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/backend/src/ee/tokenRefresh.ts` around lines 154 - 257, The GitLab
redirect_uri is hardcoded to "/api/auth/callback/gitlab" in tryRefreshToken;
change tryRefreshToken to accept providerId (add parameter providerId: string)
and update both call sites in refreshOAuthToken (the envCredentials branch and
the idpConfig branch) to pass providerId into tryRefreshToken; then build
redirect_uri using `/api/auth/callback/${providerId}` when providerType ===
'gitlab' so secondary/custom-id GitLab providers reuse their provider-specific
callback. Ensure the updated tryRefreshToken signature is applied wherever it’s
called.
packages/web/src/app/login/components/loginForm.tsx (1)

46-65: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unhandled provider types are being tracked as GitHub logins.

The default branch now records every unsupported providerType as wa_login_with_github, so providers like authentik, bitbucket-cloud, and bitbucket-server will be misattributed in analytics. Please add explicit mappings for the supported OAuth types here, or fall back to a neutral/unknown event instead of GitHub.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/login/components/loginForm.tsx` around lines 46 - 65,
The function getLoginEventName currently maps unknown providerType values to
"wa_login_with_github", causing misattribution; update getLoginEventName to add
explicit cases for additional supported providers (e.g., "authentik",
"bitbucket-cloud", "bitbucket-server") mapping to distinct analytics event
names, and change the default fallback to a neutral/unknown event (e.g.,
"wa_login_with_unknown") instead of GitHub so unsupported providers aren’t
tracked as GitHub; locate and modify the switch inside getLoginEventName to add
the new case labels and adjust the default return accordingly.
🧹 Nitpick comments (3)
packages/web/src/lib/encryptedPrismaAdapter.ts (2)

49-60: 💤 Low value

Ensure linkAccount handles async errors gracefully.

resolveProviderType can fail if config loading throws (e.g., disk error, malformed YAML). The current implementation does not wrap the call in try-catch, so an exception will propagate and fail the OAuth flow. Consider whether this is the desired behavior or if a fallback is needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/lib/encryptedPrismaAdapter.ts` around lines 49 - 60, Wrap
the resolveProviderType(account.provider) call in a try/catch inside linkAccount
so async failures don’t bubble out and break the OAuth flow; on error capture
the original error, log it (e.g., console.error or your logger), set a safe
fallback value for providerType (e.g., 'unknown' or null), then continue to call
encryptAccountTokens(account) and prisma.account.create(...) as before;
alternatively, if you prefer failing fast, rethrow a new Error that includes
context and the original error to make the failure explicit.

28-35: Cache the config-derived provider mapping in resolveProviderType

  • packages/web/src/lib/encryptedPrismaAdapter.ts calls loadConfig(env.CONFIG_PATH) inside resolveProviderType, so every linkAccount triggers config fetch/readFile, JSON parse, and AJV validation.
  • packages/shared/src/env.server.ts loadConfig has no module-scope memoization; it directly reads from the config path each time, so concurrent OAuth callbacks can cause repeated I/O/CPU.
  • Cache the normalized identityProviders (or the resolved config) behind a module-scope promise (optionally with TTL/invalidation if the config can change at runtime).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/lib/encryptedPrismaAdapter.ts` around lines 28 - 35,
resolveProviderType currently calls loadConfig(env.CONFIG_PATH) on every
invocation causing repeated I/O/parse/validation; introduce a module-scope
cached promise or variable (e.g., cachedConfigPromise or
cachedIdentityProviders) that loads and normalizes config.identityProviders once
and is reused by resolveProviderType (falling back to providerId if mapping
missing); ensure the cache is populated by calling loadConfig(env.CONFIG_PATH)
once and storing either the full resolved config or a mapping, and optionally
add a TTL/invalidate function if runtime config changes must be supported.
packages/web/src/ee/features/sso/actions.ts (1)

33-35: 🏗️ Heavy lift

Move getLinkedAccounts out of the server-action module.

This is still a read path inside a 'use server' actions file. Repo guidance reserves server actions for mutations, so this fetch should live in a plain server helper (or an API route if the client needs it), while this module stays focused on mutating actions.

As per coding guidelines, packages/web/src/**/*.ts: "Server actions should be used for mutations (POST/PUT/DELETE operations), not for data fetching."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/ee/features/sso/actions.ts` around lines 33 - 35,
getLinkedAccounts is a read-only data fetch currently defined in the
server-actions module; move it out into a plain server helper (or an API route)
so 'use server' actions remain mutation-only. Create a new non-action module and
export the function there, preserving the implementation that wraps
sew/withAuth/withMinimumOrgRole (references: getLinkedAccounts, sew, withAuth,
withMinimumOrgRole, OrgRole.MEMBER), then update any imports that consume
getLinkedAccounts to point to the new helper and remove it from the actions file
so the actions module contains only mutation functions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 53-54: Move the changelog entry "- Fixed issue where using
multiple identity providers of the same type (e.g., gitlab) would result in
unexpected behaviours.
[`#1177`](https://github.com/sourcebot-dev/sourcebot/pull/1177)" out of the
historical "4.17.2" release block and append it to the bottom of the
"[Unreleased]" section; ensure it remains under the "### Fixed" subsection
within that “[Unreleased]” header so the PR entry follows the repository's
changelog placement rules.

In `@packages/backend/src/ee/accountPermissionSyncer.ts`:
- Around line 245-251: syncAccountPermissions currently throws when
config.identityProviders[account.providerId] is missing, breaking installs that
rely on legacy auth env vars; change the lookup for idpConfig in
syncAccountPermissions to fall back to the existing legacy method used by
ensureFreshAccountToken (reuse the same fallback logic/path) so that when
config.identityProviders is undefined or does not contain account.providerId you
use the legacy env-var-based IDP config instead of throwing; modify the
idpConfig resolution (the variable named idpConfig and its lookup of
config.identityProviders[account.providerId]) to attempt the config.json value
first and then the legacy fallback used elsewhere, preserving the thrown error
only if both are absent.

In `@packages/web/src/ee/features/sso/actions.ts`:
- Around line 77-88: The loop in the SSO action that iterates over
Object.entries(config.identityProviders ?? {}) is adding all unlinked providers
to the result; change it to only consider providers intended for account linking
by filtering where providerConfig.purpose === 'account_linking' before pushing
into result (i.e., restrict the iteration or add an if that skips
non-account_linking providers), keeping the existing fields (providerId,
providerType, displayName, isLinked, isAccountLinkingProvider, required,
supportsPermissionSync) and preserving the required =
providerConfig.accountLinkingRequired fallback logic and permission sync check
via doesIdpSupportPermissionSyncing.

---

Outside diff comments:
In `@packages/backend/src/ee/tokenRefresh.ts`:
- Around line 154-257: The GitLab redirect_uri is hardcoded to
"/api/auth/callback/gitlab" in tryRefreshToken; change tryRefreshToken to accept
providerId (add parameter providerId: string) and update both call sites in
refreshOAuthToken (the envCredentials branch and the idpConfig branch) to pass
providerId into tryRefreshToken; then build redirect_uri using
`/api/auth/callback/${providerId}` when providerType === 'gitlab' so
secondary/custom-id GitLab providers reuse their provider-specific callback.
Ensure the updated tryRefreshToken signature is applied wherever it’s called.

In `@packages/web/src/app/login/components/loginForm.tsx`:
- Around line 46-65: The function getLoginEventName currently maps unknown
providerType values to "wa_login_with_github", causing misattribution; update
getLoginEventName to add explicit cases for additional supported providers
(e.g., "authentik", "bitbucket-cloud", "bitbucket-server") mapping to distinct
analytics event names, and change the default fallback to a neutral/unknown
event (e.g., "wa_login_with_unknown") instead of GitHub so unsupported providers
aren’t tracked as GitHub; locate and modify the switch inside getLoginEventName
to add the new case labels and adjust the default return accordingly.

---

Nitpick comments:
In `@packages/web/src/ee/features/sso/actions.ts`:
- Around line 33-35: getLinkedAccounts is a read-only data fetch currently
defined in the server-actions module; move it out into a plain server helper (or
an API route) so 'use server' actions remain mutation-only. Create a new
non-action module and export the function there, preserving the implementation
that wraps sew/withAuth/withMinimumOrgRole (references: getLinkedAccounts, sew,
withAuth, withMinimumOrgRole, OrgRole.MEMBER), then update any imports that
consume getLinkedAccounts to point to the new helper and remove it from the
actions file so the actions module contains only mutation functions.

In `@packages/web/src/lib/encryptedPrismaAdapter.ts`:
- Around line 49-60: Wrap the resolveProviderType(account.provider) call in a
try/catch inside linkAccount so async failures don’t bubble out and break the
OAuth flow; on error capture the original error, log it (e.g., console.error or
your logger), set a safe fallback value for providerType (e.g., 'unknown' or
null), then continue to call encryptAccountTokens(account) and
prisma.account.create(...) as before; alternatively, if you prefer failing fast,
rethrow a new Error that includes context and the original error to make the
failure explicit.
- Around line 28-35: resolveProviderType currently calls
loadConfig(env.CONFIG_PATH) on every invocation causing repeated
I/O/parse/validation; introduce a module-scope cached promise or variable (e.g.,
cachedConfigPromise or cachedIdentityProviders) that loads and normalizes
config.identityProviders once and is reused by resolveProviderType (falling back
to providerId if mapping missing); ensure the cache is populated by calling
loadConfig(env.CONFIG_PATH) once and storing either the full resolved config or
a mapping, and optionally add a TTL/invalidate function if runtime config
changes must be supported.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5ba93271-72e6-4824-972b-bbe591dcc18b

📥 Commits

Reviewing files that changed from the base of the PR and between ded4282 and 1732ec4.

📒 Files selected for processing (30)
  • CHANGELOG.md
  • docs/snippets/schemas/v3/identityProvider.schema.mdx
  • docs/snippets/schemas/v3/index.schema.mdx
  • packages/backend/src/api.ts
  • packages/backend/src/ee/accountPermissionSyncer.ts
  • packages/backend/src/ee/repoPermissionSyncer.ts
  • packages/backend/src/ee/tokenRefresh.ts
  • packages/db/prisma/migrations/20260530203443_add_provider_id_and_type_to_account/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/schemas/src/v3/identityProvider.schema.ts
  • packages/schemas/src/v3/identityProvider.type.ts
  • packages/schemas/src/v3/index.schema.ts
  • packages/schemas/src/v3/index.type.ts
  • packages/shared/src/constants.ts
  • packages/shared/src/env.server.ts
  • packages/shared/src/index.server.ts
  • packages/web/src/app/(app)/settings/linked-accounts/page.tsx
  • packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts
  • packages/web/src/app/components/authMethodSelector.tsx
  • packages/web/src/app/login/components/loginForm.tsx
  • packages/web/src/auth.ts
  • packages/web/src/ee/features/sso/actions.ts
  • packages/web/src/ee/features/sso/components/connectAccountsCard.tsx
  • packages/web/src/ee/features/sso/components/linkedAccountProviderCard.tsx
  • packages/web/src/ee/features/sso/sso.ts
  • packages/web/src/lib/encryptedPrismaAdapter.ts
  • packages/web/src/lib/identityProviders.ts
  • packages/web/src/lib/utils.ts
  • schemas/v3/identityProvider.json
  • schemas/v3/index.json

Comment thread CHANGELOG.md Outdated
Comment on lines +245 to +251
const idpConfig = config.identityProviders ?
config.identityProviders[account.providerId] :
undefined;

if (!idpConfig) {
throw new Error(`Unable to find IDP config in config.json.`);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep a legacy fallback when identityProviders[providerId] is missing.

syncAccountPermissions() now hard-fails unless the linked account resolves to config.identityProviders[account.providerId]. That regresses installs still using the deprecated auth env vars: ensureFreshAccountToken() can still refresh those accounts, but every permission-sync job will then die here with Unable to find IDP config in config.json. Please reuse the same legacy fallback here instead of requiring config.json outright.

Based on the PR objective's backward-compatibility requirement and the provided token-refresh fallback context.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/backend/src/ee/accountPermissionSyncer.ts` around lines 245 - 251,
syncAccountPermissions currently throws when
config.identityProviders[account.providerId] is missing, breaking installs that
rely on legacy auth env vars; change the lookup for idpConfig in
syncAccountPermissions to fall back to the existing legacy method used by
ensureFreshAccountToken (reuse the same fallback logic/path) so that when
config.identityProviders is undefined or does not contain account.providerId you
use the legacy env-var-based IDP config instead of throwing; modify the
idpConfig resolution (the variable named idpConfig and its lookup of
config.identityProviders[account.providerId]) to attempt the config.json value
first and then the legacy fallback used elsewhere, preserving the thrown error
only if both are absent.

Comment on lines +77 to 88
for (const [id, providerConfig] of Object.entries(config.identityProviders ?? {})) {
const account = accounts.find((account) => account.providerId === id);
if (!account) {
result.push({
provider: providerConfig.provider,
providerId: id,
providerType: providerConfig.provider,
displayName: providerConfig.displayName,
isLinked: false,
isAccountLinkingProvider: providerConfig.purpose === 'account_linking',
required: providerConfig.purpose === 'account_linking' ? (providerConfig.accountLinkingRequired ?? false) : false,
supportsPermissionSync: permissionSyncEnabled && PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS.includes(providerConfig.provider),
supportsPermissionSync: permissionSyncEnabled && doesIdpSupportPermissionSyncing(providerConfig.provider),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Filter the “unlinked” list to account_linking providers before pushing.

This loop currently adds every configured-but-unlinked provider. packages/web/src/app/(app)/settings/linked-accounts/page.tsx renders the full getLinkedAccounts() result, so login-only providers will now show up as connectable linked accounts in Settings.

Suggested fix
 for (const [id, providerConfig] of Object.entries(config.identityProviders ?? {})) {
+    if (providerConfig.purpose !== 'account_linking') {
+        continue;
+    }
+
     const account = accounts.find((account) => account.providerId === id);
     if (!account) {
         result.push({
             providerId: id,
             providerType: providerConfig.provider,
             displayName: providerConfig.displayName,
             isLinked: false,
-            isAccountLinkingProvider: providerConfig.purpose === 'account_linking',
-            required: providerConfig.purpose === 'account_linking' ? (providerConfig.accountLinkingRequired ?? false) : false,
+            isAccountLinkingProvider: true,
+            required: providerConfig.accountLinkingRequired ?? false,
             supportsPermissionSync: permissionSyncEnabled && doesIdpSupportPermissionSyncing(providerConfig.provider),
         });
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const [id, providerConfig] of Object.entries(config.identityProviders ?? {})) {
const account = accounts.find((account) => account.providerId === id);
if (!account) {
result.push({
provider: providerConfig.provider,
providerId: id,
providerType: providerConfig.provider,
displayName: providerConfig.displayName,
isLinked: false,
isAccountLinkingProvider: providerConfig.purpose === 'account_linking',
required: providerConfig.purpose === 'account_linking' ? (providerConfig.accountLinkingRequired ?? false) : false,
supportsPermissionSync: permissionSyncEnabled && PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS.includes(providerConfig.provider),
supportsPermissionSync: permissionSyncEnabled && doesIdpSupportPermissionSyncing(providerConfig.provider),
});
for (const [id, providerConfig] of Object.entries(config.identityProviders ?? {})) {
if (providerConfig.purpose !== 'account_linking') {
continue;
}
const account = accounts.find((account) => account.providerId === id);
if (!account) {
result.push({
providerId: id,
providerType: providerConfig.provider,
displayName: providerConfig.displayName,
isLinked: false,
isAccountLinkingProvider: true,
required: providerConfig.accountLinkingRequired ?? false,
supportsPermissionSync: permissionSyncEnabled && doesIdpSupportPermissionSyncing(providerConfig.provider),
});
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/ee/features/sso/actions.ts` around lines 77 - 88, The loop
in the SSO action that iterates over Object.entries(config.identityProviders ??
{}) is adding all unlinked providers to the result; change it to only consider
providers intended for account linking by filtering where providerConfig.purpose
=== 'account_linking' before pushing into result (i.e., restrict the iteration
or add an if that skips non-account_linking providers), keeping the existing
fields (providerId, providerType, displayName, isLinked,
isAccountLinkingProvider, required, supportsPermissionSync) and preserving the
required = providerConfig.accountLinkingRequired fallback logic and permission
sync check via doesIdpSupportPermissionSyncing.

@brendan-kellam brendan-kellam changed the base branch from main to v5 June 10, 2026 03:01
@brendan-kellam brendan-kellam changed the base branch from v5 to main June 10, 2026 03:12
brendan-kellam and others added 2 commits June 9, 2026 20:14
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brendan-kellam

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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