diff --git a/apps/api/src/integration-platform/controllers/oauth.controller.spec.ts b/apps/api/src/integration-platform/controllers/oauth.controller.spec.ts index 01cf1192c0..576aaafa7e 100644 --- a/apps/api/src/integration-platform/controllers/oauth.controller.spec.ts +++ b/apps/api/src/integration-platform/controllers/oauth.controller.spec.ts @@ -4,6 +4,7 @@ import type { Request } from 'express'; import { OAuthController } from './oauth.controller'; import { HybridAuthGuard } from '../../auth/hybrid-auth.guard'; import { PermissionGuard } from '../../auth/permission.guard'; +import { SessionOnlyGuard } from '../../auth/session-only.guard'; import { OAuthStateRepository } from '../repositories/oauth-state.repository'; import { ProviderRepository } from '../repositories/provider.repository'; import { ConnectionRepository } from '../repositories/connection.repository'; @@ -36,6 +37,10 @@ jest.mock('../../auth/permission.guard', () => ({ PermissionGuard: class PermissionGuard {}, })); +jest.mock('../../auth/session-only.guard', () => ({ + SessionOnlyGuard: class SessionOnlyGuard {}, +})); + jest.mock('@trycompai/auth', () => ({ statement: { integration: ['create', 'read', 'update', 'delete'], @@ -134,6 +139,8 @@ describe('OAuthController', () => { }) .overrideGuard(HybridAuthGuard) .useValue(mockGuard) + .overrideGuard(SessionOnlyGuard) + .useValue(mockGuard) .overrideGuard(PermissionGuard) .useValue(mockGuard) .compile(); @@ -176,9 +183,8 @@ describe('OAuthController', () => { mockedGetManifest.mockReturnValue(undefined as never); await expect( - controller.startOAuth('org_1', { + controller.startOAuth('org_1', 'user_1', { providerSlug: 'nonexistent', - userId: 'user_1', }), ).rejects.toThrow(HttpException); }); @@ -189,9 +195,8 @@ describe('OAuthController', () => { } as never); await expect( - controller.startOAuth('org_1', { + controller.startOAuth('org_1', 'user_1', { providerSlug: 'datadog', - userId: 'user_1', }), ).rejects.toThrow(HttpException); }); @@ -219,9 +224,8 @@ describe('OAuthController', () => { }); await expect( - controller.startOAuth('org_1', { + controller.startOAuth('org_1', 'user_1', { providerSlug: 'github', - userId: 'user_1', }), ).rejects.toThrow(HttpException); }); @@ -254,9 +258,8 @@ describe('OAuthController', () => { state: 'random_state_token', }); - const result = await controller.startOAuth('org_1', { + const result = await controller.startOAuth('org_1', 'user_1', { providerSlug: 'github', - userId: 'user_1', }); expect(result.authorizationUrl).toContain( @@ -303,9 +306,8 @@ describe('OAuthController', () => { state: 'state_abc', }); - const result = await controller.startOAuth('org_1', { + const result = await controller.startOAuth('org_1', 'user_1', { providerSlug: 'linear', - userId: 'user_1', }); expect(result.authorizationUrl).toContain('code_challenge='); diff --git a/apps/api/src/integration-platform/controllers/oauth.controller.ts b/apps/api/src/integration-platform/controllers/oauth.controller.ts index 3d3d5e8120..e62c6ca58b 100644 --- a/apps/api/src/integration-platform/controllers/oauth.controller.ts +++ b/apps/api/src/integration-platform/controllers/oauth.controller.ts @@ -17,8 +17,9 @@ import { randomBytes, createHash } from 'crypto'; import { auth } from '../../auth/auth.server'; import { HybridAuthGuard } from '../../auth/hybrid-auth.guard'; import { PermissionGuard } from '../../auth/permission.guard'; +import { SessionOnlyGuard } from '../../auth/session-only.guard'; import { RequirePermission } from '../../auth/require-permission.decorator'; -import { OrganizationId } from '../../auth/auth-context.decorator'; +import { OrganizationId, UserId } from '../../auth/auth-context.decorator'; import { OAuthStateRepository } from '../repositories/oauth-state.repository'; import { ProviderRepository } from '../repositories/provider.repository'; import { ConnectionRepository } from '../repositories/connection.repository'; @@ -30,7 +31,6 @@ import { getManifest, type OAuthConfig } from '@trycompai/integration-platform'; interface StartOAuthDto { providerSlug: string; - userId: string; redirectUrl?: string; } @@ -86,13 +86,18 @@ export class OAuthController { */ @Post('start') @ApiOperation({ summary: 'Start an OAuth authorization flow' }) - @UseGuards(HybridAuthGuard, PermissionGuard) + // SessionOnlyGuard rejects API-key and service-token callers with a 403 + // before @UserId() is evaluated. The OAuth callback also requires a real + // session (see checkSessionMatchesState), so non-session auth could never + // complete the flow anyway. + @UseGuards(HybridAuthGuard, SessionOnlyGuard, PermissionGuard) @RequirePermission('integration', 'create') async startOAuth( @OrganizationId() organizationId: string, + @UserId() userId: string, @Body() body: StartOAuthDto, ): Promise<{ authorizationUrl: string }> { - const { providerSlug, userId, redirectUrl } = body; + const { providerSlug, redirectUrl } = body; // Get manifest and OAuth config const manifest = getManifest(providerSlug); diff --git a/apps/app/src/hooks/use-integration-platform.ts b/apps/app/src/hooks/use-integration-platform.ts index a1f001af0c..cd6a9e1d0f 100644 --- a/apps/app/src/hooks/use-integration-platform.ts +++ b/apps/app/src/hooks/use-integration-platform.ts @@ -216,7 +216,6 @@ export function useIntegrationMutations() { const response = await api.post('/v1/integrations/oauth/start', { providerSlug, organizationId: orgId, - userId: '', // Will be filled by API from auth context redirectUrl, });