From 5783c41df267a882c28eae9e3d720a777e6d2c77 Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Fri, 24 Jan 2025 10:36:18 +0100 Subject: [PATCH] fix(auth): Improve error management with sso + fix microsoft saml (#9799) Fix #9760 #9758 --- .../components/SettingsSSOSAMLForm.tsx | 4 +- .../utils/parseSAMLMetadataFromXMLFile.ts | 84 ++++++++++++----- .../SSOIdentityProviderSchema.ts | 14 +-- .../core-query-builder.factory.ts | 2 +- .../core-modules/auth/auth.exception.ts | 1 - .../engine/core-modules/auth/auth.module.ts | 2 + .../core-modules/auth/auth.resolver.spec.ts | 2 +- .../engine/core-modules/auth/auth.resolver.ts | 2 +- .../google-apis-auth.controller.ts | 2 +- .../controllers/google-auth.controller.ts | 12 ++- .../microsoft-apis-auth.controller.ts | 2 +- .../controllers/microsoft-auth.controller.ts | 12 ++- .../auth/controllers/sso-auth.controller.ts | 81 +++++++++------- .../filters/auth-oauth-exception.filter.ts | 2 +- .../enterprise-features-enabled.guard.ts | 39 ++++++++ .../auth/guards/google-oauth.guard.ts | 93 ++++++++++++------- .../guards/google-provider-enabled.guard.ts | 38 +++++--- .../auth/guards/microsoft-oauth.guard.ts | 82 ++++++++++------ .../microsoft-provider-enabled.guard.ts | 38 +++++--- .../auth/guards/oidc-auth.guard.ts | 34 +++++-- .../auth/guards/saml-auth.guard.ts | 35 +++++-- .../auth/guards/sso-provider-enabled.guard.ts | 27 ------ .../auth/services/auth.service.spec.ts | 2 +- .../auth/services/auth.service.ts | 4 +- .../services/reset-password.service.spec.ts | 2 +- .../auth/services/reset-password.service.ts | 2 +- .../auth/services/sign-in-up.service.spec.ts | 2 +- .../auth/services/sign-in-up.service.ts | 2 +- .../auth/strategies/oidc.auth.strategy.ts | 13 +-- .../billing-portal.workspace-service.ts | 2 +- .../services/stripe-billing-portal.service.ts | 2 +- .../client-config.resolver.spec.ts | 2 +- .../client-config/client-config.resolver.ts | 2 +- .../domain-manager/domain-manager.module.ts | 2 +- .../domain-manager.service.spec.ts | 3 + .../domain-manager.service.ts | 55 ++++------- .../email-verification.resolver.ts | 2 +- .../services/email-verification.service.ts | 13 +-- .../guard-redirect/guard-redirect.module.ts | 11 +++ .../services/guard-redirect.service.ts | 38 ++++++++ .../core-modules/sso/services/sso.service.ts | 7 +- .../src/engine/core-modules/sso/sso.module.ts | 4 + .../engine/core-modules/sso/sso.resolver.ts | 12 +-- .../engine/core-modules/user/user.resolver.ts | 2 +- .../workspace-invitation.service.spec.ts | 2 +- .../services/workspace-invitation.service.ts | 2 +- .../services/workspace.service.spec.ts | 2 +- .../workspace/workspace.resolver.ts | 2 +- .../src/filters/unhandled-exception.filter.ts | 2 +- 49 files changed, 499 insertions(+), 303 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/auth/guards/enterprise-features-enabled.guard.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/auth/guards/sso-provider-enabled.guard.ts rename packages/twenty-server/src/engine/core-modules/domain-manager/{service => services}/domain-manager.service.spec.ts (98%) rename packages/twenty-server/src/engine/core-modules/domain-manager/{service => services}/domain-manager.service.ts (87%) create mode 100644 packages/twenty-server/src/engine/core-modules/guard-redirect/guard-redirect.module.ts create mode 100644 packages/twenty-server/src/engine/core-modules/guard-redirect/services/guard-redirect.service.ts diff --git a/packages/twenty-front/src/modules/settings/security/components/SettingsSSOSAMLForm.tsx b/packages/twenty-front/src/modules/settings/security/components/SettingsSSOSAMLForm.tsx index ee8933cff203..af6b898cbc8d 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SettingsSSOSAMLForm.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SettingsSSOSAMLForm.tsx @@ -62,12 +62,12 @@ export const SettingsSSOSAMLForm = () => { if (isDefined(e.target.files)) { const text = await e.target.files[0].text(); const samlMetadataParsed = parseSAMLMetadataFromXMLFile(text); + e.target.value = ''; if (!samlMetadataParsed.success) { - enqueueSnackBar('Invalid File', { + return enqueueSnackBar('Invalid File', { variant: SnackBarVariant.Error, duration: 2000, }); - return; } setValue('ssoURL', samlMetadataParsed.data.ssoUrl); setValue('certificate', samlMetadataParsed.data.certificate); diff --git a/packages/twenty-front/src/modules/settings/security/utils/parseSAMLMetadataFromXMLFile.ts b/packages/twenty-front/src/modules/settings/security/utils/parseSAMLMetadataFromXMLFile.ts index 6ed9924b07ef..b67d9105970f 100644 --- a/packages/twenty-front/src/modules/settings/security/utils/parseSAMLMetadataFromXMLFile.ts +++ b/packages/twenty-front/src/modules/settings/security/utils/parseSAMLMetadataFromXMLFile.ts @@ -8,6 +8,29 @@ const validator = z.object({ certificate: z.string().min(1), }); +const getByPrefixAndKey = ( + xmlDoc: Document | Element, + key: string, + prefix = 'md', +): Element | undefined => { + return ( + xmlDoc.getElementsByTagName(`${prefix}:${key}`)?.[0] ?? + xmlDoc.getElementsByTagName(`${key}`)?.[0] + ); +}; + +const getAllByPrefixAndKey = ( + xmlDoc: Document | Element, + key: string, + prefix = 'md', +) => { + const withPrefix = xmlDoc.getElementsByTagName(`${prefix}:${key}`); + if (withPrefix.length !== 0) { + return Array.from(withPrefix); + } + return Array.from(xmlDoc.getElementsByTagName(`${key}`)); +}; + export const parseSAMLMetadataFromXMLFile = ( xmlString: string, ): @@ -20,33 +43,44 @@ export const parseSAMLMetadataFromXMLFile = ( throw new Error('Error parsing XML'); } - const entityDescriptor = xmlDoc.getElementsByTagName( - 'md:EntityDescriptor', - )?.[0]; - const idpSSODescriptor = xmlDoc.getElementsByTagName( - 'md:IDPSSODescriptor', - )?.[0]; - const keyDescriptor = xmlDoc.getElementsByTagName('md:KeyDescriptor')[0]; - const keyInfo = keyDescriptor?.getElementsByTagName('ds:KeyInfo')[0]; - const x509Data = keyInfo?.getElementsByTagName('ds:X509Data')[0]; - const x509Certificate = x509Data - ?.getElementsByTagName('ds:X509Certificate')?.[0] - .textContent?.trim(); - - const singleSignOnServices = Array.from( - idpSSODescriptor.getElementsByTagName('md:SingleSignOnService'), - ).map((service) => ({ - Binding: service.getAttribute('Binding'), - Location: service.getAttribute('Location'), - })); + const entityDescriptor = getByPrefixAndKey(xmlDoc, 'EntityDescriptor'); + if (!entityDescriptor) throw new Error('No EntityDescriptor found'); + + const IDPSSODescriptor = getByPrefixAndKey(xmlDoc, 'IDPSSODescriptor'); + if (!IDPSSODescriptor) throw new Error('No IDPSSODescriptor found'); + + const keyDescriptors = getByPrefixAndKey(IDPSSODescriptor, 'KeyDescriptor'); + if (!keyDescriptors) throw new Error('No KeyDescriptor found'); + + const keyInfo = getByPrefixAndKey(keyDescriptors, 'KeyInfo', 'ds'); + if (!keyInfo) throw new Error('No KeyInfo found'); + + const x509Data = getByPrefixAndKey(keyInfo, 'X509Data', 'ds'); + if (!x509Data) throw new Error('No X509Data found'); + + const x509Certificate = getByPrefixAndKey( + x509Data, + 'X509Certificate', + 'ds', + )?.textContent?.trim(); + if (!x509Certificate) throw new Error('No X509Certificate found'); + + const singleSignOnServices = getAllByPrefixAndKey( + IDPSSODescriptor, + 'SingleSignOnService', + ); const result = { - ssoUrl: singleSignOnServices.find((singleSignOnService) => { - return ( - singleSignOnService.Binding === - 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' - ); - })?.Location, + ssoUrl: singleSignOnServices + .map((service) => ({ + Binding: service.getAttribute('Binding'), + Location: service.getAttribute('Location'), + })) + .find( + (singleSignOnService) => + singleSignOnService.Binding === + 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + )?.Location, certificate: x509Certificate, entityID: entityDescriptor?.getAttribute('entityID'), }; diff --git a/packages/twenty-front/src/modules/settings/security/validation-schemas/SSOIdentityProviderSchema.ts b/packages/twenty-front/src/modules/settings/security/validation-schemas/SSOIdentityProviderSchema.ts index adfd8680f590..d4dc0f6486a5 100644 --- a/packages/twenty-front/src/modules/settings/security/validation-schemas/SSOIdentityProviderSchema.ts +++ b/packages/twenty-front/src/modules/settings/security/validation-schemas/SSOIdentityProviderSchema.ts @@ -5,17 +5,17 @@ import { z } from 'zod'; export const SSOIdentitiesProvidersOIDCParamsSchema = z .object({ type: z.literal('OIDC'), - clientID: z.string().optional(), - clientSecret: z.string().optional(), + clientID: z.string().nonempty(), + clientSecret: z.string().nonempty(), }) .required(); export const SSOIdentitiesProvidersSAMLParamsSchema = z .object({ type: z.literal('SAML'), - id: z.string().optional(), - ssoURL: z.string().url().optional(), - certificate: z.string().optional(), + id: z.string().nonempty(), + ssoURL: z.string().url().nonempty(), + certificate: z.string().nonempty(), }) .required(); @@ -27,8 +27,8 @@ export const SSOIdentitiesProvidersParamsSchema = z .and( z .object({ - name: z.string().min(1), - issuer: z.string().url().optional(), + name: z.string().nonempty(), + issuer: z.string().url().nonempty(), }) .required(), ); diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts index 7c1658fe9cd9..095d8dd466a2 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts @@ -21,7 +21,7 @@ import { Query } from 'src/engine/api/rest/core/types/query.type'; import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; @Injectable() export class CoreQueryBuilderFactory { diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts index b658f25e28d1..e5d4e03a55ea 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts @@ -1,7 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; export class AuthException extends CustomException { - code: AuthExceptionCode; constructor(message: string, code: AuthExceptionCode) { super(message, code); } 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 b975d94abc5f..929da2ac3e02 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 @@ -44,6 +44,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module'; +import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module'; import { AuthResolver } from './auth.resolver'; @@ -81,6 +82,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; FeatureFlagModule, WorkspaceInvitationModule, EmailVerificationModule, + GuardRedirectModule, ], controllers: [ GoogleAuthController, diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts index de7f682ca78a..ae46def3dab4 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts @@ -3,7 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service'; diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index 952df1567534..53329513e06e 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -31,7 +31,7 @@ import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/l import { RenewTokenService } from 'src/engine/core-modules/auth/token/services/renew-token.service'; import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service'; import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service'; diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts index 203451a0c3e5..f5f4f55fda6f 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts @@ -24,7 +24,7 @@ import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; @Controller('auth/google-apis') @UseFilters(AuthRestApiExceptionFilter) diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts index 7a4b9ba4fe18..f9d0052d2edb 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts @@ -22,8 +22,9 @@ import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/ import { AuthService } from 'src/engine/core-modules/auth/services/auth.service'; import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { User } from 'src/engine/core-modules/user/user.entity'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; @Controller('auth/google') @UseFilters(AuthRestApiExceptionFilter) @@ -32,6 +33,7 @@ export class GoogleAuthController { private readonly loginTokenService: LoginTokenService, private readonly authService: AuthService, private readonly domainManagerService: DomainManagerService, + private readonly environmentService: EnvironmentService, @InjectRepository(User, 'core') private readonly userRepository: Repository, ) {} @@ -120,9 +122,11 @@ export class GoogleAuthController { } catch (err) { if (err instanceof AuthException) { return res.redirect( - this.domainManagerService.computeRedirectErrorUrl(err.message, { - subdomain: currentWorkspace?.subdomain, - }), + this.domainManagerService.computeRedirectErrorUrl( + err.message, + currentWorkspace?.subdomain ?? + this.environmentService.get('DEFAULT_SUBDOMAIN'), + ), ); } throw new AuthException(err, AuthExceptionCode.INTERNAL_SERVER_ERROR); diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts index c1c2c65d42e9..e5ee09085248 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts @@ -25,7 +25,7 @@ import { EnvironmentService } from 'src/engine/core-modules/environment/environm import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; @Controller('auth/microsoft-apis') @UseFilters(AuthRestApiExceptionFilter) diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts index 90c1295402b6..e90937eda2de 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts @@ -18,8 +18,9 @@ import { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guar import { AuthService } from 'src/engine/core-modules/auth/services/auth.service'; import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { User } from 'src/engine/core-modules/user/user.entity'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; @Controller('auth/microsoft') @UseFilters(AuthRestApiExceptionFilter) @@ -28,6 +29,7 @@ export class MicrosoftAuthController { private readonly loginTokenService: LoginTokenService, private readonly authService: AuthService, private readonly domainManagerService: DomainManagerService, + private readonly environmentService: EnvironmentService, @InjectRepository(User, 'core') private readonly userRepository: Repository, ) {} @@ -119,9 +121,11 @@ export class MicrosoftAuthController { } catch (err) { if (err instanceof AuthException) { return res.redirect( - this.domainManagerService.computeRedirectErrorUrl(err.message, { - subdomain: currentWorkspace?.subdomain, - }), + this.domainManagerService.computeRedirectErrorUrl( + err.message, + currentWorkspace?.subdomain ?? + this.environmentService.get('DEFAULT_SUBDOMAIN'), + ), ); } throw err; diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts index 5a8c35f75031..a482a86756f4 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts @@ -22,7 +22,7 @@ import { import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; import { OIDCAuthGuard } from 'src/engine/core-modules/auth/guards/oidc-auth.guard'; import { SAMLAuthGuard } from 'src/engine/core-modules/auth/guards/saml-auth.guard'; -import { SSOProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/sso-provider-enabled.guard'; +import { EnterpriseFeaturesEnabledGuard } from 'src/engine/core-modules/auth/guards/enterprise-features-enabled.guard'; import { AuthService } from 'src/engine/core-modules/auth/services/auth.service'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; @@ -30,17 +30,19 @@ import { IdentityProviderType, WorkspaceSSOIdentityProvider, } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { User } from 'src/engine/core-modules/user/user.entity'; +import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; @Controller('auth') -@UseFilters(AuthRestApiExceptionFilter) export class SSOAuthController { constructor( private readonly loginTokenService: LoginTokenService, private readonly authService: AuthService, private readonly domainManagerService: DomainManagerService, - private readonly ssoService: SSOService, + private readonly environmentService: EnvironmentService, + private readonly sSOService: SSOService, @InjectRepository(User, 'core') private readonly userRepository: Repository, @InjectRepository(WorkspaceSSOIdentityProvider, 'core') @@ -48,15 +50,16 @@ export class SSOAuthController { ) {} @Get('saml/metadata/:identityProviderId') - @UseGuards(SSOProviderEnabledGuard) + @UseGuards(EnterpriseFeaturesEnabledGuard) + @UseFilters(AuthRestApiExceptionFilter) async generateMetadata(@Req() req: any): Promise { return generateServiceProviderMetadata({ wantAssertionsSigned: false, - issuer: this.ssoService.buildIssuerURL({ + issuer: this.sSOService.buildIssuerURL({ id: req.params.identityProviderId, type: IdentityProviderType.SAML, }), - callbackUrl: this.ssoService.buildCallbackUrl({ + callbackUrl: this.sSOService.buildCallbackUrl({ id: req.params.identityProviderId, type: IdentityProviderType.SAML, }), @@ -64,29 +67,40 @@ export class SSOAuthController { } @Get('oidc/login/:identityProviderId') - @UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard) + @UseGuards(EnterpriseFeaturesEnabledGuard, OIDCAuthGuard) + @UseFilters(AuthRestApiExceptionFilter) async oidcAuth() { // As this method is protected by OIDC Auth guard, it will trigger OIDC SSO flow return; } @Get('saml/login/:identityProviderId') - @UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard) + @UseGuards(EnterpriseFeaturesEnabledGuard, SAMLAuthGuard) + @UseFilters(AuthRestApiExceptionFilter) async samlAuth() { // As this method is protected by SAML Auth guard, it will trigger SAML SSO flow return; } @Get('oidc/callback') - @UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard) + @UseGuards(EnterpriseFeaturesEnabledGuard, OIDCAuthGuard) + @UseFilters(AuthOAuthExceptionFilter) async oidcAuthCallback(@Req() req: any, @Res() res: Response) { - return this.authCallback(req, res); + return await this.authCallback(req, res); } @Post('saml/callback/:identityProviderId') - @UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard) + @UseGuards(EnterpriseFeaturesEnabledGuard, SAMLAuthGuard) + @UseFilters(AuthOAuthExceptionFilter) async samlAuthCallback(@Req() req: any, @Res() res: Response) { - return this.authCallback(req, res); + try { + return await this.authCallback(req, res); + } catch (err) { + return new AuthException( + err.message ?? 'Access denied', + AuthExceptionCode.OAUTH_ACCESS_DENIED, + ); + } } private async authCallback({ user }: any, res: Response) { @@ -95,21 +109,21 @@ export class SSOAuthController { user.identityProviderId, ); - if (!workspaceIdentityProvider) { - throw new AuthException( - 'Identity provider not found', - AuthExceptionCode.INVALID_DATA, - ); - } + try { + if (!workspaceIdentityProvider) { + throw new AuthException( + 'Identity provider not found', + AuthExceptionCode.OAUTH_ACCESS_DENIED, + ); + } - if (!user.user.email) { - throw new AuthException( - 'Email not found', - AuthExceptionCode.INVALID_DATA, - ); - } + if (!user.user.email) { + throw new AuthException( + 'Email not found from identity provider.', + AuthExceptionCode.OAUTH_ACCESS_DENIED, + ); + } - try { const { loginToken, identityProvider } = await this.generateLoginToken( user.user, workspaceIdentityProvider, @@ -122,14 +136,13 @@ export class SSOAuthController { }), ); } catch (err) { - if (err instanceof AuthException) { - return res.redirect( - this.domainManagerService.computeRedirectErrorUrl(err.message, { - subdomain: workspaceIdentityProvider.workspace.subdomain, - }), - ); - } - throw err; + return res.redirect( + this.domainManagerService.computeRedirectErrorUrl( + err.message, + workspaceIdentityProvider?.workspace.subdomain ?? + this.environmentService.get('DEFAULT_SUBDOMAIN'), + ), + ); } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts index 8adf85481814..26c804f09f12 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts @@ -6,7 +6,7 @@ import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service'; @Catch(AuthException) diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/enterprise-features-enabled.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/enterprise-features-enabled.guard.ts new file mode 100644 index 000000000000..4f9d664ce71d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/enterprise-features-enabled.guard.ts @@ -0,0 +1,39 @@ +/* @license Enterprise */ + +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; + +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; + +@Injectable() +export class EnterpriseFeaturesEnabledGuard implements CanActivate { + constructor( + private readonly guardRedirectService: GuardRedirectService, + private readonly environmentService: EnvironmentService, + ) {} + + canActivate(context: ExecutionContext): boolean { + try { + if (!this.environmentService.get('ENTERPRISE_KEY')) { + throw new AuthException( + 'Enterprise key missing', + AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE, + ); + } + + return true; + } catch (err) { + this.guardRedirectService.dispatchErrorFromGuard( + context, + err, + this.guardRedirectService.getSubdomainFromContext(context), + ); + + return false; + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts index f080dfe77eba..e45a5ed1338a 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts @@ -1,14 +1,25 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; @Injectable() export class GoogleOauthGuard extends AuthGuard('google') { - constructor() { + constructor( + private readonly guardRedirectService: GuardRedirectService, + private readonly environmentService: EnvironmentService, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + ) { super({ prompt: 'select_account', }); @@ -16,43 +27,59 @@ export class GoogleOauthGuard extends AuthGuard('google') { async canActivate(context: ExecutionContext) { const request = context.switchToHttp().getRequest(); - const workspaceInviteHash = request.query.inviteHash; - const workspacePersonalInviteToken = request.query.inviteToken; + let workspace: Workspace | null = null; - if (request.query.error === 'access_denied') { - throw new AuthException( - 'Google OAuth access denied', - AuthExceptionCode.OAUTH_ACCESS_DENIED, - ); - } + try { + if ( + request.query.workspaceId && + typeof request.query.workspaceId === 'string' + ) { + request.params.workspaceId = request.query.workspaceId; + workspace = await this.workspaceRepository.findOneBy({ + id: request.query.workspaceId, + }); + } - if (workspaceInviteHash && typeof workspaceInviteHash === 'string') { - request.params.workspaceInviteHash = workspaceInviteHash; - } + const workspaceInviteHash = request.query.inviteHash; + const workspacePersonalInviteToken = request.query.inviteToken; - if ( - workspacePersonalInviteToken && - typeof workspacePersonalInviteToken === 'string' - ) { - request.params.workspacePersonalInviteToken = - workspacePersonalInviteToken; - } + if (request.query.error === 'access_denied') { + throw new AuthException( + 'Google OAuth access denied', + AuthExceptionCode.OAUTH_ACCESS_DENIED, + ); + } - if ( - request.query.workspaceId && - typeof request.query.workspaceId === 'string' - ) { - request.params.workspaceId = request.query.workspaceId; - } + if (workspaceInviteHash && typeof workspaceInviteHash === 'string') { + request.params.workspaceInviteHash = workspaceInviteHash; + } - if ( - request.query.billingCheckoutSessionState && - typeof request.query.billingCheckoutSessionState === 'string' - ) { - request.params.billingCheckoutSessionState = - request.query.billingCheckoutSessionState; - } + if ( + workspacePersonalInviteToken && + typeof workspacePersonalInviteToken === 'string' + ) { + request.params.workspacePersonalInviteToken = + workspacePersonalInviteToken; + } + + if ( + request.query.billingCheckoutSessionState && + typeof request.query.billingCheckoutSessionState === 'string' + ) { + request.params.billingCheckoutSessionState = + request.query.billingCheckoutSessionState; + } - return (await super.canActivate(context)) as boolean; + return (await super.canActivate(context)) as boolean; + } catch (err) { + this.guardRedirectService.dispatchErrorFromGuard( + context, + err, + workspace?.subdomain ?? + this.environmentService.get('DEFAULT_SUBDOMAIN'), + ); + + return false; + } } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-provider-enabled.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-provider-enabled.guard.ts index 3b1716d48cd9..8cbdde7c6a21 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-provider-enabled.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-provider-enabled.guard.ts @@ -1,6 +1,4 @@ -import { CanActivate, Injectable } from '@nestjs/common'; - -import { Observable } from 'rxjs'; +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { AuthException, @@ -8,21 +6,35 @@ import { } from 'src/engine/core-modules/auth/auth.exception'; import { GoogleStrategy } from 'src/engine/core-modules/auth/strategies/google.auth.strategy'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; @Injectable() export class GoogleProviderEnabledGuard implements CanActivate { - constructor(private readonly environmentService: EnvironmentService) {} + constructor( + private readonly environmentService: EnvironmentService, + private readonly guardRedirectService: GuardRedirectService, + ) {} - canActivate(): boolean | Promise | Observable { - if (!this.environmentService.get('AUTH_GOOGLE_ENABLED')) { - throw new AuthException( - 'Google auth is not enabled', - AuthExceptionCode.GOOGLE_API_AUTH_DISABLED, - ); - } + canActivate(context: ExecutionContext): boolean { + try { + if (!this.environmentService.get('AUTH_GOOGLE_ENABLED')) { + throw new AuthException( + 'Google auth is not enabled', + AuthExceptionCode.GOOGLE_API_AUTH_DISABLED, + ); + } - new GoogleStrategy(this.environmentService); + new GoogleStrategy(this.environmentService); - return true; + return true; + } catch (err) { + this.guardRedirectService.dispatchErrorFromGuard( + context, + err, + this.guardRedirectService.getSubdomainFromContext(context), + ); + + return false; + } } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts index 6a2d40faf2c8..0a41344454cd 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts @@ -1,9 +1,21 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @Injectable() export class MicrosoftOAuthGuard extends AuthGuard('microsoft') { - constructor() { + constructor( + private readonly guardRedirectService: GuardRedirectService, + private readonly environmentService: EnvironmentService, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + ) { super({ prompt: 'select_account', }); @@ -11,36 +23,52 @@ export class MicrosoftOAuthGuard extends AuthGuard('microsoft') { async canActivate(context: ExecutionContext) { const request = context.switchToHttp().getRequest(); - const workspaceInviteHash = request.query.inviteHash; - const workspacePersonalInviteToken = request.query.inviteToken; + let workspace: Workspace | null = null; - if (workspaceInviteHash && typeof workspaceInviteHash === 'string') { - request.params.workspaceInviteHash = workspaceInviteHash; - } + try { + if ( + request.query.workspaceId && + typeof request.query.workspaceId === 'string' + ) { + request.params.workspaceId = request.query.workspaceId; + workspace = await this.workspaceRepository.findOneBy({ + id: request.query.workspaceId, + }); + } - if ( - workspacePersonalInviteToken && - typeof workspacePersonalInviteToken === 'string' - ) { - request.params.workspacePersonalInviteToken = - workspacePersonalInviteToken; - } + const workspaceInviteHash = request.query.inviteHash; + const workspacePersonalInviteToken = request.query.inviteToken; - if ( - request.query.workspaceSubdomain && - typeof request.query.workspaceSubdomain === 'string' - ) { - request.params.workspaceSubdomain = request.query.workspaceSubdomain; - } + if (workspaceInviteHash && typeof workspaceInviteHash === 'string') { + request.params.workspaceInviteHash = workspaceInviteHash; + } - if ( - request.query.billingCheckoutSessionState && - typeof request.query.billingCheckoutSessionState === 'string' - ) { - request.params.billingCheckoutSessionState = - request.query.billingCheckoutSessionState; - } + if ( + workspacePersonalInviteToken && + typeof workspacePersonalInviteToken === 'string' + ) { + request.params.workspacePersonalInviteToken = + workspacePersonalInviteToken; + } + + if ( + request.query.billingCheckoutSessionState && + typeof request.query.billingCheckoutSessionState === 'string' + ) { + request.params.billingCheckoutSessionState = + request.query.billingCheckoutSessionState; + } - return (await super.canActivate(context)) as boolean; + return (await super.canActivate(context)) as boolean; + } catch (err) { + this.guardRedirectService.dispatchErrorFromGuard( + context, + err, + workspace?.subdomain ?? + this.environmentService.get('DEFAULT_SUBDOMAIN'), + ); + + return false; + } } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard.ts index 076b2a6b2aac..e35065a04c71 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard.ts @@ -1,6 +1,4 @@ -import { CanActivate, Injectable } from '@nestjs/common'; - -import { Observable } from 'rxjs'; +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { AuthException, @@ -8,21 +6,35 @@ import { } from 'src/engine/core-modules/auth/auth.exception'; import { MicrosoftStrategy } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; @Injectable() export class MicrosoftProviderEnabledGuard implements CanActivate { - constructor(private readonly environmentService: EnvironmentService) {} + constructor( + private readonly environmentService: EnvironmentService, + private readonly guardRedirectService: GuardRedirectService, + ) {} - canActivate(): boolean | Promise | Observable { - if (!this.environmentService.get('AUTH_MICROSOFT_ENABLED')) { - throw new AuthException( - 'Microsoft auth is not enabled', - AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED, - ); - } + canActivate(context: ExecutionContext): boolean { + try { + if (!this.environmentService.get('AUTH_MICROSOFT_ENABLED')) { + throw new AuthException( + 'Microsoft auth is not enabled', + AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED, + ); + } - new MicrosoftStrategy(this.environmentService); + new MicrosoftStrategy(this.environmentService); - return true; + return true; + } catch (err) { + this.guardRedirectService.dispatchErrorFromGuard( + context, + err, + this.guardRedirectService.getSubdomainFromContext(context), + ); + + return false; + } } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts index d7b8de1e8a47..3108476d8d4e 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts @@ -11,10 +11,18 @@ import { } from 'src/engine/core-modules/auth/auth.exception'; import { OIDCAuthStrategy } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy'; import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; +import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; @Injectable() export class OIDCAuthGuard extends AuthGuard('openidconnect') { - constructor(private readonly ssoService: SSOService) { + constructor( + private readonly sSOService: SSOService, + private readonly guardRedirectService: GuardRedirectService, + private readonly environmentService: EnvironmentService, + ) { super(); } @@ -38,13 +46,17 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') { } async canActivate(context: ExecutionContext): Promise { - try { - const request = context.switchToHttp().getRequest(); + const request = context.switchToHttp().getRequest(); + + let identityProvider: + | (SSOConfiguration & WorkspaceSSOIdentityProvider) + | null = null; + try { const identityProviderId = this.getIdentityProviderId(request); - const identityProvider = - await this.ssoService.findSSOIdentityProviderById(identityProviderId); + identityProvider = + await this.sSOService.findSSOIdentityProviderById(identityProviderId); if (!identityProvider) { throw new AuthException( @@ -56,17 +68,19 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') { const issuer = await Issuer.discover(identityProvider.issuer); new OIDCAuthStrategy( - this.ssoService.getOIDCClient(identityProvider, issuer), + this.sSOService.getOIDCClient(identityProvider, issuer), identityProvider.id, ); return (await super.canActivate(context)) as boolean; } catch (err) { - if (err instanceof AuthException) { - return false; - } + this.guardRedirectService.dispatchErrorFromGuard( + context, + err, + identityProvider?.workspace.subdomain ?? + this.environmentService.get('DEFAULT_SUBDOMAIN'), + ); - // TODO AMOREAUX: trigger sentry error return false; } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts index 22ecb2306676..8a428e3b576e 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts @@ -9,33 +9,50 @@ import { } from 'src/engine/core-modules/auth/auth.exception'; import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy'; import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; +import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; @Injectable() export class SAMLAuthGuard extends AuthGuard('saml') { - constructor(private readonly sSOService: SSOService) { + constructor( + private readonly sSOService: SSOService, + private readonly guardRedirectService: GuardRedirectService, + private readonly environmentService: EnvironmentService, + ) { super(); } async canActivate(context: ExecutionContext) { + const request = context.switchToHttp().getRequest(); + + let identityProvider: + | (SSOConfiguration & WorkspaceSSOIdentityProvider) + | null = null; + try { - const request = context.switchToHttp().getRequest(); + identityProvider = await this.sSOService.findSSOIdentityProviderById( + request.params.identityProviderId, + ); - if (!request.params.identityProviderId) { + if (!identityProvider) { throw new AuthException( - 'Invalid SAML identity provider', + 'Identity provider not found', AuthExceptionCode.INVALID_DATA, ); } - new SamlAuthStrategy(this.sSOService); return (await super.canActivate(context)) as boolean; } catch (err) { - if (err instanceof AuthException) { - return false; - } + this.guardRedirectService.dispatchErrorFromGuard( + context, + err, + identityProvider?.workspace.subdomain ?? + this.environmentService.get('DEFAULT_SUBDOMAIN'), + ); - // TODO AMOREAUX: trigger sentry error return false; } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/sso-provider-enabled.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/sso-provider-enabled.guard.ts deleted file mode 100644 index 35df1b3d3619..000000000000 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/sso-provider-enabled.guard.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* @license Enterprise */ - -import { CanActivate, Injectable } from '@nestjs/common'; - -import { Observable } from 'rxjs'; - -import { - AuthException, - AuthExceptionCode, -} from 'src/engine/core-modules/auth/auth.exception'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; - -@Injectable() -export class SSOProviderEnabledGuard implements CanActivate { - constructor(private readonly environmentService: EnvironmentService) {} - - canActivate(): boolean | Promise | Observable { - if (!this.environmentService.get('ENTERPRISE_KEY')) { - throw new AuthException( - 'Enterprise key must be defined to use SSO', - AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE, - ); - } - - return true; - } -} diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.spec.ts index 31ec9b0c8cd3..109fef2bf1e0 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.spec.ts @@ -9,7 +9,7 @@ import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service'; import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index 6d8bc98ecfe2..1ff7e8060353 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -37,7 +37,7 @@ import { WorkspaceInviteHashValid } from 'src/engine/core-modules/auth/dto/works import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service'; import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; @@ -460,7 +460,7 @@ export class AuthService { billingCheckoutSessionState, }: { loginToken: string; - subdomain?: string; + subdomain: string; billingCheckoutSessionState?: string; }) { const url = this.domainManagerService.buildWorkspaceURL({ diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.spec.ts index c056bbfaf874..237964556eca 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.spec.ts @@ -13,7 +13,7 @@ import { EmailService } from 'src/engine/core-modules/email/email.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { ResetPasswordService } from './reset-password.service'; diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts index 90e9b13db54d..df84d6f0c304 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts @@ -21,7 +21,7 @@ import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-p import { InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity'; import { PasswordResetToken } from 'src/engine/core-modules/auth/dto/token.entity'; import { ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.entity'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { User } from 'src/engine/core-modules/user/user.entity'; diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts index b4cdba99ecd8..8d92084a2605 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts @@ -12,7 +12,7 @@ import { ExistingUserOrPartialUserWithPicture, SignInUpBaseParams, } from 'src/engine/core-modules/auth/types/signInUp.type'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts index 5d62e6bf7f22..18356cba91a1 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts @@ -29,7 +29,7 @@ import { SignInUpBaseParams, SignInUpNewUserPayload, } from 'src/engine/core-modules/auth/types/signInUp.type'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/oidc.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/oidc.auth.strategy.ts index 493e3972d3c7..8d3b94e9b024 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/oidc.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/oidc.auth.strategy.ts @@ -9,11 +9,6 @@ import { StrategyVerifyCallbackReq, } from 'openid-client'; -import { - AuthException, - AuthExceptionCode, -} from 'src/engine/core-modules/auth/auth.exception'; - @Injectable() export class OIDCAuthStrategy extends PassportStrategy( Strategy, @@ -47,7 +42,7 @@ export class OIDCAuthStrategy extends PassportStrategy( validate: StrategyVerifyCallbackReq<{ identityProviderId: string; user: { - email: string; + email?: string; firstName?: string | null; lastName?: string | null; }; @@ -66,12 +61,6 @@ export class OIDCAuthStrategy extends PassportStrategy( const userinfo = await this.client.userinfo(tokenset); - if (!userinfo || !userinfo.email) { - return done( - new AuthException('Email not found', AuthExceptionCode.INVALID_DATA), - ); - } - const user = { email: userinfo.email, ...(userinfo.given_name ? { firstName: userinfo.given_name } : {}), diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts index f95789a32c79..b70bddad15f9 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts @@ -14,11 +14,11 @@ import { StripeBillingPortalService } from 'src/engine/core-modules/billing/stri import { StripeCheckoutService } from 'src/engine/core-modules/billing/stripe/services/stripe-checkout.service'; import { BillingGetPricesPerPlanResult } from 'src/engine/core-modules/billing/types/billing-get-prices-per-plan-result.type'; import { BillingPortalCheckoutSessionParameters } from 'src/engine/core-modules/billing/types/billing-portal-checkout-session-parameters.type'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { assert } from 'src/utils/assert'; @Injectable() diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service.ts index f899d8457db2..f0f352137f9f 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service.ts @@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'; import Stripe from 'stripe'; import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; @Injectable() diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.spec.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.spec.ts index a1f9169c2368..fdca070c8797 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { ClientConfigResolver } from './client-config.resolver'; diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts index 04a25f28677f..e46ab98abccc 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts @@ -1,6 +1,6 @@ import { Query, Resolver } from '@nestjs/graphql'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const'; diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.module.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.module.ts index eae5ed641b5a..4273a58f978d 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.module.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @Module({ diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/service/domain-manager.service.spec.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.spec.ts similarity index 98% rename from packages/twenty-server/src/engine/core-modules/domain-manager/service/domain-manager.service.spec.ts rename to packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.spec.ts index 7225c5445be2..e8c8b167c770 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/service/domain-manager.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.spec.ts @@ -125,6 +125,7 @@ describe('DomainManagerService', () => { }); const result = domainManagerService.buildWorkspaceURL({ + subdomain: 'subdomain', pathname: '/path/to/resource', }); @@ -144,6 +145,8 @@ describe('DomainManagerService', () => { }); const result = domainManagerService.buildWorkspaceURL({ + subdomain: 'subdomain', + searchParams: { foo: 'bar', baz: 123, diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/service/domain-manager.service.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts similarity index 87% rename from packages/twenty-server/src/engine/core-modules/domain-manager/service/domain-manager.service.ts rename to packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts index 73b95ac42657..54595e56774d 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/service/domain-manager.service.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts @@ -62,14 +62,14 @@ export class DomainManagerService { buildEmailVerificationURL({ emailVerificationToken, email, - workspaceSubdomain, + subdomain, }: { emailVerificationToken: string; email: string; - workspaceSubdomain?: string; + subdomain: string; }) { return this.buildWorkspaceURL({ - subdomain: workspaceSubdomain, + subdomain, pathname: 'verify-email', searchParams: { emailVerificationToken, email }, }); @@ -80,28 +80,14 @@ export class DomainManagerService { pathname, searchParams, }: { - subdomain?: string; + subdomain: string; pathname?: string; searchParams?: Record; }) { - const url = this.getBaseUrl(); + const url = this.getFrontUrl(); - if ( - this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') && - !subdomain - ) { - throw new Error('subdomain is required when multiworkspace is enable'); - } - - if ( - subdomain && - subdomain.length > 0 && - this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') - ) { - url.hostname = url.hostname.replace( - this.environmentService.get('DEFAULT_SUBDOMAIN'), - subdomain, - ); + if (this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) { + url.hostname = `${subdomain}.${url.hostname}`; } if (pathname) { @@ -119,18 +105,18 @@ export class DomainManagerService { return url; } - getWorkspaceSubdomainByOrigin = (origin: string) => { - const { hostname: originHostname } = new URL(origin); + getWorkspaceSubdomainFromUrl = (url: string) => { + const { hostname: originHostname } = new URL(url); + + if (!originHostname.endsWith(this.getFrontUrl().hostname)) { + return null; + } const frontDomain = this.getFrontUrl().hostname; const subdomain = originHostname.replace(`.${frontDomain}`, ''); - if (this.isDefaultSubdomain(subdomain)) { - return; - } - - return subdomain; + return this.isDefaultSubdomain(subdomain) ? null : subdomain; }; async getWorkspaceBySubdomainOrDefaultWorkspace(subdomain?: string) { @@ -145,16 +131,9 @@ export class DomainManagerService { return subdomain === this.environmentService.get('DEFAULT_SUBDOMAIN'); } - computeRedirectErrorUrl( - errorMessage: string, - { - subdomain, - }: { - subdomain?: string; - }, - ) { + computeRedirectErrorUrl(errorMessage: string, subdomain: string) { const url = this.buildWorkspaceURL({ - subdomain: subdomain ?? this.environmentService.get('DEFAULT_SUBDOMAIN'), + subdomain: subdomain, pathname: '/verify', searchParams: { errorMessage }, }); @@ -206,7 +185,7 @@ export class DomainManagerService { return this.getDefaultWorkspace(); } - const subdomain = this.getWorkspaceSubdomainByOrigin(origin); + const subdomain = this.getWorkspaceSubdomainFromUrl(origin); if (!isDefined(subdomain)) return; diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts index f9d80f3399fc..1a22a62e4919 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts @@ -1,6 +1,6 @@ import { Args, Mutation, Resolver } from '@nestjs/graphql'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { ResendEmailVerificationTokenInput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.input'; import { ResendEmailVerificationTokenOutput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.output'; import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service'; diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts b/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts index e7e90010962c..1d31398bc3b8 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts @@ -12,7 +12,7 @@ import { AppTokenType, } from 'src/engine/core-modules/app-token/app-token.entity'; import { EmailVerificationTokenService } from 'src/engine/core-modules/auth/token/services/email-verification-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EmailVerificationException, EmailVerificationExceptionCode, @@ -37,7 +37,7 @@ export class EmailVerificationService { async sendVerificationEmail( userId: string, email: string, - workspaceSubdomain?: string, + subdomain: string, ) { if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) { return { success: false }; @@ -50,7 +50,7 @@ export class EmailVerificationService { this.domainManagerService.buildEmailVerificationURL({ emailVerificationToken, email, - workspaceSubdomain, + subdomain, }); const emailData = { @@ -80,10 +80,7 @@ export class EmailVerificationService { return { success: true }; } - async resendEmailVerificationToken( - email: string, - workspaceSubdomain?: string, - ) { + async resendEmailVerificationToken(email: string, subdomain: string) { if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) { throw new EmailVerificationException( 'Email verification token cannot be sent because email verification is not required', @@ -124,7 +121,7 @@ export class EmailVerificationService { await this.appTokenRepository.delete(existingToken.id); } - await this.sendVerificationEmail(user.id, email, workspaceSubdomain); + await this.sendVerificationEmail(user.id, email, subdomain); return { success: true }; } diff --git a/packages/twenty-server/src/engine/core-modules/guard-redirect/guard-redirect.module.ts b/packages/twenty-server/src/engine/core-modules/guard-redirect/guard-redirect.module.ts new file mode 100644 index 000000000000..30773322bbbc --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/guard-redirect/guard-redirect.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; + +import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; + +@Module({ + imports: [DomainManagerModule], + providers: [GuardRedirectService], + exports: [GuardRedirectService], +}) +export class GuardRedirectModule {} diff --git a/packages/twenty-server/src/engine/core-modules/guard-redirect/services/guard-redirect.service.ts b/packages/twenty-server/src/engine/core-modules/guard-redirect/services/guard-redirect.service.ts new file mode 100644 index 000000000000..9cb33b26f096 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/guard-redirect/services/guard-redirect.service.ts @@ -0,0 +1,38 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; + +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; + +@Injectable() +export class GuardRedirectService { + constructor( + private readonly domainManagerService: DomainManagerService, + private readonly environmentService: EnvironmentService, + ) {} + + dispatchErrorFromGuard(context: any, err: any, subdomain: string) { + if ('contextType' in context && context.contextType === 'graphql') { + throw err; + } + + context + .switchToHttp() + .getResponse() + .redirect( + this.domainManagerService + .computeRedirectErrorUrl(err.message ?? 'Unknown error', subdomain) + .toString(), + ); + } + + getSubdomainFromContext(context: ExecutionContext) { + const request = context.switchToHttp().getRequest(); + + const subdomainFromUrl = + this.domainManagerService.getWorkspaceSubdomainFromUrl( + request.headers.referer, + ); + + return subdomainFromUrl ?? this.environmentService.get('DEFAULT_SUBDOMAIN'); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts index fcfb35cb3888..3719bb6ee551 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts @@ -129,13 +129,10 @@ export class SSOService { }; } - async findSSOIdentityProviderById(identityProviderId?: string) { - // if identityProviderId is not provide, typeorm return a random idp instead of undefined - if (!identityProviderId) return undefined; - + async findSSOIdentityProviderById(identityProviderId: string) { return (await this.workspaceSSOIdentityProviderRepository.findOne({ where: { id: identityProviderId }, - })) as (SSOConfiguration & WorkspaceSSOIdentityProvider) | undefined; + })) as (SSOConfiguration & WorkspaceSSOIdentityProvider) | null; } buildCallbackUrl( diff --git a/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts b/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts index d51a566d3a89..24c3c80cb3e0 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts @@ -11,6 +11,8 @@ import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; import { SSOResolver } from 'src/engine/core-modules/sso/sso.resolver'; import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; +import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module'; @Module({ imports: [ @@ -19,6 +21,8 @@ import { User } from 'src/engine/core-modules/user/user.entity'; 'core', ), BillingModule, + DomainManagerModule, + GuardRedirectModule, ], exports: [SSOService], providers: [SSOService, SSOResolver], diff --git a/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts b/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts index 0754a9939d3a..bdf5b08892bd 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts @@ -3,7 +3,7 @@ import { UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { SSOProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/sso-provider-enabled.guard'; +import { EnterpriseFeaturesEnabledGuard } from 'src/engine/core-modules/auth/guards/enterprise-features-enabled.guard'; import { DeleteSsoInput } from 'src/engine/core-modules/sso/dtos/delete-sso.input'; import { DeleteSsoOutput } from 'src/engine/core-modules/sso/dtos/delete-sso.output'; import { EditSsoInput } from 'src/engine/core-modules/sso/dtos/edit-sso.input'; @@ -26,7 +26,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; export class SSOResolver { constructor(private readonly sSOService: SSOService) {} - @UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard) + @UseGuards(WorkspaceAuthGuard, EnterpriseFeaturesEnabledGuard) @Mutation(() => SetupSsoOutput) async createOIDCIdentityProvider( @Args('input') setupSsoInput: SetupOIDCSsoInput, @@ -38,7 +38,7 @@ export class SSOResolver { ); } - @UseGuards(SSOProviderEnabledGuard) + @UseGuards(EnterpriseFeaturesEnabledGuard) @Query(() => [FindAvailableSSOIDPOutput]) async listSSOIdentityProvidersByWorkspaceId( @AuthWorkspace() { id: workspaceId }: Workspace, @@ -53,7 +53,7 @@ export class SSOResolver { return this.sSOService.getAuthorizationUrl(identityProviderId); } - @UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard) + @UseGuards(WorkspaceAuthGuard, EnterpriseFeaturesEnabledGuard) @Mutation(() => SetupSsoOutput) async createSAMLIdentityProvider( @Args('input') setupSsoInput: SetupSAMLSsoInput, @@ -65,7 +65,7 @@ export class SSOResolver { ); } - @UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard) + @UseGuards(WorkspaceAuthGuard, EnterpriseFeaturesEnabledGuard) @Mutation(() => DeleteSsoOutput) async deleteSSOIdentityProvider( @Args('input') { identityProviderId }: DeleteSsoInput, @@ -77,7 +77,7 @@ export class SSOResolver { ); } - @UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard) + @UseGuards(WorkspaceAuthGuard, EnterpriseFeaturesEnabledGuard) @Mutation(() => EditSsoOutput) async editSSOIdentityProvider( @Args('input') input: EditSsoInput, diff --git a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts index beee4de0ffeb..2d5bb9d0036b 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts @@ -45,7 +45,7 @@ import { } from 'src/engine/core-modules/auth/auth.exception'; import { userValidator } from 'src/engine/core-modules/user/user.validate'; import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; const getHMACKey = (email?: string, key?: string | null) => { diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.spec.ts index 100c77ca6a12..7c176c618ff2 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.spec.ts @@ -14,7 +14,7 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works import { User } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceInvitationException } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.exception'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; import { WorkspaceInvitationService } from './workspace-invitation.service'; diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts index 34d0d62501b0..2c25a04c4ca4 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts @@ -17,7 +17,7 @@ import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts index 18e0f3a109c0..2e5020e55002 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts @@ -3,7 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index db28abe7688f..6fae46dab65c 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -16,7 +16,7 @@ import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder. import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; diff --git a/packages/twenty-server/src/filters/unhandled-exception.filter.ts b/packages/twenty-server/src/filters/unhandled-exception.filter.ts index 9d6fe1f0919a..a84ccfa21e19 100644 --- a/packages/twenty-server/src/filters/unhandled-exception.filter.ts +++ b/packages/twenty-server/src/filters/unhandled-exception.filter.ts @@ -16,7 +16,7 @@ export class UnhandledExceptionFilter implements ExceptionFilter { const ctx = host.switchToHttp(); const response = ctx.getResponse(); - if (!response.header) { + if (!response.header || response.headersSent) { return; }