diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example index 038b42d8b..76c24492d 100644 --- a/packages/twenty-server/.env.example +++ b/packages/twenty-server/.env.example @@ -27,7 +27,6 @@ FRONT_PORT=3001 # IS_MULTIWORKSPACE_ENABLED=false # AUTH_MICROSOFT_ENABLED=false # AUTH_MICROSOFT_CLIENT_ID=replace_me_with_azure_client_id -# AUTH_MICROSOFT_TENANT_ID=replace_me_with_azure_tenant_id # AUTH_MICROSOFT_CLIENT_SECRET=replace_me_with_azure_client_secret # AUTH_MICROSOFT_CALLBACK_URL=http://localhost:3000/auth/microsoft/redirect # AUTH_MICROSOFT_APIS_CALLBACK_URL=http://localhost:3000/auth/microsoft-apis/get-access-token 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 0a7b7a451..b658f25e2 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 @@ -14,6 +14,7 @@ export enum AuthExceptionCode { WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND', INVALID_INPUT = 'INVALID_INPUT', FORBIDDEN_EXCEPTION = 'FORBIDDEN_EXCEPTION', + INSUFFICIENT_SCOPES = 'INSUFFICIENT_SCOPES', UNAUTHENTICATED = 'UNAUTHENTICATED', INVALID_DATA = 'INVALID_DATA', INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', 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 db94e5d1f..de9bce16f 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 @@ -1,6 +1,10 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; 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'; @@ -14,18 +18,31 @@ export class MicrosoftAPIsOauthExchangeCodeForTokenGuard extends AuthGuard( } 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); - new MicrosoftAPIsOauthExchangeCodeForTokenStrategy(this.environmentService); + new MicrosoftAPIsOauthExchangeCodeForTokenStrategy( + this.environmentService, + ); - setRequestExtraParams(request, { - transientToken: state.transientToken, - redirectLocation: state.redirectLocation, - calendarVisibility: state.calendarVisibility, - messageVisibility: state.messageVisibility, - }); + setRequestExtraParams(request, { + transientToken: state.transientToken, + redirectLocation: state.redirectLocation, + calendarVisibility: state.calendarVisibility, + messageVisibility: state.messageVisibility, + }); - return (await super.canActivate(context)) as boolean; + 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, + ); + } + + return false; + } } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft-apis-oauth-common.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft-apis-oauth-common.auth.strategy.ts index 8b506a980..e0be81b2e 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft-apis-oauth-common.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft-apis-oauth-common.auth.strategy.ts @@ -22,7 +22,7 @@ export class MicrosoftAPIsOauthCommonStrategy extends PassportStrategy( super({ clientID: environmentService.get('AUTH_MICROSOFT_CLIENT_ID'), clientSecret: environmentService.get('AUTH_MICROSOFT_CLIENT_SECRET'), - tenant: environmentService.get('AUTH_MICROSOFT_TENANT_ID'), + tenant: 'common', callbackURL: environmentService.get('AUTH_MICROSOFT_APIS_CALLBACK_URL'), scope: scopes, passReqToCallback: true, diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft.auth.strategy.ts index 4460d906b..cbe7231d9 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft.auth.strategy.ts @@ -32,7 +32,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') { clientID: environmentService.get('AUTH_MICROSOFT_CLIENT_ID'), clientSecret: environmentService.get('AUTH_MICROSOFT_CLIENT_SECRET'), callbackURL: environmentService.get('AUTH_MICROSOFT_CALLBACK_URL'), - tenant: environmentService.get('AUTH_MICROSOFT_TENANT_ID'), + tenant: 'common', scope: ['user.read'], passReqToCallback: true, }); diff --git a/packages/twenty-server/src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes.ts b/packages/twenty-server/src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes.ts index e8353e88b..0daa2c3b7 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes.ts @@ -6,6 +6,7 @@ export const getMicrosoftApisOauthScopes = () => { 'offline_access', 'Mail.Read', 'Calendars.Read', + 'User.Read', ]; return scopes; diff --git a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts index d7e67b996..04edd54f0 100644 --- a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts @@ -204,10 +204,6 @@ export class EnvironmentVariables { @ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED) AUTH_MICROSOFT_CLIENT_ID: string; - @IsString() - @ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED) - AUTH_MICROSOFT_TENANT_ID: string; - @IsString() @ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED) AUTH_MICROSOFT_CLIENT_SECRET: string; diff --git a/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service.ts b/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service.ts index b14bb1f82..7f4d26334 100644 --- a/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service.ts +++ b/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service.ts @@ -17,10 +17,6 @@ export class MicrosoftOAuth2ClientManagerService { callback: AuthProviderCallback, ) => { try { - const tenantId = this.environmentService.get( - 'AUTH_MICROSOFT_TENANT_ID', - ); - const urlData = new URLSearchParams(); urlData.append( @@ -36,7 +32,7 @@ export class MicrosoftOAuth2ClientManagerService { urlData.append('grant_type', 'refresh_token'); const res = await fetch( - `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, + `https://login.microsoftonline.com/common/oauth2/v2.0/token`, { method: 'POST', body: urlData, diff --git a/packages/twenty-server/src/modules/contact-creation-manager/utils/filter-out-contacts-from-company-or-workspace.util.ts b/packages/twenty-server/src/modules/contact-creation-manager/utils/filter-out-contacts-from-company-or-workspace.util.ts index 42ecfd59d..6558f8717 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/utils/filter-out-contacts-from-company-or-workspace.util.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/utils/filter-out-contacts-from-company-or-workspace.util.ts @@ -2,6 +2,7 @@ import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/s import { Contact } from 'src/modules/contact-creation-manager/types/contact.type'; import { getDomainNameFromHandle } from 'src/modules/contact-creation-manager/utils/get-domain-name-from-handle.util'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { isWorkDomain } from 'src/utils/is-work-email'; export function filterOutSelfAndContactsFromCompanyOrWorkspace( contacts: Contact[], @@ -21,9 +22,13 @@ export function filterOutSelfAndContactsFromCompanyOrWorkspace( new Map(), ); + const isDifferentDomain = (contact: Contact, selfDomainName: string) => + getDomainNameFromHandle(contact.handle) !== selfDomainName; + return contacts.filter( (contact) => - getDomainNameFromHandle(contact.handle) !== selfDomainName && + (isDifferentDomain(contact, selfDomainName) || + !isWorkDomain(selfDomainName)) && !workspaceMembersMap[contact.handle] && !handleAliases.includes(contact.handle), ); diff --git a/packages/twenty-server/src/utils/is-work-email.ts b/packages/twenty-server/src/utils/is-work-email.ts index f5f8c3b75..edc3fade7 100644 --- a/packages/twenty-server/src/utils/is-work-email.ts +++ b/packages/twenty-server/src/utils/is-work-email.ts @@ -8,3 +8,7 @@ export const isWorkEmail = (email: string) => { return false; } }; + +export const isWorkDomain = (domain: string) => { + return !emailProvidersSet.has(domain); +}; diff --git a/packages/twenty-website/src/content/developers/self-hosting/setup.mdx b/packages/twenty-website/src/content/developers/self-hosting/setup.mdx index 4158c4fd1..d0110ae57 100644 --- a/packages/twenty-website/src/content/developers/self-hosting/setup.mdx +++ b/packages/twenty-website/src/content/developers/self-hosting/setup.mdx @@ -79,7 +79,6 @@ Then you can set the following environment variables: - `AUTH_MICROSOFT_ENABLED=true` - `AUTH_MICROSOFT_CLIENT_ID=` -- `AUTH_MICROSOFT_TENANT_ID=` - `AUTH_MICROSOFT_CLIENT_SECRET=` - `AUTH_MICROSOFT_CALLBACK_URL=https:///auth/microsoft/redirect` if you want to use Microsoft SSO - `AUTH_MICROSOFT_APIS_CALLBACK_URL=https:///auth/microsoft-apis/get-access-token` @@ -189,9 +188,8 @@ yarn command:prod cron:calendar:ongoing-stale ['AUTH_GOOGLE_CALLBACK_URL', 'https://[YourDomain]/auth/google/redirect', 'Google auth callback'], ['AUTH_MICROSOFT_ENABLED', 'false', 'Enable Microsoft SSO login'], ['AUTH_MICROSOFT_CLIENT_ID', '', 'Microsoft client ID'], - ['AUTH_MICROSOFT_TENANT_ID', '', 'Microsoft tenant ID'], ['AUTH_MICROSOFT_CLIENT_SECRET', '', 'Microsoft client secret'], - ['AUTH_MICROSOFT_CALLBACK_URL', 'http://[YourDomain]/auth/microsoft/redirect', 'Microsoft auth callback'], + ['AUTH_MICROSOFT_CALLBACK_URL', 'https://[YourDomain]/auth/microsoft/redirect', 'Microsoft auth callback'], ['AUTH_MICROSOFT_APIS_CALLBACK_URL', 'http://[YourDomain]/auth/microsoft-apis/get-access-token', 'Microsoft APIs auth callback'], ['IS_MULTIWORKSPACE_ENABLED', 'false', 'Allows the use of multiple workspaces. Requires a web server that can manage wildcards for subdomains.'], ['PASSWORD_RESET_TOKEN_EXPIRES_IN', '5m', 'Password reset token expiration time'],