Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 62 additions & 3 deletions apps/sim/app/api/auth/oauth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
accessToken: account.accessToken,
refreshToken: account.refreshToken,
accessTokenExpiresAt: account.accessTokenExpiresAt,
accountId: account.accountId,
providerId: account.providerId,
password: account.password, // Include password field for Snowflake OAuth credentials
})
.from(account)
.where(and(eq(account.userId, userId), eq(account.providerId, providerId)))
Expand All @@ -93,8 +96,25 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
)

try {
// Extract account URL and OAuth credentials for Snowflake
let metadata: { accountUrl?: string; clientId?: string; clientSecret?: string } | undefined
if (providerId === 'snowflake' && credential.accountId) {
metadata = { accountUrl: credential.accountId }

// Extract clientId and clientSecret from the password field (stored as JSON)
if (credential.password) {
try {
const oauthCredentials = JSON.parse(credential.password)
metadata.clientId = oauthCredentials.clientId
metadata.clientSecret = oauthCredentials.clientSecret
} catch (e) {
logger.error('Failed to parse Snowflake OAuth credentials', { error: e })
}
}
}

// Use the existing refreshOAuthToken function
const refreshResult = await refreshOAuthToken(providerId, credential.refreshToken!)
const refreshResult = await refreshOAuthToken(providerId, credential.refreshToken!, metadata)

if (!refreshResult) {
logger.error(`Failed to refresh token for user ${userId}, provider ${providerId}`, {
Expand Down Expand Up @@ -177,9 +197,27 @@ export async function refreshAccessTokenIfNeeded(
if (shouldRefresh) {
logger.info(`[${requestId}] Token expired, attempting to refresh for credential`)
try {
// Extract account URL and OAuth credentials for Snowflake
let metadata: { accountUrl?: string; clientId?: string; clientSecret?: string } | undefined
if (credential.providerId === 'snowflake' && credential.accountId) {
metadata = { accountUrl: credential.accountId }

// Extract clientId and clientSecret from the password field (stored as JSON)
if (credential.password) {
try {
const oauthCredentials = JSON.parse(credential.password)
metadata.clientId = oauthCredentials.clientId
metadata.clientSecret = oauthCredentials.clientSecret
} catch (e) {
logger.error('Failed to parse Snowflake OAuth credentials', { error: e })
}
}
}

const refreshedToken = await refreshOAuthToken(
credential.providerId,
credential.refreshToken!
credential.refreshToken!,
metadata
)

if (!refreshedToken) {
Expand Down Expand Up @@ -251,7 +289,28 @@ export async function refreshTokenIfNeeded(
}

try {
const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken!)
// Extract account URL and OAuth credentials for Snowflake
let metadata: { accountUrl?: string; clientId?: string; clientSecret?: string } | undefined
if (credential.providerId === 'snowflake' && credential.accountId) {
metadata = { accountUrl: credential.accountId }

// Extract clientId and clientSecret from the password field (stored as JSON)
if (credential.password) {
try {
const oauthCredentials = JSON.parse(credential.password)
metadata.clientId = oauthCredentials.clientId
metadata.clientSecret = oauthCredentials.clientSecret
} catch (e) {
logger.error('Failed to parse Snowflake OAuth credentials', { error: e })
}
}
}

const refreshResult = await refreshOAuthToken(
credential.providerId,
credential.refreshToken!,
metadata
)

if (!refreshResult) {
logger.error(`[${requestId}] Failed to refresh token for credential`)
Expand Down
92 changes: 92 additions & 0 deletions apps/sim/app/api/auth/snowflake/authorize/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateCodeChallenge, generateCodeVerifier } from '@/lib/oauth/pkce'
import { getBaseUrl } from '@/lib/urls/utils'

const logger = createLogger('SnowflakeAuthorize')

export const dynamic = 'force-dynamic'

/**
* Initiates Snowflake OAuth flow
* Expects credentials to be posted in the request body (accountUrl, clientId, clientSecret)
*/
export async function POST(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn('Unauthorized Snowflake OAuth attempt')
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const { accountUrl, clientId, clientSecret } = body

if (!accountUrl || !clientId || !clientSecret) {
logger.error('Missing required Snowflake OAuth parameters', {
hasAccountUrl: !!accountUrl,
hasClientId: !!clientId,
hasClientSecret: !!clientSecret,
})
return NextResponse.json(
{ error: 'accountUrl, clientId, and clientSecret are required' },
{ status: 400 }
)
}

// Parse and clean the account URL
let cleanAccountUrl = accountUrl.replace(/^https?:\/\//, '')
cleanAccountUrl = cleanAccountUrl.replace(/\/$/, '')
if (!cleanAccountUrl.includes('snowflakecomputing.com')) {
cleanAccountUrl = `${cleanAccountUrl}.snowflakecomputing.com`
}

const baseUrl = getBaseUrl()
const redirectUri = `${baseUrl}/api/auth/snowflake/callback`

// Generate PKCE values
const codeVerifier = generateCodeVerifier()
const codeChallenge = await generateCodeChallenge(codeVerifier)

// Store user-provided credentials in the state (will be used in callback)
const state = Buffer.from(
JSON.stringify({
userId: session.user.id,
accountUrl: cleanAccountUrl,
clientId,
clientSecret,
timestamp: Date.now(),
codeVerifier,
})
).toString('base64url')

// Construct Snowflake-specific authorization URL
const authUrl = new URL(`https://${cleanAccountUrl}/oauth/authorize`)
authUrl.searchParams.set('client_id', clientId)
authUrl.searchParams.set('response_type', 'code')
authUrl.searchParams.set('redirect_uri', redirectUri)
// Add scope parameter to specify a safe role (not ACCOUNTADMIN or SECURITYADMIN)
authUrl.searchParams.set('scope', 'refresh_token session:role:PUBLIC')
authUrl.searchParams.set('state', state)
// Add PKCE parameters for security and compatibility with OAUTH_ENFORCE_PKCE
authUrl.searchParams.set('code_challenge', codeChallenge)
authUrl.searchParams.set('code_challenge_method', 'S256')

logger.info('Initiating Snowflake OAuth flow with user-provided credentials (PKCE)', {
userId: session.user.id,
accountUrl: cleanAccountUrl,
hasClientId: !!clientId,
hasClientSecret: !!clientSecret,
redirectUri,
hasPkce: true,
})

return NextResponse.json({
authUrl: authUrl.toString(),
})
} catch (error) {
logger.error('Error initiating Snowflake authorization:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
Loading
Loading