diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts index 29a7fad53d..40c10fe42d 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts @@ -13,6 +13,7 @@ import { GoogleAPIsAuthController } from 'src/engine/core-modules/auth/controlle import { GoogleAuthController } from 'src/engine/core-modules/auth/controllers/google-auth.controller'; import { MicrosoftAPIsAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller'; import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller'; +import { OAuthPropagatorController } from 'src/engine/core-modules/auth/controllers/oauth-propagator.controller'; import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-auth.controller'; import { AuthSsoService } from 'src/engine/core-modules/auth/services/auth-sso.service'; import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service'; @@ -126,6 +127,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; MicrosoftAuthController, GoogleAPIsAuthController, MicrosoftAPIsAuthController, + OAuthPropagatorController, SSOAuthController, ], providers: [ diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/oauth-propagator.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/oauth-propagator.controller.ts new file mode 100644 index 0000000000..758ae412f4 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/oauth-propagator.controller.ts @@ -0,0 +1,86 @@ +import { + BadRequestException, + Controller, + ForbiddenException, + Get, + Query, + Res, + UseFilters, + UseGuards, +} from '@nestjs/common'; + +import { Response } from 'express'; +import { isDefined } from 'twenty-shared/utils'; + +import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface'; + +import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; +import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; + +@Controller('auth/oauth-propagator') +@UseFilters(AuthRestApiExceptionFilter) +export class OAuthPropagatorController { + constructor( + private readonly domainServerConfigService: DomainServerConfigService, + private readonly twentyConfigService: TwentyConfigService, + private readonly workspaceDomainsService: WorkspaceDomainsService, + ) {} + + @Get('callback') + @UseGuards(PublicEndpointGuard, NoPermissionGuard) + async propagateOAuthCallback( + @Query('state') state: string, + @Query('code') code: string, + @Res() res: Response, + ) { + if (!isDefined(state)) { + throw new BadRequestException('Missing state parameter'); + } + + if (!isDefined(code)) { + throw new BadRequestException('Missing code parameter'); + } + + const decodedRedirectUri = decodeURIComponent(state); + + let redirectUrl: URL; + + try { + redirectUrl = new URL(decodedRedirectUri); + } catch { + throw new BadRequestException('Invalid redirect URI in state'); + } + + const isValidDomain = await this.isValidDomain(redirectUrl); + + if (!isValidDomain) { + throw new ForbiddenException( + `${redirectUrl.hostname} is not a valid Twenty domain`, + ); + } + + redirectUrl.searchParams.set('code', code); + redirectUrl.searchParams.set('state', state); + + return res.redirect(302, redirectUrl.toString()); + } + + private async isValidDomain(url: URL): Promise { + if ( + this.twentyConfigService.get('NODE_ENV') === NodeEnvironment.DEVELOPMENT + ) { + return true; + } + + const workspace = + await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( + url.href, + ); + + return isDefined(workspace); + } +}