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 f5f4f55fd..04fda4d41 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 @@ -25,6 +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 { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; @Controller('auth/google-apis') @UseFilters(AuthRestApiExceptionFilter) @@ -35,6 +36,7 @@ export class GoogleAPIsAuthController { private readonly environmentService: EnvironmentService, private readonly onboardingService: OnboardingService, private readonly domainManagerService: DomainManagerService, + private readonly guardRedirectService: GuardRedirectService, @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, ) {} @@ -52,75 +54,89 @@ export class GoogleAPIsAuthController { @Req() req: GoogleAPIsRequest, @Res() res: Response, ) { - const { user } = req; + let workspace: Workspace | null = null; - const { - emails, - accessToken, - refreshToken, - transientToken, - redirectLocation, - calendarVisibility, - messageVisibility, - } = user; + try { + const { user } = req; - const { workspaceMemberId, userId, workspaceId } = - await this.transientTokenService.verifyTransientToken(transientToken); + const { + emails, + accessToken, + refreshToken, + transientToken, + redirectLocation, + calendarVisibility, + messageVisibility, + } = user; - const demoWorkspaceIds = this.environmentService.get('DEMO_WORKSPACE_IDS'); + const { workspaceMemberId, userId, workspaceId } = + await this.transientTokenService.verifyTransientToken(transientToken); - if (demoWorkspaceIds.includes(workspaceId)) { - throw new AuthException( - 'Cannot connect Google account to demo workspace', - AuthExceptionCode.FORBIDDEN_EXCEPTION, - ); - } + const demoWorkspaceIds = + this.environmentService.get('DEMO_WORKSPACE_IDS'); - if (!workspaceId) { - throw new AuthException( - 'Workspace not found', - AuthExceptionCode.WORKSPACE_NOT_FOUND, - ); - } + if (demoWorkspaceIds.includes(workspaceId)) { + throw new AuthException( + 'Cannot connect Google account to demo workspace', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } - const handle = emails[0].value; + if (!workspaceId) { + throw new AuthException( + 'Workspace not found', + AuthExceptionCode.WORKSPACE_NOT_FOUND, + ); + } - await this.googleAPIsService.refreshGoogleRefreshToken({ - handle, - workspaceMemberId: workspaceMemberId, - workspaceId: workspaceId, - accessToken, - refreshToken, - calendarVisibility, - messageVisibility, - }); - - if (userId) { - await this.onboardingService.setOnboardingConnectAccountPending({ - userId, - workspaceId, - value: false, + workspace = await this.workspaceRepository.findOneBy({ + id: workspaceId, }); - } - const workspace = await this.workspaceRepository.findOneBy({ - id: workspaceId, - }); + const handle = emails[0].value; - if (!workspace) { - throw new AuthException( - 'Workspace not found', - AuthExceptionCode.WORKSPACE_NOT_FOUND, + await this.googleAPIsService.refreshGoogleRefreshToken({ + handle, + workspaceMemberId: workspaceMemberId, + workspaceId: workspaceId, + accessToken, + refreshToken, + calendarVisibility, + messageVisibility, + }); + + if (userId) { + await this.onboardingService.setOnboardingConnectAccountPending({ + userId, + workspaceId, + value: false, + }); + } + + if (!workspace) { + throw new AuthException( + 'Workspace not found', + AuthExceptionCode.WORKSPACE_NOT_FOUND, + ); + } + + return res.redirect( + this.domainManagerService + .buildWorkspaceURL({ + subdomain: workspace.subdomain, + pathname: redirectLocation || '/settings/accounts', + }) + .toString(), + ); + } catch (err) { + return res.redirect( + this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions( + err, + workspace ?? { + subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), + }, + ), ); } - - return res.redirect( - this.domainManagerService - .buildWorkspaceURL({ - subdomain: workspace.subdomain, - pathname: redirectLocation || '/settings/accounts', - }) - .toString(), - ); } } 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 f9d0052d2..530fd5533 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 @@ -11,10 +11,6 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Response } from 'express'; import { Repository } from 'typeorm'; -import { - AuthException, - AuthExceptionCode, -} from 'src/engine/core-modules/auth/auth.exception'; import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter'; import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; import { GoogleOauthGuard } from 'src/engine/core-modules/auth/guards/google-oauth.guard'; @@ -22,9 +18,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/services/domain-manager.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; @Controller('auth/google') @UseFilters(AuthRestApiExceptionFilter) @@ -32,8 +28,8 @@ export class GoogleAuthController { constructor( private readonly loginTokenService: LoginTokenService, private readonly authService: AuthService, - private readonly domainManagerService: DomainManagerService, private readonly environmentService: EnvironmentService, + private readonly guardRedirectService: GuardRedirectService, @InjectRepository(User, 'core') private readonly userRepository: Repository, ) {} @@ -120,16 +116,14 @@ export class GoogleAuthController { }), ); } catch (err) { - if (err instanceof AuthException) { - return res.redirect( - this.domainManagerService.computeRedirectErrorUrl( - err.message, - currentWorkspace?.subdomain ?? - this.environmentService.get('DEFAULT_SUBDOMAIN'), - ), - ); - } - throw new AuthException(err, AuthExceptionCode.INTERNAL_SERVER_ERROR); + return res.redirect( + this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions( + err, + currentWorkspace ?? { + subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), + }, + ), + ); } } } 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 99a893de2..af7dcef51 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 @@ -24,8 +24,8 @@ import { MicrosoftAPIsRequest } from 'src/engine/core-modules/auth/types/microso import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; -import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; @Controller('auth/microsoft-apis') @UseFilters(AuthRestApiExceptionFilter) @@ -34,9 +34,9 @@ export class MicrosoftAPIsAuthController { private readonly microsoftAPIsService: MicrosoftAPIsService, private readonly transientTokenService: TransientTokenService, private readonly environmentService: EnvironmentService, - private readonly workspaceService: WorkspaceService, private readonly domainManagerService: DomainManagerService, private readonly onboardingService: OnboardingService, + private readonly guardRedirectService: GuardRedirectService, @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, ) {} @@ -54,80 +54,96 @@ export class MicrosoftAPIsAuthController { @Req() req: MicrosoftAPIsRequest, @Res() res: Response, ) { - const { user } = req; + let workspace: Workspace | null = null; - const { - emails, - accessToken, - refreshToken, - transientToken, - redirectLocation, - calendarVisibility, - messageVisibility, - } = user; + try { + const { user } = req; - const { workspaceMemberId, userId, workspaceId } = - await this.transientTokenService.verifyTransientToken(transientToken); + const { + emails, + accessToken, + refreshToken, + transientToken, + redirectLocation, + calendarVisibility, + messageVisibility, + } = user; - const demoWorkspaceIds = this.environmentService.get('DEMO_WORKSPACE_IDS'); + const { workspaceMemberId, userId, workspaceId } = + await this.transientTokenService.verifyTransientToken(transientToken); - if (demoWorkspaceIds.includes(workspaceId)) { - throw new AuthException( - 'Cannot connect Microsoft account to demo workspace', - AuthExceptionCode.FORBIDDEN_EXCEPTION, - ); - } - if (!workspaceId) { - throw new AuthException( - 'Workspace not found', - AuthExceptionCode.WORKSPACE_NOT_FOUND, - ); - } + const demoWorkspaceIds = + this.environmentService.get('DEMO_WORKSPACE_IDS'); - if (emails.length === 0) { - throw new AuthException( - 'No email - Ask your Azure Entra Admin to add you one on top of your User Principal Name', - AuthExceptionCode.USER_NOT_FOUND, - ); - } - const handle = emails[0].value; + if (demoWorkspaceIds.includes(workspaceId)) { + throw new AuthException( + 'Cannot connect Microsoft account to demo workspace', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } - await this.microsoftAPIsService.refreshMicrosoftRefreshToken({ - handle, - workspaceMemberId: workspaceMemberId, - workspaceId: workspaceId, - accessToken, - refreshToken, - calendarVisibility, - messageVisibility, - }); + if (!workspaceId) { + throw new AuthException( + 'Workspace not found', + AuthExceptionCode.WORKSPACE_NOT_FOUND, + ); + } - if (userId) { - await this.onboardingService.setOnboardingConnectAccountPending({ - userId, - workspaceId, - value: false, + workspace = await this.workspaceRepository.findOneBy({ + id: workspaceId, }); - } - const workspace = await this.workspaceRepository.findOneBy({ - id: workspaceId, - }); + if (emails.length === 0) { + throw new AuthException( + 'No email - Ask your Azure Entra Admin to add you one on top of your User Principal Name', + AuthExceptionCode.USER_NOT_FOUND, + ); + } - if (!workspace) { - throw new AuthException( - 'Workspace not found', - AuthExceptionCode.WORKSPACE_NOT_FOUND, + const handle = emails[0].value; + + await this.microsoftAPIsService.refreshMicrosoftRefreshToken({ + handle, + workspaceMemberId: workspaceMemberId, + workspaceId: workspaceId, + accessToken, + refreshToken, + calendarVisibility, + messageVisibility, + }); + + if (userId) { + await this.onboardingService.setOnboardingConnectAccountPending({ + userId, + workspaceId, + value: false, + }); + } + + if (!workspace) { + throw new AuthException( + 'Workspace not found', + AuthExceptionCode.WORKSPACE_NOT_FOUND, + ); + } + + return res.redirect( + this.domainManagerService + .buildWorkspaceURL({ + subdomain: workspace.subdomain, + pathname: redirectLocation || '/settings/accounts', + }) + .toString(), + ); + } catch (err) { + return res.redirect( + this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions( + err, + workspace ?? { + subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), + }, + ), ); } - - return res.redirect( - this.domainManagerService - .buildWorkspaceURL({ - subdomain: workspace.subdomain, - pathname: redirectLocation || '/settings/accounts', - }) - .toString(), - ); } } 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 e90937eda..7f172d04e 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 @@ -11,16 +11,15 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Response } from 'express'; import { Repository } from 'typeorm'; -import { AuthException } from 'src/engine/core-modules/auth/auth.exception'; import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; import { MicrosoftOAuthGuard } from 'src/engine/core-modules/auth/guards/microsoft-oauth.guard'; import { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard'; 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/services/domain-manager.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; @Controller('auth/microsoft') @UseFilters(AuthRestApiExceptionFilter) @@ -28,7 +27,7 @@ export class MicrosoftAuthController { constructor( private readonly loginTokenService: LoginTokenService, private readonly authService: AuthService, - private readonly domainManagerService: DomainManagerService, + private readonly guardRedirectService: GuardRedirectService, private readonly environmentService: EnvironmentService, @InjectRepository(User, 'core') private readonly userRepository: Repository, @@ -119,16 +118,14 @@ export class MicrosoftAuthController { }), ); } catch (err) { - if (err instanceof AuthException) { - return res.redirect( - this.domainManagerService.computeRedirectErrorUrl( - err.message, - currentWorkspace?.subdomain ?? - this.environmentService.get('DEFAULT_SUBDOMAIN'), - ), - ); - } - throw err; + return res.redirect( + this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions( + err, + currentWorkspace ?? { + subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), + }, + ), + ); } } } 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 a482a8675..67ade3f87 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 @@ -30,17 +30,17 @@ import { IdentityProviderType, WorkspaceSSOIdentityProvider, } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; -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'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; @Controller('auth') export class SSOAuthController { constructor( private readonly loginTokenService: LoginTokenService, private readonly authService: AuthService, - private readonly domainManagerService: DomainManagerService, + private readonly guardRedirectService: GuardRedirectService, private readonly environmentService: EnvironmentService, private readonly sSOService: SSOService, @InjectRepository(User, 'core') @@ -137,10 +137,11 @@ export class SSOAuthController { ); } catch (err) { return res.redirect( - this.domainManagerService.computeRedirectErrorUrl( - err.message, - workspaceIdentityProvider?.workspace.subdomain ?? - this.environmentService.get('DEFAULT_SUBDOMAIN'), + this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions( + err, + workspaceIdentityProvider?.workspace ?? { + subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), + }, ), ); } 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 index 4f9d664ce..9a1eb5cb8 100644 --- 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 @@ -27,11 +27,9 @@ export class EnterpriseFeaturesEnabledGuard implements CanActivate { return true; } catch (err) { - this.guardRedirectService.dispatchErrorFromGuard( - context, - err, - this.guardRedirectService.getSubdomainFromContext(context), - ); + this.guardRedirectService.dispatchErrorFromGuard(context, err, { + subdomain: this.guardRedirectService.getSubdomainFromContext(context), + }); return false; } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts index ea984e8eb..963c9a9a8 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts @@ -8,41 +8,53 @@ import { import { GoogleAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy'; import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util'; 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 GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard( 'google-apis', ) { - constructor(private readonly environmentService: EnvironmentService) { + constructor( + private readonly guardRedirectService: GuardRedirectService, + private readonly environmentService: EnvironmentService, + ) { super(); } async canActivate(context: ExecutionContext) { - const request = context.switchToHttp().getRequest(); - const state = JSON.parse(request.query.state); + try { + const request = context.switchToHttp().getRequest(); + const state = JSON.parse(request.query.state); - if ( - !this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') && - !this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED') - ) { - throw new AuthException( - 'Google apis auth is not enabled', - AuthExceptionCode.GOOGLE_API_AUTH_DISABLED, + if ( + !this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') && + !this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED') + ) { + throw new AuthException( + 'Google apis auth is not enabled', + AuthExceptionCode.GOOGLE_API_AUTH_DISABLED, + ); + } + + new GoogleAPIsOauthExchangeCodeForTokenStrategy( + this.environmentService, + {}, ); + + setRequestExtraParams(request, { + transientToken: state.transientToken, + redirectLocation: state.redirectLocation, + calendarVisibility: state.calendarVisibility, + messageVisibility: state.messageVisibility, + }); + + return (await super.canActivate(context)) as boolean; + } catch (err) { + this.guardRedirectService.dispatchErrorFromGuard(context, err, { + subdomain: this.guardRedirectService.getSubdomainFromContext(context), + }); + + return false; } - - new GoogleAPIsOauthExchangeCodeForTokenStrategy( - this.environmentService, - {}, - ); - - setRequestExtraParams(request, { - transientToken: state.transientToken, - redirectLocation: state.redirectLocation, - calendarVisibility: state.calendarVisibility, - messageVisibility: state.messageVisibility, - }); - - return (await super.canActivate(context)) as boolean; } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts index 3a52e0037..25e396736 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts @@ -1,5 +1,8 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; import { AuthException, @@ -9,14 +12,17 @@ import { GoogleAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service'; import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @Injectable() export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') { constructor( private readonly environmentService: EnvironmentService, - private readonly featureFlagService: FeatureFlagService, private readonly transientTokenService: TransientTokenService, + private readonly guardRedirectService: GuardRedirectService, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, ) { super({ prompt: 'select_account', @@ -24,37 +30,53 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') { } async canActivate(context: ExecutionContext) { - const request = context.switchToHttp().getRequest(); + let workspace: Workspace | null = null; - const { workspaceId, userId } = - await this.transientTokenService.verifyTransientToken( - request.query.transientToken, + try { + const request = context.switchToHttp().getRequest(); + + const { workspaceId, userId } = + await this.transientTokenService.verifyTransientToken( + request.query.transientToken, + ); + + workspace = await this.workspaceRepository.findOneBy({ + id: workspaceId, + }); + + setRequestExtraParams(request, { + transientToken: request.query.transientToken, + redirectLocation: request.query.redirectLocation, + calendarVisibility: request.query.calendarVisibility, + messageVisibility: request.query.messageVisibility, + loginHint: request.query.loginHint, + userId: userId, + workspaceId: workspaceId, + }); + + if ( + !this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') && + !this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED') + ) { + throw new AuthException( + 'Google apis auth is not enabled', + AuthExceptionCode.GOOGLE_API_AUTH_DISABLED, + ); + } + + new GoogleAPIsOauthRequestCodeStrategy(this.environmentService, {}); + + return (await super.canActivate(context)) as boolean; + } catch (err) { + this.guardRedirectService.dispatchErrorFromGuard( + context, + err, + workspace ?? { + subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), + }, ); - setRequestExtraParams(request, { - transientToken: request.query.transientToken, - redirectLocation: request.query.redirectLocation, - calendarVisibility: request.query.calendarVisibility, - messageVisibility: request.query.messageVisibility, - loginHint: request.query.loginHint, - userId: userId, - workspaceId: workspaceId, - }); - - if ( - !this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') && - !this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED') - ) { - throw new AuthException( - 'Google apis auth is not enabled', - AuthExceptionCode.GOOGLE_API_AUTH_DISABLED, - ); + return false; } - - new GoogleAPIsOauthRequestCodeStrategy(this.environmentService, {}); - - const activate = (await super.canActivate(context)) as boolean; - - return activate; } } 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 e45a5ed13..e53c50194 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 @@ -75,8 +75,9 @@ export class GoogleOauthGuard extends AuthGuard('google') { this.guardRedirectService.dispatchErrorFromGuard( context, err, - workspace?.subdomain ?? - this.environmentService.get('DEFAULT_SUBDOMAIN'), + 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 8cbdde7c6..7ca9fc98f 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 @@ -28,11 +28,9 @@ export class GoogleProviderEnabledGuard implements CanActivate { return true; } catch (err) { - this.guardRedirectService.dispatchErrorFromGuard( - context, - err, - this.guardRedirectService.getSubdomainFromContext(context), - ); + this.guardRedirectService.dispatchErrorFromGuard(context, err, { + subdomain: this.guardRedirectService.getSubdomainFromContext(context), + }); return false; } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-exchange-code-for-token.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-exchange-code-for-token.guard.ts index de9bce16f..ae6b0968d 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-exchange-code-for-token.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-exchange-code-for-token.guard.ts @@ -8,12 +8,16 @@ import { import { MicrosoftAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-modules/auth/strategies/microsoft-apis-oauth-exchange-code-for-token.auth.strategy'; import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util'; 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 MicrosoftAPIsOauthExchangeCodeForTokenGuard extends AuthGuard( 'microsoft-apis', ) { - constructor(private readonly environmentService: EnvironmentService) { + constructor( + private readonly guardRedirectService: GuardRedirectService, + private readonly environmentService: EnvironmentService, + ) { super(); } @@ -35,12 +39,18 @@ export class MicrosoftAPIsOauthExchangeCodeForTokenGuard extends AuthGuard( return (await super.canActivate(context)) as boolean; } catch (error) { - if (error?.oauthError?.statusCode === 403) { - throw new AuthException( - `Insufficient privileges to access this microsoft resource. Make sure you have the correct scopes or ask your admin to update your scopes. ${error?.message}`, - AuthExceptionCode.INSUFFICIENT_SCOPES, - ); - } + this.guardRedirectService.dispatchErrorFromGuard( + context, + error?.oauthError?.statusCode === 403 + ? new AuthException( + `Insufficient privileges to access this microsoft resource. Make sure you have the correct scopes or ask your admin to update your scopes. ${error?.message}`, + AuthExceptionCode.INSUFFICIENT_SCOPES, + ) + : error, + { + subdomain: this.guardRedirectService.getSubdomainFromContext(context), + }, + ); return false; } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-request-code.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-request-code.guard.ts index 427c8fde6..6c309ef39 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-request-code.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-request-code.guard.ts @@ -1,5 +1,8 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; import { AuthException, @@ -11,6 +14,8 @@ import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google 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 { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; @Injectable() export class MicrosoftAPIsOauthRequestCodeGuard extends AuthGuard( @@ -20,6 +25,9 @@ export class MicrosoftAPIsOauthRequestCodeGuard extends AuthGuard( private readonly environmentService: EnvironmentService, private readonly featureFlagService: FeatureFlagService, private readonly transientTokenService: TransientTokenService, + private readonly guardRedirectService: GuardRedirectService, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, ) { super({ prompt: 'select_account', @@ -27,36 +35,53 @@ export class MicrosoftAPIsOauthRequestCodeGuard extends AuthGuard( } async canActivate(context: ExecutionContext) { - const request = context.switchToHttp().getRequest(); + let workspace: Workspace | null = null; - const { workspaceId } = - await this.transientTokenService.verifyTransientToken( - request.query.transientToken, - ); - const isMicrosoftSyncEnabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IsMicrosoftSyncEnabled, - workspaceId, + try { + const request = context.switchToHttp().getRequest(); + + const { workspaceId } = + await this.transientTokenService.verifyTransientToken( + request.query.transientToken, + ); + + workspace = await this.workspaceRepository.findOneBy({ + id: workspaceId, + }); + + const isMicrosoftSyncEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsMicrosoftSyncEnabled, + workspaceId, + ); + + if (!isMicrosoftSyncEnabled) { + throw new AuthException( + 'Microsoft sync is not enabled', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + + new MicrosoftAPIsOauthRequestCodeStrategy(this.environmentService); + setRequestExtraParams(request, { + transientToken: request.query.transientToken, + redirectLocation: request.query.redirectLocation, + calendarVisibility: request.query.calendarVisibility, + messageVisibility: request.query.messageVisibility, + loginHint: request.query.loginHint, + }); + + return (await super.canActivate(context)) as boolean; + } catch (err) { + this.guardRedirectService.dispatchErrorFromGuard( + context, + err, + workspace ?? { + subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), + }, ); - if (!isMicrosoftSyncEnabled) { - throw new AuthException( - 'Microsoft sync is not enabled', - AuthExceptionCode.FORBIDDEN_EXCEPTION, - ); + return false; } - - new MicrosoftAPIsOauthRequestCodeStrategy(this.environmentService); - setRequestExtraParams(request, { - transientToken: request.query.transientToken, - redirectLocation: request.query.redirectLocation, - calendarVisibility: request.query.calendarVisibility, - messageVisibility: request.query.messageVisibility, - loginHint: request.query.loginHint, - }); - - const activate = (await super.canActivate(context)) as boolean; - - return activate; } } 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 0a4134445..78c7b0083 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 @@ -64,8 +64,9 @@ export class MicrosoftOAuthGuard extends AuthGuard('microsoft') { this.guardRedirectService.dispatchErrorFromGuard( context, err, - workspace?.subdomain ?? - this.environmentService.get('DEFAULT_SUBDOMAIN'), + 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 e35065a04..48a3d549e 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 @@ -28,11 +28,9 @@ export class MicrosoftProviderEnabledGuard implements CanActivate { return true; } catch (err) { - this.guardRedirectService.dispatchErrorFromGuard( - context, - err, - this.guardRedirectService.getSubdomainFromContext(context), - ); + this.guardRedirectService.dispatchErrorFromGuard(context, err, { + subdomain: 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 3108476d8..69a1dd59a 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 @@ -77,8 +77,9 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') { this.guardRedirectService.dispatchErrorFromGuard( context, err, - identityProvider?.workspace.subdomain ?? - this.environmentService.get('DEFAULT_SUBDOMAIN'), + identityProvider?.workspace ?? { + subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), + }, ); 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 8a428e3b5..b5aaec503 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 @@ -49,8 +49,9 @@ export class SAMLAuthGuard extends AuthGuard('saml') { this.guardRedirectService.dispatchErrorFromGuard( context, err, - identityProvider?.workspace.subdomain ?? - this.environmentService.get('DEFAULT_SUBDOMAIN'), + identityProvider?.workspace ?? { + subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), + }, ); return false; diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/social-sso.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/social-sso.spec.ts new file mode 100644 index 000000000..b03074726 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/services/social-sso.spec.ts @@ -0,0 +1,114 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { SocialSsoService } from 'src/engine/core-modules/auth/services/social-sso.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +describe('SocialSsoService', () => { + let socialSsoService: SocialSsoService; + let workspaceRepository: Repository; + let environmentService: EnvironmentService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SocialSsoService, + { + provide: getRepositoryToken(Workspace, 'core'), + useClass: Repository, + }, + { + provide: EnvironmentService, + useValue: { + get: jest.fn(), + }, + }, + ], + }).compile(); + + socialSsoService = module.get(SocialSsoService); + workspaceRepository = module.get>( + getRepositoryToken(Workspace, 'core'), + ); + environmentService = module.get(EnvironmentService); + }); + + describe('findWorkspaceFromWorkspaceIdOrAuthProvider', () => { + it('should return a workspace by workspaceId', async () => { + const workspaceId = 'workspace-id-123'; + const mockWorkspace = { id: workspaceId } as Workspace; + + jest + .spyOn(workspaceRepository, 'findOneBy') + .mockResolvedValue(mockWorkspace); + + const result = + await socialSsoService.findWorkspaceFromWorkspaceIdOrAuthProvider( + { authProvider: 'google', email: 'test@example.com' }, + workspaceId, + ); + + expect(result).toEqual(mockWorkspace); + expect(workspaceRepository.findOneBy).toHaveBeenCalledWith({ + id: workspaceId, + }); + }); + + it('should return a workspace from authProvider and email when multi-workspace mode is enabled', async () => { + const authProvider = 'google'; + const email = 'test@example.com'; + const mockWorkspace = { id: 'workspace-id-456' } as Workspace; + + jest.spyOn(environmentService, 'get').mockReturnValue(true); + jest + .spyOn(workspaceRepository, 'findOne') + .mockResolvedValue(mockWorkspace); + + const result = + await socialSsoService.findWorkspaceFromWorkspaceIdOrAuthProvider({ + authProvider, + email, + }); + + expect(result).toEqual(mockWorkspace); + expect(workspaceRepository.findOne).toHaveBeenCalledWith({ + where: { + isGoogleAuthEnabled: true, + workspaceUsers: { + user: { + email, + }, + }, + }, + relations: ['workspaceUsers', 'workspaceUsers.user'], + }); + }); + + it('should return undefined if no workspace is found when multi-workspace mode is enabled', async () => { + jest.spyOn(environmentService, 'get').mockReturnValue(true); + jest.spyOn(workspaceRepository, 'findOne').mockResolvedValue(null); + + const result = + await socialSsoService.findWorkspaceFromWorkspaceIdOrAuthProvider({ + authProvider: 'google', + email: 'notfound@example.com', + }); + + expect(result).toBeUndefined(); + }); + + it('should throw an error for an invalid authProvider', async () => { + jest.spyOn(environmentService, 'get').mockReturnValue(true); + + await expect( + socialSsoService.findWorkspaceFromWorkspaceIdOrAuthProvider({ + authProvider: 'invalid-provider' as any, + email: 'test@example.com', + }), + ).rejects.toThrow('invalid-provider is not a valid auth provider.'); + }); + }); +}); 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 index 9cb33b26f..94e9f2564 100644 --- 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 @@ -2,27 +2,31 @@ 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'; +import { CustomException } from 'src/utils/custom-exception'; +import { AuthException } from 'src/engine/core-modules/auth/auth.exception'; +import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; @Injectable() export class GuardRedirectService { constructor( private readonly domainManagerService: DomainManagerService, private readonly environmentService: EnvironmentService, + private readonly exceptionHandlerService: ExceptionHandlerService, ) {} - dispatchErrorFromGuard(context: any, err: any, subdomain: string) { + dispatchErrorFromGuard( + context: ExecutionContext, + error: Error | CustomException, + workspace: { id?: string; subdomain: string }, + ) { if ('contextType' in context && context.contextType === 'graphql') { - throw err; + throw error; } context .switchToHttp() .getResponse() - .redirect( - this.domainManagerService - .computeRedirectErrorUrl(err.message ?? 'Unknown error', subdomain) - .toString(), - ); + .redirect(this.getRedirectErrorUrlAndCaptureExceptions(error, workspace)); } getSubdomainFromContext(context: ExecutionContext) { @@ -35,4 +39,26 @@ export class GuardRedirectService { return subdomainFromUrl ?? this.environmentService.get('DEFAULT_SUBDOMAIN'); } + + private captureException(err: Error | CustomException, workspaceId?: string) { + if (err instanceof AuthException) return; + + this.exceptionHandlerService.captureExceptions([err], { + workspace: { + id: workspaceId, + }, + }); + } + + getRedirectErrorUrlAndCaptureExceptions( + err: Error | CustomException, + workspace: { id?: string; subdomain: string }, + ) { + this.captureException(err, workspace.id); + + return this.domainManagerService.computeRedirectErrorUrl( + err instanceof AuthException ? err.message : 'Unknown error', + workspace.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 b72ab4f21..4d3185210 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 @@ -23,6 +23,7 @@ import { OIDCResponseType, WorkspaceSSOIdentityProvider, } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; +import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; @Injectable() export class SSOService { @@ -32,6 +33,7 @@ export class SSOService { private readonly workspaceSSOIdentityProviderRepository: Repository, private readonly environmentService: EnvironmentService, private readonly billingService: BillingService, + private readonly exceptionHandlerService: ExceptionHandlerService, ) {} private async isSSOEnabled(workspaceId: string) { @@ -48,6 +50,17 @@ export class SSOService { } } + private async getIssuerForOIDC(issuerUrl: string) { + try { + return await Issuer.discover(issuerUrl); + } catch (err) { + throw new SSOException( + 'Invalid issuer', + SSOExceptionCode.INVALID_ISSUER_URL, + ); + } + } + async createOIDCIdentityProvider( data: Pick< WorkspaceSSOIdentityProvider, @@ -58,21 +71,7 @@ export class SSOService { try { await this.isSSOEnabled(workspaceId); - if (!data.issuer) { - throw new SSOException( - 'Invalid issuer URL', - SSOExceptionCode.INVALID_ISSUER_URL, - ); - } - - const issuer = await Issuer.discover(data.issuer); - - if (!issuer.metadata.issuer) { - throw new SSOException( - 'Invalid issuer URL from discovery', - SSOExceptionCode.INVALID_ISSUER_URL, - ); - } + const issuer = await this.getIssuerForOIDC(data.issuer); const identityProvider = await this.workspaceSSOIdentityProviderRepository.save({ @@ -96,6 +95,8 @@ export class SSOService { return err; } + this.exceptionHandlerService.captureExceptions([err]); + return new SSOException( 'Unknown SSO configuration error', SSOExceptionCode.UNKNOWN_SSO_CONFIGURATION_ERROR, @@ -131,6 +132,7 @@ export class SSOService { async findSSOIdentityProviderById(identityProviderId: string) { return (await this.workspaceSSOIdentityProviderRepository.findOne({ where: { id: identityProviderId }, + relations: ['workspace'], })) as (SSOConfiguration & WorkspaceSSOIdentityProvider) | null; }