OAuth Client - Add OAuth propagator (#18266)

For OAuth Server without wildcard redirect URL

https://www.benanderson.co.uk/2023/07/28/dynamic-redirect-uris-oauth/
This commit is contained in:
Etienne
2026-02-26 13:44:54 +01:00
committed by GitHub
parent 08bfbfda45
commit 2f0103faa5
2 changed files with 88 additions and 0 deletions
@@ -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: [
@@ -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<boolean> {
if (
this.twentyConfigService.get('NODE_ENV') === NodeEnvironment.DEVELOPMENT
) {
return true;
}
const workspace =
await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
url.href,
);
return isDefined(workspace);
}
}