Skip to content

Commit 4b7e0c4

Browse files
author
aadamgough
committed
salesforce fix
1 parent f03430a commit 4b7e0c4

File tree

3 files changed

+105
-24
lines changed

3 files changed

+105
-24
lines changed

apps/sim/app/api/auth/oauth/token/route.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,20 @@ export async function POST(request: NextRequest) {
7878
try {
7979
// Refresh the token if needed
8080
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
81+
82+
let instanceUrl: string | undefined
83+
if (credential.providerId === 'salesforce' && credential.scope) {
84+
const instanceMatch = credential.scope.match(/__sf_instance__:([^\s]+)/)
85+
if (instanceMatch) {
86+
instanceUrl = instanceMatch[1]
87+
}
88+
}
89+
8190
return NextResponse.json(
8291
{
8392
accessToken,
8493
idToken: credential.idToken || undefined,
94+
...(instanceUrl && { instanceUrl }),
8595
},
8696
{ status: 200 }
8797
)
@@ -147,10 +157,21 @@ export async function GET(request: NextRequest) {
147157

148158
try {
149159
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
160+
161+
// For Salesforce, extract instanceUrl from the scope field
162+
let instanceUrl: string | undefined
163+
if (credential.providerId === 'salesforce' && credential.scope) {
164+
const instanceMatch = credential.scope.match(/__sf_instance__:([^\s]+)/)
165+
if (instanceMatch) {
166+
instanceUrl = instanceMatch[1]
167+
}
168+
}
169+
150170
return NextResponse.json(
151171
{
152172
accessToken,
153173
idToken: credential.idToken || undefined,
174+
...(instanceUrl && { instanceUrl }),
154175
},
155176
{ status: 200 }
156177
)

apps/sim/lib/auth/auth.ts

Lines changed: 80 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,39 @@ export const auth = betterAuth({
120120
})
121121

122122
if (existing) {
123+
let scopeToStore = account.scope
124+
125+
// For Salesforce, fetch instance URL and add it to the scope
126+
if (account.providerId === 'salesforce' && account.accessToken) {
127+
try {
128+
const response = await fetch(
129+
'https://login.salesforce.com/services/oauth2/userinfo',
130+
{
131+
headers: {
132+
Authorization: `Bearer ${account.accessToken}`,
133+
},
134+
}
135+
)
136+
137+
if (response.ok) {
138+
const data = await response.json()
139+
140+
// Extract instance URL from profile field (format: https://na1.salesforce.com/id/...)
141+
if (data.profile) {
142+
const match = data.profile.match(/^(https:\/\/[^/]+)/)
143+
if (match && match[1] !== 'https://login.salesforce.com') {
144+
const instanceUrl = match[1]
145+
const existingScope =
146+
account.scope || 'api refresh_token openid offline_access'
147+
scopeToStore = `__sf_instance__:${instanceUrl} ${existingScope}`
148+
}
149+
}
150+
}
151+
} catch (error) {
152+
logger.error('Failed to fetch Salesforce instance URL', { error })
153+
}
154+
}
155+
123156
await db
124157
.update(schema.account)
125158
.set({
@@ -129,7 +162,7 @@ export const auth = betterAuth({
129162
idToken: account.idToken,
130163
accessTokenExpiresAt: account.accessTokenExpiresAt,
131164
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
132-
scope: account.scope,
165+
scope: scopeToStore,
133166
updatedAt: new Date(),
134167
})
135168
.where(eq(schema.account.id, existing.id))
@@ -140,24 +173,52 @@ export const auth = betterAuth({
140173
return { data: account }
141174
},
142175
after: async (account) => {
143-
// Salesforce doesn't return expires_in in its token response (unlike other OAuth providers).
144-
// We set a default 2-hour expiration so token refresh logic works correctly.
145-
if (account.providerId === 'salesforce' && !account.accessTokenExpiresAt) {
146-
const twoHoursFromNow = new Date(Date.now() + 2 * 60 * 60 * 1000)
147-
try {
148-
await db
149-
.update(schema.account)
150-
.set({ accessTokenExpiresAt: twoHoursFromNow })
151-
.where(eq(schema.account.id, account.id))
152-
logger.info(
153-
'[databaseHooks.account.create.after] Set default expiration for Salesforce token',
154-
{ accountId: account.id, expiresAt: twoHoursFromNow }
155-
)
156-
} catch (error) {
157-
logger.error(
158-
'[databaseHooks.account.create.after] Failed to set Salesforce token expiration',
159-
{ accountId: account.id, error }
160-
)
176+
// Salesforce-specific handling: set default token expiration and fetch instance URL
177+
if (account.providerId === 'salesforce') {
178+
const updates: {
179+
accessTokenExpiresAt?: Date
180+
scope?: string
181+
} = {}
182+
183+
// Salesforce doesn't return expires_in, set default 2-hour expiration
184+
if (!account.accessTokenExpiresAt) {
185+
updates.accessTokenExpiresAt = new Date(Date.now() + 2 * 60 * 60 * 1000)
186+
}
187+
188+
// Fetch instance URL from Salesforce userinfo endpoint and store it in scope
189+
if (account.accessToken) {
190+
try {
191+
const response = await fetch(
192+
'https://login.salesforce.com/services/oauth2/userinfo',
193+
{
194+
headers: {
195+
Authorization: `Bearer ${account.accessToken}`,
196+
},
197+
}
198+
)
199+
200+
if (response.ok) {
201+
const data = await response.json()
202+
203+
// Extract instance URL from profile field (format: https://na1.salesforce.com/id/...)
204+
if (data.profile) {
205+
const match = data.profile.match(/^(https:\/\/[^/]+)/)
206+
if (match && match[1] !== 'https://login.salesforce.com') {
207+
const instanceUrl = match[1]
208+
const existingScope =
209+
account.scope || 'api refresh_token openid offline_access'
210+
updates.scope = `__sf_instance__:${instanceUrl} ${existingScope}`
211+
}
212+
}
213+
}
214+
} catch (error) {
215+
logger.error('Failed to fetch Salesforce instance URL', { error })
216+
}
217+
}
218+
219+
// Apply updates if any
220+
if (Object.keys(updates).length > 0) {
221+
await db.update(schema.account).set(updates).where(eq(schema.account.id, account.id))
161222
}
162223
}
163224
},
@@ -928,8 +989,6 @@ export const auth = betterAuth({
928989
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/salesforce`,
929990
getUserInfo: async (tokens) => {
930991
try {
931-
logger.info('Fetching Salesforce user profile')
932-
933992
const response = await fetch(
934993
'https://login.salesforce.com/services/oauth2/userinfo',
935994
{

apps/sim/tools/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,10 +305,11 @@ export async function executeTool(
305305
if (data.idToken) {
306306
contextParams.idToken = data.idToken
307307
}
308+
if (data.instanceUrl) {
309+
contextParams.instanceUrl = data.instanceUrl
310+
}
308311

309-
logger.info(
310-
`[${requestId}] Successfully got access token for ${toolId}, length: ${data.accessToken?.length || 0}`
311-
)
312+
logger.info(`[${requestId}] Successfully got access token for ${toolId}`)
312313

313314
// Preserve credential for downstream transforms while removing it from request payload
314315
// so we don't leak it to external services.

0 commit comments

Comments
 (0)