Skip to content

Commit 1f8c88f

Browse files
author
aadamgough
committed
client id and secret for individual projects, no more single client and secret
1 parent c0d357d commit 1f8c88f

File tree

23 files changed

+310
-203
lines changed

23 files changed

+310
-203
lines changed

apps/sim/app/api/auth/oauth/utils.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
6969
accessTokenExpiresAt: account.accessTokenExpiresAt,
7070
accountId: account.accountId,
7171
providerId: account.providerId,
72+
password: account.password, // Include password field for Snowflake OAuth credentials
7273
})
7374
.from(account)
7475
.where(and(eq(account.userId, userId), eq(account.providerId, providerId)))
@@ -95,10 +96,21 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
9596
)
9697

9798
try {
98-
// Extract account URL from accountId for Snowflake
99-
let metadata: { accountUrl?: string } | undefined
99+
// Extract account URL and OAuth credentials for Snowflake
100+
let metadata: { accountUrl?: string; clientId?: string; clientSecret?: string } | undefined
100101
if (providerId === 'snowflake' && credential.accountId) {
101102
metadata = { accountUrl: credential.accountId }
103+
104+
// Extract clientId and clientSecret from the password field (stored as JSON)
105+
if (credential.password) {
106+
try {
107+
const oauthCredentials = JSON.parse(credential.password)
108+
metadata.clientId = oauthCredentials.clientId
109+
metadata.clientSecret = oauthCredentials.clientSecret
110+
} catch (e) {
111+
logger.error('Failed to parse Snowflake OAuth credentials', { error: e })
112+
}
113+
}
102114
}
103115

104116
// Use the existing refreshOAuthToken function
@@ -185,10 +197,21 @@ export async function refreshAccessTokenIfNeeded(
185197
if (shouldRefresh) {
186198
logger.info(`[${requestId}] Token expired, attempting to refresh for credential`)
187199
try {
188-
// Extract account URL from accountId for Snowflake
189-
let metadata: { accountUrl?: string } | undefined
200+
// Extract account URL and OAuth credentials for Snowflake
201+
let metadata: { accountUrl?: string; clientId?: string; clientSecret?: string } | undefined
190202
if (credential.providerId === 'snowflake' && credential.accountId) {
191203
metadata = { accountUrl: credential.accountId }
204+
205+
// Extract clientId and clientSecret from the password field (stored as JSON)
206+
if (credential.password) {
207+
try {
208+
const oauthCredentials = JSON.parse(credential.password)
209+
metadata.clientId = oauthCredentials.clientId
210+
metadata.clientSecret = oauthCredentials.clientSecret
211+
} catch (e) {
212+
logger.error('Failed to parse Snowflake OAuth credentials', { error: e })
213+
}
214+
}
192215
}
193216

194217
const refreshedToken = await refreshOAuthToken(
@@ -266,13 +289,28 @@ export async function refreshTokenIfNeeded(
266289
}
267290

268291
try {
269-
// Extract account URL from accountId for Snowflake
270-
let metadata: { accountUrl?: string } | undefined
292+
// Extract account URL and OAuth credentials for Snowflake
293+
let metadata: { accountUrl?: string; clientId?: string; clientSecret?: string } | undefined
271294
if (credential.providerId === 'snowflake' && credential.accountId) {
272295
metadata = { accountUrl: credential.accountId }
296+
297+
// Extract clientId and clientSecret from the password field (stored as JSON)
298+
if (credential.password) {
299+
try {
300+
const oauthCredentials = JSON.parse(credential.password)
301+
metadata.clientId = oauthCredentials.clientId
302+
metadata.clientSecret = oauthCredentials.clientSecret
303+
} catch (e) {
304+
logger.error('Failed to parse Snowflake OAuth credentials', { error: e })
305+
}
306+
}
273307
}
274308

275-
const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken!, metadata)
309+
const refreshResult = await refreshOAuthToken(
310+
credential.providerId,
311+
credential.refreshToken!,
312+
metadata
313+
)
276314

277315
if (!refreshResult) {
278316
logger.error(`[${requestId}] Failed to refresh token for credential`)
Lines changed: 21 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,40 @@
11
import { type NextRequest, NextResponse } from 'next/server'
22
import { getSession } from '@/lib/auth'
3-
import { env } from '@/lib/env'
43
import { createLogger } from '@/lib/logs/console/logger'
5-
import { getBaseUrl } from '@/lib/urls/utils'
64
import { generateCodeChallenge, generateCodeVerifier } from '@/lib/oauth/pkce'
5+
import { getBaseUrl } from '@/lib/urls/utils'
76

87
const logger = createLogger('SnowflakeAuthorize')
98

109
export const dynamic = 'force-dynamic'
1110

1211
/**
1312
* Initiates Snowflake OAuth flow
14-
* Requires accountUrl as query parameter
13+
* Expects credentials to be posted in the request body (accountUrl, clientId, clientSecret)
1514
*/
16-
export async function GET(request: NextRequest) {
15+
export async function POST(request: NextRequest) {
1716
try {
1817
const session = await getSession()
1918
if (!session?.user?.id) {
2019
logger.warn('Unauthorized Snowflake OAuth attempt')
2120
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
2221
}
2322

24-
const { searchParams } = new URL(request.url)
25-
const accountUrl = searchParams.get('accountUrl')
23+
const body = await request.json()
24+
const { accountUrl, clientId, clientSecret } = body
2625

27-
if (!accountUrl) {
28-
logger.error('Missing accountUrl parameter')
26+
if (!accountUrl || !clientId || !clientSecret) {
27+
logger.error('Missing required Snowflake OAuth parameters', {
28+
hasAccountUrl: !!accountUrl,
29+
hasClientId: !!clientId,
30+
hasClientSecret: !!clientSecret,
31+
})
2932
return NextResponse.json(
30-
{ error: 'accountUrl parameter is required' },
33+
{ error: 'accountUrl, clientId, and clientSecret are required' },
3134
{ status: 400 }
3235
)
3336
}
3437

35-
const clientId = env.SNOWFLAKE_CLIENT_ID
36-
const clientSecret = env.SNOWFLAKE_CLIENT_SECRET
37-
38-
if (!clientId || !clientSecret) {
39-
logger.error('Snowflake OAuth credentials not configured')
40-
return NextResponse.json(
41-
{ error: 'Snowflake OAuth not configured' },
42-
{ status: 500 }
43-
)
44-
}
45-
4638
// Parse and clean the account URL
4739
let cleanAccountUrl = accountUrl.replace(/^https?:\/\//, '')
4840
cleanAccountUrl = cleanAccountUrl.replace(/\/$/, '')
@@ -57,11 +49,13 @@ export async function GET(request: NextRequest) {
5749
const codeVerifier = generateCodeVerifier()
5850
const codeChallenge = await generateCodeChallenge(codeVerifier)
5951

60-
52+
// Store user-provided credentials in the state (will be used in callback)
6153
const state = Buffer.from(
6254
JSON.stringify({
6355
userId: session.user.id,
6456
accountUrl: cleanAccountUrl,
57+
clientId,
58+
clientSecret,
6559
timestamp: Date.now(),
6660
codeVerifier,
6761
})
@@ -78,32 +72,21 @@ export async function GET(request: NextRequest) {
7872
// Add PKCE parameters for security and compatibility with OAUTH_ENFORCE_PKCE
7973
authUrl.searchParams.set('code_challenge', codeChallenge)
8074
authUrl.searchParams.set('code_challenge_method', 'S256')
81-
82-
logger.info('Initiating Snowflake OAuth flow (CONFIDENTIAL client with PKCE)', {
75+
76+
logger.info('Initiating Snowflake OAuth flow with user-provided credentials (PKCE)', {
8377
userId: session.user.id,
8478
accountUrl: cleanAccountUrl,
85-
authUrl: authUrl.toString(),
86-
redirectUri,
87-
clientId,
79+
hasClientId: !!clientId,
8880
hasClientSecret: !!clientSecret,
81+
redirectUri,
8982
hasPkce: true,
90-
parametersCount: authUrl.searchParams.toString().length,
9183
})
9284

93-
logger.info('Authorization URL parameters:', {
94-
client_id: authUrl.searchParams.get('client_id'),
95-
response_type: authUrl.searchParams.get('response_type'),
96-
redirect_uri: authUrl.searchParams.get('redirect_uri'),
97-
state_length: authUrl.searchParams.get('state')?.length,
98-
scope: authUrl.searchParams.get('scope'),
99-
has_pkce: authUrl.searchParams.has('code_challenge'),
100-
code_challenge_method: authUrl.searchParams.get('code_challenge_method'),
85+
return NextResponse.json({
86+
authUrl: authUrl.toString(),
10187
})
102-
103-
return NextResponse.redirect(authUrl.toString())
10488
} catch (error) {
10589
logger.error('Error initiating Snowflake authorization:', error)
10690
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
10791
}
10892
}
109-

apps/sim/app/api/auth/snowflake/callback/route.ts

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { and, eq } from 'drizzle-orm'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { getSession } from '@/lib/auth'
4-
import { env } from '@/lib/env'
54
import { createLogger } from '@/lib/logs/console/logger'
65
import { getBaseUrl } from '@/lib/urls/utils'
76
import { db } from '@/../../packages/db'
@@ -41,10 +40,12 @@ export async function GET(request: NextRequest) {
4140
return NextResponse.redirect(`${getBaseUrl()}/workspace?error=snowflake_invalid_callback`)
4241
}
4342

44-
// Decode state to get account URL and code verifier
43+
// Decode state to get account URL, credentials, and code verifier
4544
let stateData: {
4645
userId: string
4746
accountUrl: string
47+
clientId: string
48+
clientSecret: string
4849
timestamp: number
4950
codeVerifier: string
5051
}
@@ -54,6 +55,8 @@ export async function GET(request: NextRequest) {
5455
logger.info('Decoded state successfully', {
5556
userId: stateData.userId,
5657
accountUrl: stateData.accountUrl,
58+
hasClientId: !!stateData.clientId,
59+
hasClientSecret: !!stateData.clientSecret,
5760
age: Date.now() - stateData.timestamp,
5861
hasCodeVerifier: !!stateData.codeVerifier,
5962
})
@@ -79,12 +82,13 @@ export async function GET(request: NextRequest) {
7982
return NextResponse.redirect(`${getBaseUrl()}/workspace?error=snowflake_state_expired`)
8083
}
8184

82-
const clientId = env.SNOWFLAKE_CLIENT_ID
83-
const clientSecret = env.SNOWFLAKE_CLIENT_SECRET
85+
// Use user-provided credentials from state
86+
const clientId = stateData.clientId
87+
const clientSecret = stateData.clientSecret
8488

8589
if (!clientId || !clientSecret) {
86-
logger.error('Snowflake OAuth credentials not configured')
87-
return NextResponse.redirect(`${getBaseUrl()}/workspace?error=snowflake_not_configured`)
90+
logger.error('Missing client credentials in state')
91+
return NextResponse.redirect(`${getBaseUrl()}/workspace?error=snowflake_missing_credentials`)
8892
}
8993

9094
// Exchange authorization code for tokens
@@ -127,15 +131,15 @@ export async function GET(request: NextRequest) {
127131
tokenUrl,
128132
redirectUri,
129133
})
130-
134+
131135
// Try to parse error as JSON for better diagnostics
132136
try {
133137
const errorJson = JSON.parse(errorText)
134138
logger.error('Snowflake error details:', errorJson)
135139
} catch (e) {
136140
logger.error('Error text (not JSON):', errorText)
137141
}
138-
142+
139143
return NextResponse.redirect(
140144
`${getBaseUrl()}/workspace?error=snowflake_token_exchange_failed&details=${encodeURIComponent(errorText)}`
141145
)
@@ -157,33 +161,37 @@ export async function GET(request: NextRequest) {
157161

158162
// Store the account and tokens in the database
159163
const existing = await db.query.account.findFirst({
160-
where: and(
161-
eq(account.userId, session.user.id),
162-
eq(account.providerId, 'snowflake')
163-
),
164+
where: and(eq(account.userId, session.user.id), eq(account.providerId, 'snowflake')),
164165
})
165166

166167
const now = new Date()
167168
const expiresAt = tokens.expires_in
168169
? new Date(now.getTime() + tokens.expires_in * 1000)
169170
: new Date(now.getTime() + 10 * 60 * 1000) // Default 10 minutes
170171

172+
// Store user-provided OAuth credentials securely
173+
// We use the password field to store a JSON object with clientId and clientSecret
174+
// and idToken to store the accountUrl for easier retrieval
175+
const oauthCredentials = JSON.stringify({
176+
clientId: stateData.clientId,
177+
clientSecret: stateData.clientSecret,
178+
})
179+
171180
const accountData = {
172181
userId: session.user.id,
173182
providerId: 'snowflake',
174183
accountId: stateData.accountUrl, // Store the Snowflake account URL here
175184
accessToken: tokens.access_token,
176185
refreshToken: tokens.refresh_token || null,
186+
idToken: stateData.accountUrl, // Store accountUrl for easier access
187+
password: oauthCredentials, // Store clientId and clientSecret as JSON
177188
accessTokenExpiresAt: expiresAt,
178189
scope: tokens.scope || null,
179190
updatedAt: now,
180191
}
181192

182193
if (existing) {
183-
await db
184-
.update(account)
185-
.set(accountData)
186-
.where(eq(account.id, existing.id))
194+
await db.update(account).set(accountData).where(eq(account.id, existing.id))
187195

188196
logger.info('Updated existing Snowflake account', {
189197
userId: session.user.id,
@@ -208,4 +216,3 @@ export async function GET(request: NextRequest) {
208216
return NextResponse.redirect(`${getBaseUrl()}/workspace?error=snowflake_callback_failed`)
209217
}
210218
}
211-

0 commit comments

Comments
 (0)