feat(salesforce): add JWT bearer flow and sandbox OAuth support by waleedlatif1 · Pull Request #6508 · simstudioai/sim · GitHub
Skip to content
Merged
Prev Previous commit
Next Next commit
fix(salesforce): match sandbox credentials in Chat and the connect draft
Two more surfaces resolved a credential to its service by exact provider id:

- `credentialsForTarget` compared only `providerId`/`baseProviderId`, so a
  sandbox-only user's Salesforce chip in Chat read as disconnected and
  re-prompted them to connect. The alternate ids are passed in by the caller
  rather than resolved in the module, which is `'use client'` and would
  otherwise pull the OAuth provider registry into the chat bundle.
- `createConnectDraft` resolved the service name by exact id, so a sandbox
  connect defaulted to the label "My salesforce-sandbox".
  • Loading branch information
waleedlatif1 committed Aug 11, 2026
commit 3b04abc88d13c9dddfa1720541f20d210bd4b034
13 changes: 13 additions & 0 deletions apps/sim/app/api/auth/oauth2/authorize/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from '@/lib/credentials/oauth-chat-attempt'
import { getDesktopBridge } from '@/lib/desktop'
import type { OAuthProvider } from '@/lib/oauth/types'
import { parseProvider } from '@/lib/oauth/utils'
import { parseProvider, providerIdsForService } from '@/lib/oauth/utils'
import { useWorkspaceCredentials } from '@/hooks/queries/credentials'

const OAUTH_POPUP_WINDOW_NAME = 'sim-oauth-connect'
Expand Down Expand Up @@ -188,7 +188,15 @@ export function useOAuthChipConnection({
} | null>(null)

const credentialTarget = useMemo(
() => ({ providerId, baseProviderId, credentialId: reconnectCredentialId }),
() => ({
providerId,
baseProviderId,
credentialId: reconnectCredentialId,
// A credential from an alternate authorization server (Salesforce
// sandbox) still connects this chip's service; without these the chip
// reads as disconnected and re-prompts a user who is already connected.
additionalProviderIds: providerIdsForService(providerId),
}),
[baseProviderId, providerId, reconnectCredentialId]
)
const credentialScope = `${workspaceId}:${providerId}:${reconnectCredentialId ?? ''}`
Expand Down
9 changes: 7 additions & 2 deletions apps/sim/lib/credentials/connect-draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, lt } from 'drizzle-orm'
import { defaultCredentialDisplayName } from '@/lib/credentials/display-name'
import { getAllOAuthServices } from '@/lib/oauth/utils'
import { credentialProviderMatchesService, getAllOAuthServices } from '@/lib/oauth/utils'

const logger = createLogger('OAuthConnectDraft')
const DRAFT_TTL_MS = 15 * 60 * 1000
Expand All @@ -26,7 +26,12 @@ export async function createConnectDraft(params: {

let displayName = params.displayName
if (!displayName) {
const service = getAllOAuthServices().find((s) => s.providerId === providerId)
// Matches through the canonical predicate so an alternate authorization
// server's id resolves the service's real name — otherwise the default
// label reads "My salesforce-sandbox".
const service = getAllOAuthServices().find((s) =>
credentialProviderMatchesService(providerId, s)
)
const serviceName = service?.name ?? providerId

let userName: string | null = null
Expand Down
57 changes: 57 additions & 0 deletions apps/sim/lib/credentials/oauth-chat-attempt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
createOAuthChatAttempt,
getOAuthCredentialBaseline,
hasOAuthCredentialChanged,
hasOAuthCredentialForTarget,
OAUTH_CHAT_ATTEMPT_EVENT,
OAUTH_CHAT_ATTEMPT_PARAM,
OAUTH_CHAT_COMPLETE_PATH,
Expand Down Expand Up @@ -254,3 +255,59 @@ describe('OAuth chat attempts', () => {
).toBe(true)
})
})

describe('credential matching for a chat chip', () => {
const credential = (providerId: string) => ({
id: `cred-${providerId}`,
providerId,
updatedAt: '2026-08-11T00:00:00.000Z',
})

it('matches a credential from an alternate authorization server', () => {
// A sandbox-only user is connected; without this the chip reads as
// disconnected and re-prompts them to connect Salesforce again.
expect(
hasOAuthCredentialForTarget(
{
providerId: 'salesforce',
baseProviderId: 'salesforce',
additionalProviderIds: ['salesforce-sandbox'],
},
[credential('salesforce-sandbox')]
)
).toBe(true)
})

it('still matches the primary id and the base provider', () => {
const target = { providerId: 'google-email', baseProviderId: 'google' }
expect(hasOAuthCredentialForTarget(target, [credential('google-email')])).toBe(true)
expect(hasOAuthCredentialForTarget(target, [credential('google')])).toBe(true)
})

it('does not match an unrelated provider', () => {
expect(
hasOAuthCredentialForTarget(
{
providerId: 'salesforce',
baseProviderId: 'salesforce',
additionalProviderIds: ['salesforce-sandbox'],
},
[credential('hubspot')]
)
).toBe(false)
})

it('honours an explicit credentialId over any provider match', () => {
expect(
hasOAuthCredentialForTarget(
{
providerId: 'salesforce',
baseProviderId: 'salesforce',
credentialId: 'cred-other',
additionalProviderIds: ['salesforce-sandbox'],
},
[credential('salesforce-sandbox')]
)
).toBe(false)
})
})
12 changes: 11 additions & 1 deletion apps/sim/lib/credentials/oauth-chat-attempt.ts