From 4deef0b769e613477345f477332e8a9bb520d9a1 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 14 Mar 2025 16:07:34 -0700 Subject: [PATCH] fix: removed multiple account oauth functionality, updated better-auth to acknowledge security vulnerability --- sim/app/api/auth/oauth/connections/route.ts | 59 +++++++++++++++---- sim/app/api/auth/oauth/credentials/route.ts | 46 ++++++++++++--- sim/app/components/ui/file-selector.tsx | 4 +- sim/app/components/ui/folder-selector.tsx | 4 +- .../components/ui/oauth-required-modal.tsx | 8 ++- sim/app/lib/auth-client.ts | 1 - sim/app/lib/auth.ts | 15 +++++ .../components/credential-selector.tsx | 18 +----- .../components/credentials/credentials.tsx | 50 ++++++++++++---- sim/middleware.ts | 17 ++++-- sim/package-lock.json | 51 ++++++++++++---- sim/package.json | 2 +- 12 files changed, 206 insertions(+), 69 deletions(-) diff --git a/sim/app/api/auth/oauth/connections/route.ts b/sim/app/api/auth/oauth/connections/route.ts index 0b942033b0..debf6824da 100644 --- a/sim/app/api/auth/oauth/connections/route.ts +++ b/sim/app/api/auth/oauth/connections/route.ts @@ -5,13 +5,14 @@ import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console-logger' import { OAuthService } from '@/lib/oauth' import { db } from '@/db' -import { account } from '@/db/schema' +import { account, user } from '@/db/schema' const logger = createLogger('OAuthConnectionsAPI') interface GoogleIdToken { email?: string sub?: string + name?: string } // Valid OAuth providers @@ -36,30 +37,61 @@ export async function GET(request: NextRequest) { // Get all accounts for this user const accounts = await db.select().from(account).where(eq(account.userId, session.user.id)) + // Get the user's email for fallback + const userRecord = await db + .select({ email: user.email }) + .from(user) + .where(eq(user.id, session.user.id)) + .limit(1) + + const userEmail = userRecord.length > 0 ? userRecord[0].email : null + // Process accounts to determine connections const connections: any[] = [] - accounts.forEach((acc) => { + for (const acc of accounts) { // Extract the base provider and feature type from providerId (e.g., 'google-email' -> 'google', 'email') const [provider, featureType = 'default'] = acc.providerId.split('-') if (provider && VALID_PROVIDERS.includes(provider)) { - // Get the account name (try to get email for Google accounts) - let name = acc.accountId - if (provider === 'google' && acc.idToken) { + // Try multiple methods to get a user-friendly display name + let displayName = '' + + // Method 1: Try to extract email from ID token (works for Google, etc.) + if (acc.idToken) { try { const decoded = jwtDecode(acc.idToken) if (decoded.email) { - name = decoded.email + displayName = decoded.email + } else if (decoded.name) { + displayName = decoded.name } } catch (error) { - logger.warn(`[${requestId}] Error decoding Google ID token`, { accountId: acc.id }) + logger.warn(`[${requestId}] Error decoding ID token`, { accountId: acc.id }) } } + + // Method 2: For GitHub, the accountId might be the username + if (!displayName && provider === 'github') { + displayName = `${acc.accountId} (GitHub)` + } + + // Method 3: Use the user's email from our database + if (!displayName && userEmail) { + displayName = userEmail + } + + // Fallback: Use accountId with provider type as context + if (!displayName) { + displayName = `${acc.accountId} (${provider})` + } - // Find existing connection for this provider and feature type + // Create a unique connection key that includes the full provider ID + const connectionKey = acc.providerId + + // Find existing connection for this specific provider ID const existingConnection = connections.find( - (conn) => conn.provider === provider && conn.featureType === featureType + (conn) => conn.provider === connectionKey ) if (existingConnection) { @@ -67,12 +99,13 @@ export async function GET(request: NextRequest) { existingConnection.accounts = existingConnection.accounts || [] existingConnection.accounts.push({ id: acc.id, - name, + name: displayName, }) } else { // Create new connection connections.push({ - provider: provider as OAuthService, + provider: connectionKey, + baseProvider: provider, featureType, isConnected: true, scopes: acc.scope ? acc.scope.split(' ') : [], @@ -80,13 +113,13 @@ export async function GET(request: NextRequest) { accounts: [ { id: acc.id, - name, + name: displayName, }, ], }) } } - }) + } return NextResponse.json({ connections }, { status: 200 }) } catch (error) { diff --git a/sim/app/api/auth/oauth/credentials/route.ts b/sim/app/api/auth/oauth/credentials/route.ts index 23a411e2f4..865f461053 100644 --- a/sim/app/api/auth/oauth/credentials/route.ts +++ b/sim/app/api/auth/oauth/credentials/route.ts @@ -6,13 +6,14 @@ import { createLogger } from '@/lib/logs/console-logger' import { parseProvider } from '@/lib/oauth' import { OAuthService } from '@/lib/oauth' import { db } from '@/db' -import { account } from '@/db/schema' +import { account, user } from '@/db/schema' const logger = createLogger('OAuthCredentialsAPI') interface GoogleIdToken { email?: string sub?: string + name?: string } /** @@ -55,22 +56,53 @@ export async function GET(request: NextRequest) { // Extract the feature type from providerId (e.g., 'google-default' -> 'default') const [_, featureType = 'default'] = acc.providerId.split('-') - // For Google accounts, try to get the email from the ID token - let name = acc.accountId - if (baseProvider === 'google' && acc.idToken) { + // Try multiple methods to get a user-friendly display name + let displayName = '' + + // Method 1: Try to extract email from ID token (works for Google, etc.) + if (acc.idToken) { try { const decoded = jwtDecode(acc.idToken) if (decoded.email) { - name = decoded.email + displayName = decoded.email + } else if (decoded.name) { + displayName = decoded.name } } catch (error) { - logger.warn(`[${requestId}] Error decoding Google ID token`, { accountId: acc.id }) + logger.warn(`[${requestId}] Error decoding ID token`, { accountId: acc.id }) } } + + // Method 2: For GitHub, the accountId might be the username + if (!displayName && baseProvider === 'github') { + displayName = `${acc.accountId} (GitHub)` + } + + // Method 3: Try to get the user's email from our database + if (!displayName) { + try { + const userRecord = await db + .select({ email: user.email }) + .from(user) + .where(eq(user.id, acc.userId)) + .limit(1) + + if (userRecord.length > 0) { + displayName = userRecord[0].email + } + } catch (error) { + logger.warn(`[${requestId}] Error fetching user email`, { userId: acc.userId }) + } + } + + // Fallback: Use accountId with provider type as context + if (!displayName) { + displayName = `${acc.accountId} (${baseProvider})` + } return { id: acc.id, - name, + name: displayName, provider, lastUsed: acc.updatedAt.toISOString(), isDefault: featureType === 'default', diff --git a/sim/app/components/ui/file-selector.tsx b/sim/app/components/ui/file-selector.tsx index a960cc1743..03f12c68ab 100644 --- a/sim/app/components/ui/file-selector.tsx +++ b/sim/app/components/ui/file-selector.tsx @@ -505,7 +505,7 @@ export function FileSelector({ )} - {/* Add another account option */} + {/* Add another account option {credentials.length > 0 && ( @@ -514,7 +514,7 @@ export function FileSelector({ - )} + )} */} diff --git a/sim/app/components/ui/folder-selector.tsx b/sim/app/components/ui/folder-selector.tsx index d59a664fef..f1a06b3497 100644 --- a/sim/app/components/ui/folder-selector.tsx +++ b/sim/app/components/ui/folder-selector.tsx @@ -412,7 +412,7 @@ export function FolderSelector({ )} {/* Add another account option */} - {credentials.length > 0 && ( + {/* {credentials.length > 0 && (
@@ -420,7 +420,7 @@ export function FolderSelector({
- )} + )} */} diff --git a/sim/app/components/ui/oauth-required-modal.tsx b/sim/app/components/ui/oauth-required-modal.tsx index 326d34096c..52aa5e4bbf 100644 --- a/sim/app/components/ui/oauth-required-modal.tsx +++ b/sim/app/components/ui/oauth-required-modal.tsx @@ -128,8 +128,12 @@ export function OAuthRequiredModal({ // Close the modal onClose() - // Begin OAuth flow with the appropriate provider - await client.signIn.oauth2({ + logger.info('Linking OAuth2:', { + providerId, + requiredScopes, + }) + + await client.oauth2.link({ providerId, callbackURL: window.location.href, }) diff --git a/sim/app/lib/auth-client.ts b/sim/app/lib/auth-client.ts index 92395c729b..692e4553a7 100644 --- a/sim/app/lib/auth-client.ts +++ b/sim/app/lib/auth-client.ts @@ -6,5 +6,4 @@ export const client = createAuthClient({ }) export const { useSession } = client -// Export commonly used hooks and methods export const { signIn, signUp, signOut } = client diff --git a/sim/app/lib/auth.ts b/sim/app/lib/auth.ts index 8090d19f5e..b50543d249 100644 --- a/sim/app/lib/auth.ts +++ b/sim/app/lib/auth.ts @@ -27,6 +27,13 @@ export const auth = betterAuth({ maxAge: 5 * 60, // Cache duration (5 minutes) }, }, + account: { + accountLinking: { + enabled: true, + allowDifferentEmails: true, + trustedProviders: ["google", "github", "email-password"], + }, + }, socialProviders: { github: { clientId: process.env.GITHUB_CLIENT_ID as string, @@ -120,6 +127,7 @@ export const auth = betterAuth({ tokenUrl: 'https://github.com/login/oauth/access_token', userInfoUrl: 'https://api.github.com/user', scopes: ['user:email', 'repo'], + redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/github-repo`, }, { providerId: 'github-workflow', @@ -131,6 +139,7 @@ export const auth = betterAuth({ userInfoUrl: 'https://api.github.com/user', scopes: ['workflow', 'repo'], prompt: 'consent', + redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/github-workflow`, }, // Google providers for different purposes @@ -148,6 +157,7 @@ export const auth = betterAuth({ 'https://www.googleapis.com/auth/gmail.labels', ], prompt: 'consent', + redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-email`, }, { providerId: 'google-calendar', @@ -161,6 +171,7 @@ export const auth = betterAuth({ 'https://www.googleapis.com/auth/calendar', ], prompt: 'consent', + redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-calendar`, }, { providerId: 'google-drive', @@ -175,6 +186,7 @@ export const auth = betterAuth({ 'https://www.googleapis.com/auth/drive.file', ], prompt: 'consent', + redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-drive`, }, { providerId: 'google-docs', @@ -189,6 +201,7 @@ export const auth = betterAuth({ 'https://www.googleapis.com/auth/drive.file', ], prompt: 'consent', + redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-docs`, }, { providerId: 'google-sheets', @@ -203,6 +216,7 @@ export const auth = betterAuth({ 'https://www.googleapis.com/auth/drive.file', ], prompt: 'consent', + redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-sheets`, }, // Supabase provider @@ -217,6 +231,7 @@ export const auth = betterAuth({ scopes: ['database.read', 'database.write', 'projects.read'], responseType: 'code', pkce: true, + redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/supabase`, }, // X provider diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector.tsx index 7b03297ab0..fefc1b7816 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector.tsx @@ -13,7 +13,6 @@ import { } from '@/components/ui/command' import { OAuthRequiredModal } from '@/components/ui/oauth-required-modal' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { client } from '@/lib/auth-client' import { createLogger } from '@/lib/logs/console-logger' import { Credential, @@ -106,7 +105,7 @@ export function CredentialSelector({ } finally { setIsLoading(false) } - }, [provider, onChange, selectedId]) + }, [provider, onChange, selectedId, getProviderId]) // Fetch credentials on initial mount and when dependencies change useEffect(() => { @@ -154,21 +153,6 @@ export function CredentialSelector({ setOpen(false) } - // Handle direct OAuth flow - const handleDirectOAuth = async () => { - try { - const providerId = getProviderId() - - // Begin OAuth flow with the appropriate provider - await client.signIn.oauth2({ - providerId, - callbackURL: window.location.href, - }) - } catch (error) { - logger.error('OAuth login error:', { error }) - } - } - // Get provider icon const getProviderIcon = (providerName: OAuthProvider) => { const { baseProvider } = parseProvider(providerName) diff --git a/sim/app/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx b/sim/app/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx index 6e05bb45af..2d8b89b834 100644 --- a/sim/app/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx +++ b/sim/app/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx @@ -72,12 +72,13 @@ export function Credentials({ onOpenChange }: CredentialsProps) { // Update services with connection status and account info const updatedServices = serviceDefinitions.map((service) => { - // Find matching connection + // Find matching connection - now we can do an exact match on providerId const connection = connections.find((conn: any) => { - const [provider, featureType] = conn.provider.split('-') - return service.providerId.startsWith(provider) - }) + // Exact match on providerId is the most reliable + return conn.provider === service.providerId; + }); + // If we found an exact match, use it if (connection) { return { ...service, @@ -87,7 +88,31 @@ export function Credentials({ onOpenChange }: CredentialsProps) { } } - return service + // If no exact match, check if any connection has all the required scopes + const connectionWithScopes = connections.find((conn: any) => { + // Only consider connections from the same base provider + if (!conn.baseProvider || !service.providerId.startsWith(conn.baseProvider)) { + return false; + } + + // Check if all required scopes for this service are included in the connection + if (conn.scopes && service.scopes) { + return service.scopes.every(scope => conn.scopes.includes(scope)); + } + + return false; + }); + + if (connectionWithScopes) { + return { + ...service, + isConnected: connectionWithScopes.accounts?.length > 0, + accounts: connectionWithScopes.accounts || [], + lastConnected: connectionWithScopes.lastConnected, + } + } + + return service; }) setServices(updatedServices) @@ -184,14 +209,19 @@ export function Credentials({ onOpenChange }: CredentialsProps) { saveToStorage('pending_oauth_scopes', service.scopes) saveToStorage('pending_oauth_return_url', window.location.href) saveToStorage('pending_oauth_provider_id', service.providerId) + + logger.info('Connecting service:', { + serviceId: service.id, + providerId: service.providerId, + scopes: service.scopes, + }) - // Begin OAuth flow with the appropriate provider - await client.signIn.oauth2({ + await client.oauth2.link({ providerId: service.providerId, callbackURL: window.location.href, }) } catch (error) { - logger.error('OAuth login error:', { error }) + logger.error('OAuth connection error:', { error }) setIsConnecting(null) } } @@ -359,7 +389,7 @@ export function Credentials({ onOpenChange }: CredentialsProps) { ))} - + */} )} diff --git a/sim/middleware.ts b/sim/middleware.ts index cd11e144e6..c9f311e43a 100644 --- a/sim/middleware.ts +++ b/sim/middleware.ts @@ -1,9 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' -import { getSessionCookie } from 'better-auth' export async function middleware(request: NextRequest) { - // Check if the path is exactly /w - if (request.nextUrl.pathname === '/w') { + // Check if the path is exactly /w + if (request.nextUrl.pathname === '/w') { return NextResponse.redirect(new URL('/w/1', request.url)) } @@ -12,8 +11,18 @@ export async function middleware(request: NextRequest) { return NextResponse.next() } + const cookieHeader = request.headers.get("cookie"); + const cookies = cookieHeader?.split("; ").reduce((acc, cookie) => { + const [key, value] = cookie.split("="); + acc.set(key, value); + return acc; + }, new Map()); + + const sessionCookie = + cookies?.get("better-auth.session_token") || + cookies?.get("__Secure-better-auth.session_token"); + // Existing auth check for protected routes - const sessionCookie = getSessionCookie(request) if (!sessionCookie) { return NextResponse.redirect(new URL('/login', request.url)) } diff --git a/sim/package-lock.json b/sim/package-lock.json index 09e69e15e0..7c11c1f11f 100644 --- a/sim/package-lock.json +++ b/sim/package-lock.json @@ -32,7 +32,7 @@ "@radix-ui/react-tooltip": "^1.1.6", "@vercel/og": "^0.6.5", "@webcontainer/api": "^1.5.1-internal.9", - "better-auth": "^1.1.18", + "better-auth": "^1.2.4", "browser-image-compression": "^2.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -597,7 +597,9 @@ } }, "node_modules/@better-fetch/fetch": { - "version": "1.1.12" + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.15.tgz", + "integrity": "sha512-0Bl8YYj1f8qCTNHeSn5+1DWv2hy7rLBrQ8rS8Y9XYloiwZEfc3k4yspIG0llRxafxqhGCwlGRg+F8q1HZRCMXA==" }, "node_modules/@cerebras/cerebras_cloud_sdk": { "version": "1.23.0", @@ -3939,29 +3941,34 @@ "license": "MIT" }, "node_modules/better-auth": { - "version": "1.1.18", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.2.4.tgz", + "integrity": "sha512-/ZK2jbUjm8JwdeCLFrUWUBmexPyI9PkaLVXWLWtN60sMDHTY8B5G72wcHglo1QMFBaw4G0qFkP5ayl9k6XfDaA==", "dependencies": { "@better-auth/utils": "0.2.3", - "@better-fetch/fetch": "1.1.12", + "@better-fetch/fetch": "^1.1.15", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.6.1", "@simplewebauthn/browser": "^13.0.0", "@simplewebauthn/server": "^13.0.0", - "better-call": "0.3.3", + "better-call": "^1.0.3", "defu": "^6.1.4", "jose": "^5.9.6", - "kysely": "^0.27.4", + "kysely": "^0.27.6", "nanostores": "^0.11.3", + "valibot": "1.0.0-beta.15", "zod": "^3.24.1" } }, "node_modules/better-call": { - "version": "0.3.3", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.0.4.tgz", + "integrity": "sha512-NdAihYdkS0IOz1mtz8mw1gWacCxR9r921U8YqB+VB6++rt8edMG13vVL16Y4TBL4XkjMK/DUewEsOOFkw9LJYQ==", "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", - "uncrypto": "^0.1.3", - "zod": "^3.24.1" + "set-cookie-parser": "^2.7.1", + "uncrypto": "^0.1.3" } }, "node_modules/binary-extensions": { @@ -7294,7 +7301,9 @@ } }, "node_modules/kysely": { - "version": "0.27.5", + "version": "0.27.6", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.6.tgz", + "integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -9008,6 +9017,8 @@ }, "node_modules/rou3": { "version": "0.5.1", + "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.5.1.tgz", + "integrity": "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==", "license": "MIT" }, "node_modules/run-parallel": { @@ -9150,6 +9161,12 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/sharp": { "version": "0.33.5", "hasInstallScript": true, @@ -9985,6 +10002,20 @@ "node": ">=10.12.0" } }, + "node_modules/valibot": { + "version": "1.0.0-beta.15", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.0.0-beta.15.tgz", + "integrity": "sha512-BKy8XosZkDHWmYC+cJG74LBzP++Gfntwi33pP3D3RKztz2XV9jmFWnkOi21GoqARP8wAWARwhV6eTr1JcWzjGw==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "dev": true, diff --git a/sim/package.json b/sim/package.json index bf81c8f0b8..e8235019e9 100644 --- a/sim/package.json +++ b/sim/package.json @@ -44,7 +44,7 @@ "@radix-ui/react-tooltip": "^1.1.6", "@vercel/og": "^0.6.5", "@webcontainer/api": "^1.5.1-internal.9", - "better-auth": "^1.1.18", + "better-auth": "^1.2.4", "browser-image-compression": "^2.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1",