Skip to content

Commit 76d0578

Browse files
committed
fix(oauth): updated oauth providers that had unstable reference IDs leading to duplicate oauth records (#2441)
* fix(oauth): updated oauth providers that had unstable reference IDs leading to duplicate oauth records * ack PR comments
1 parent 491bd78 commit 76d0578

File tree

10 files changed

+727
-241
lines changed

10 files changed

+727
-241
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { db } from '@sim/db'
2+
import { account } from '@sim/db/schema'
3+
import { and, eq } from 'drizzle-orm'
4+
import { type NextRequest, NextResponse } from 'next/server'
5+
import { getSession } from '@/lib/auth'
6+
import { env } from '@/lib/core/config/env'
7+
import { getBaseUrl } from '@/lib/core/utils/urls'
8+
import { createLogger } from '@/lib/logs/console/logger'
9+
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
10+
11+
const logger = createLogger('SalesforceCallback')
12+
13+
export const dynamic = 'force-dynamic'
14+
15+
export async function GET(request: NextRequest) {
16+
const baseUrl = getBaseUrl()
17+
18+
try {
19+
const session = await getSession()
20+
if (!session?.user?.id) {
21+
logger.warn('Unauthorized attempt to complete Salesforce OAuth')
22+
return NextResponse.redirect(`${baseUrl}/workspace?error=unauthorized`)
23+
}
24+
25+
const { searchParams } = request.nextUrl
26+
const code = searchParams.get('code')
27+
const state = searchParams.get('state')
28+
const error = searchParams.get('error')
29+
const errorDescription = searchParams.get('error_description')
30+
31+
if (error) {
32+
logger.error('Salesforce OAuth error:', { error, errorDescription })
33+
return NextResponse.redirect(
34+
`${baseUrl}/workspace?error=salesforce_oauth_error&message=${encodeURIComponent(errorDescription || error)}`
35+
)
36+
}
37+
38+
const storedState = request.cookies.get('salesforce_oauth_state')?.value
39+
const storedVerifier = request.cookies.get('salesforce_pkce_verifier')?.value
40+
const storedBaseUrl = request.cookies.get('salesforce_base_url')?.value
41+
const returnUrl = request.cookies.get('salesforce_return_url')?.value
42+
43+
const clientId = env.SALESFORCE_CLIENT_ID
44+
const clientSecret = env.SALESFORCE_CLIENT_SECRET
45+
46+
if (!clientId || !clientSecret) {
47+
logger.error('Salesforce credentials not configured')
48+
return NextResponse.redirect(`${baseUrl}/workspace?error=salesforce_config_error`)
49+
}
50+
51+
if (!state || state !== storedState) {
52+
logger.error('State mismatch in Salesforce OAuth callback', {
53+
receivedState: state,
54+
storedState: storedState ? 'present' : 'missing',
55+
})
56+
return NextResponse.redirect(`${baseUrl}/workspace?error=salesforce_state_mismatch`)
57+
}
58+
59+
if (!code) {
60+
logger.error('No authorization code received from Salesforce')
61+
return NextResponse.redirect(`${baseUrl}/workspace?error=salesforce_no_code`)
62+
}
63+
64+
if (!storedVerifier || !storedBaseUrl) {
65+
logger.error('Missing PKCE verifier or base URL')
66+
return NextResponse.redirect(`${baseUrl}/workspace?error=salesforce_missing_data`)
67+
}
68+
69+
const tokenUrl = `${storedBaseUrl}/services/oauth2/token`
70+
const redirectUri = `${baseUrl}/api/auth/oauth2/callback/salesforce`
71+
72+
const tokenParams = new URLSearchParams({
73+
grant_type: 'authorization_code',
74+
code: code,
75+
client_id: clientId,
76+
client_secret: clientSecret,
77+
redirect_uri: redirectUri,
78+
code_verifier: storedVerifier,
79+
})
80+
81+
const tokenResponse = await fetch(tokenUrl, {
82+
method: 'POST',
83+
headers: {
84+
'Content-Type': 'application/x-www-form-urlencoded',
85+
},
86+
body: tokenParams.toString(),
87+
})
88+
89+
if (!tokenResponse.ok) {
90+
const errorText = await tokenResponse.text()
91+
logger.error('Failed to exchange code for token:', {
92+
status: tokenResponse.status,
93+
body: errorText,
94+
tokenUrl,
95+
})
96+
return NextResponse.redirect(`${baseUrl}/workspace?error=salesforce_token_error`)
97+
}
98+
99+
const tokenData = await tokenResponse.json()
100+
const accessToken = tokenData.access_token
101+
const refreshToken = tokenData.refresh_token
102+
const instanceUrl = tokenData.instance_url
103+
const idToken = tokenData.id_token
104+
const scope = tokenData.scope
105+
const issuedAt = tokenData.issued_at
106+
107+
logger.info('Salesforce token exchange successful:', {
108+
hasAccessToken: !!accessToken,
109+
hasRefreshToken: !!refreshToken,
110+
instanceUrl,
111+
scope,
112+
})
113+
114+
if (!accessToken) {
115+
logger.error('No access token in Salesforce response')
116+
return NextResponse.redirect(`${baseUrl}/workspace?error=salesforce_no_token`)
117+
}
118+
119+
if (!instanceUrl) {
120+
logger.error('No instance URL in Salesforce response')
121+
return NextResponse.redirect(`${baseUrl}/workspace?error=salesforce_no_instance`)
122+
}
123+
124+
let userId = 'unknown'
125+
let userEmail = ''
126+
try {
127+
const userInfoUrl = `${instanceUrl}/services/oauth2/userinfo`
128+
const userInfoResponse = await fetch(userInfoUrl, {
129+
headers: {
130+
Authorization: `Bearer ${accessToken}`,
131+
},
132+
})
133+
if (userInfoResponse.ok) {
134+
const userInfo = await userInfoResponse.json()
135+
userId = userInfo.user_id || userInfo.sub || 'unknown'
136+
userEmail = userInfo.email || ''
137+
}
138+
} catch (userInfoError) {
139+
logger.warn('Failed to fetch Salesforce user info:', userInfoError)
140+
}
141+
142+
const existing = await db.query.account.findFirst({
143+
where: and(eq(account.userId, session.user.id), eq(account.providerId, 'salesforce')),
144+
})
145+
146+
const now = new Date()
147+
const expiresAt = new Date(now.getTime() + 2 * 60 * 60 * 1000)
148+
149+
const accountData = {
150+
accessToken: accessToken,
151+
refreshToken: refreshToken || null,
152+
accountId: userId,
153+
scope: scope || '',
154+
updatedAt: now,
155+
accessTokenExpiresAt: expiresAt,
156+
idToken: instanceUrl,
157+
}
158+
159+
if (existing) {
160+
await db.update(account).set(accountData).where(eq(account.id, existing.id))
161+
logger.info('Updated existing Salesforce account', { accountId: existing.id })
162+
} else {
163+
await safeAccountInsert(
164+
{
165+
id: `salesforce_${session.user.id}_${Date.now()}`,
166+
userId: session.user.id,
167+
providerId: 'salesforce',
168+
accountId: accountData.accountId,
169+
accessToken: accountData.accessToken,
170+
refreshToken: accountData.refreshToken || undefined,
171+
scope: accountData.scope,
172+
idToken: accountData.idToken,
173+
accessTokenExpiresAt: accountData.accessTokenExpiresAt,
174+
createdAt: now,
175+
updatedAt: now,
176+
},
177+
{ provider: 'Salesforce', identifier: userEmail || userId }
178+
)
179+
}
180+
181+
const redirectUrl = returnUrl || `${baseUrl}/workspace`
182+
const finalUrl = new URL(redirectUrl)
183+
finalUrl.searchParams.set('salesforce_connected', 'true')
184+
185+
const response = NextResponse.redirect(finalUrl.toString())
186+
response.cookies.delete('salesforce_oauth_state')
187+
response.cookies.delete('salesforce_pkce_verifier')
188+
response.cookies.delete('salesforce_base_url')
189+
response.cookies.delete('salesforce_return_url')
190+
191+
return response
192+
} catch (error) {
193+
logger.error('Error in Salesforce OAuth callback:', error)
194+
return NextResponse.redirect(`${baseUrl}/workspace?error=salesforce_callback_error`)
195+
}
196+
}

0 commit comments

Comments
 (0)