diff --git a/packages/twenty-front/src/modules/settings/security/utils/guessSSOIdentityProviderIconByUrl.ts b/packages/twenty-front/src/modules/settings/security/utils/guessSSOIdentityProviderIconByUrl.ts index f8582577f999..1fbde994daaa 100644 --- a/packages/twenty-front/src/modules/settings/security/utils/guessSSOIdentityProviderIconByUrl.ts +++ b/packages/twenty-front/src/modules/settings/security/utils/guessSSOIdentityProviderIconByUrl.ts @@ -1,6 +1,11 @@ /* @license Enterprise */ -import { IconComponent, IconGoogle, IconKey } from 'twenty-ui'; +import { + IconComponent, + IconGoogle, + IconKey, + IconMicrosoftOutlook, +} from 'twenty-ui'; export const guessSSOIdentityProviderIconByUrl = ( url: string, @@ -9,5 +14,9 @@ export const guessSSOIdentityProviderIconByUrl = ( return IconGoogle; } + if (url.includes('microsoft')) { + return IconMicrosoftOutlook; + } + return IconKey; }; 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 530fd553394b..d7bf1d479d3b 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 @@ -51,7 +51,6 @@ export class GoogleAuthController { email, picture, workspaceInviteHash, - workspacePersonalInviteToken, workspaceId, billingCheckoutSessionState, } = req.user; @@ -65,7 +64,7 @@ export class GoogleAuthController { try { const invitation = - currentWorkspace && workspacePersonalInviteToken && email + currentWorkspace && email ? await this.authService.findInvitationForSignInUp({ currentWorkspace, email, 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 7f172d04ee4f..7b7b6a6f620b 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 @@ -52,7 +52,6 @@ export class MicrosoftAuthController { email, picture, workspaceInviteHash, - workspacePersonalInviteToken, workspaceId, billingCheckoutSessionState, } = req.user; @@ -66,7 +65,7 @@ export class MicrosoftAuthController { try { const invitation = - currentWorkspace && workspacePersonalInviteToken && email + currentWorkspace && email ? await this.authService.findInvitationForSignInUp({ currentWorkspace, email, 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 67ade3f879f0..002fc85941b8 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 @@ -34,6 +34,8 @@ 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'; +import { SAMLRequest } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy'; +import { OIDCRequest } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy'; @Controller('auth') export class SSOAuthController { @@ -85,14 +87,14 @@ export class SSOAuthController { @Get('oidc/callback') @UseGuards(EnterpriseFeaturesEnabledGuard, OIDCAuthGuard) @UseFilters(AuthOAuthExceptionFilter) - async oidcAuthCallback(@Req() req: any, @Res() res: Response) { + async oidcAuthCallback(@Req() req: OIDCRequest, @Res() res: Response) { return await this.authCallback(req, res); } @Post('saml/callback/:identityProviderId') @UseGuards(EnterpriseFeaturesEnabledGuard, SAMLAuthGuard) @UseFilters(AuthOAuthExceptionFilter) - async samlAuthCallback(@Req() req: any, @Res() res: Response) { + async samlAuthCallback(@Req() req: SAMLRequest, @Res() res: Response) { try { return await this.authCallback(req, res); } catch (err) { @@ -103,10 +105,10 @@ export class SSOAuthController { } } - private async authCallback({ user }: any, res: Response) { + private async authCallback(req: OIDCRequest | SAMLRequest, res: Response) { const workspaceIdentityProvider = await this.findWorkspaceIdentityProviderByIdentityProviderId( - user.identityProviderId, + req.user.identityProviderId, ); try { @@ -117,7 +119,7 @@ export class SSOAuthController { ); } - if (!user.user.email) { + if (!req.user.email) { throw new AuthException( 'Email not found from identity provider.', AuthExceptionCode.OAUTH_ACCESS_DENIED, @@ -125,7 +127,7 @@ export class SSOAuthController { } const { loginToken, identityProvider } = await this.generateLoginToken( - user.user, + req.user, workspaceIdentityProvider, ); @@ -157,7 +159,7 @@ export class SSOAuthController { } private async generateLoginToken( - payload: { email: string } & Record, + payload: { email: string }, identityProvider: WorkspaceSSOIdentityProvider, ) { if (!identityProvider) { 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 e53c50194d97..96d9bfd33f41 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 @@ -3,6 +3,7 @@ import { AuthGuard } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { Request } from 'express'; import { AuthException, @@ -26,7 +27,7 @@ export class GoogleOauthGuard extends AuthGuard('google') { } async canActivate(context: ExecutionContext) { - const request = context.switchToHttp().getRequest(); + const request = context.switchToHttp().getRequest(); let workspace: Workspace | null = null; try { @@ -40,9 +41,6 @@ export class GoogleOauthGuard extends AuthGuard('google') { }); } - const workspaceInviteHash = request.query.inviteHash; - const workspacePersonalInviteToken = request.query.inviteToken; - if (request.query.error === 'access_denied') { throw new AuthException( 'Google OAuth access denied', @@ -50,26 +48,6 @@ export class GoogleOauthGuard extends AuthGuard('google') { ); } - if (workspaceInviteHash && typeof workspaceInviteHash === 'string') { - request.params.workspaceInviteHash = workspaceInviteHash; - } - - 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; } catch (err) { this.guardRedirectService.dispatchErrorFromGuard( 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 78c7b00833b3..58c1a2d596af 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 @@ -36,29 +36,6 @@ export class MicrosoftOAuthGuard extends AuthGuard('microsoft') { }); } - const workspaceInviteHash = request.query.inviteHash; - const workspacePersonalInviteToken = request.query.inviteToken; - - if (workspaceInviteHash && typeof workspaceInviteHash === 'string') { - request.params.workspaceInviteHash = workspaceInviteHash; - } - - 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; } catch (err) { this.guardRedirectService.dispatchErrorFromGuard( diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/google.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/google.auth.strategy.ts index 358ad7e62d19..e12e7c452009 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/google.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/google.auth.strategy.ts @@ -34,24 +34,14 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { }); } - authenticate(req: any, options: any) { + authenticate(req: Request, options: any) { options = { ...options, state: JSON.stringify({ - workspaceInviteHash: req.params.workspaceInviteHash, + workspaceInviteHash: req.query.workspaceInviteHash, workspaceId: req.params.workspaceId, - ...(req.params.billingCheckoutSessionState - ? { - billingCheckoutSessionState: - req.params.billingCheckoutSessionState, - } - : {}), - ...(req.params.workspacePersonalInviteToken - ? { - workspacePersonalInviteToken: - req.params.workspacePersonalInviteToken, - } - : {}), + billingCheckoutSessionState: req.query.billingCheckoutSessionState, + workspacePersonalInviteToken: req.query.workspacePersonalInviteToken, }), }; 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 cbe7231d9651..02b13f9cf218 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 @@ -38,24 +38,14 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') { }); } - authenticate(req: any, options: any) { + authenticate(req: Request, options: any) { options = { ...options, state: JSON.stringify({ - workspaceInviteHash: req.params.workspaceInviteHash, + workspaceInviteHash: req.query.workspaceInviteHash, workspaceId: req.params.workspaceId, - ...(req.params.billingCheckoutSessionState - ? { - billingCheckoutSessionState: - req.params.billingCheckoutSessionState, - } - : {}), - ...(req.params.workspacePersonalInviteToken - ? { - workspacePersonalInviteToken: - req.params.workspacePersonalInviteToken, - } - : {}), + billingCheckoutSessionState: req.query.billingCheckoutSessionState, + workspacePersonalInviteToken: req.query.workspacePersonalInviteToken, }), }; 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 8d3b94e9b024..c57623496ef4 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 @@ -3,11 +3,26 @@ import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; +import { isEmail } from 'class-validator'; +import { Request } from 'express'; +import { Strategy, StrategyOptions, TokenSet } from 'openid-client'; + import { - Strategy, - StrategyOptions, - StrategyVerifyCallbackReq, -} from 'openid-client'; + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; + +export type OIDCRequest = Omit< + Request, + 'user' | 'workspace' | 'workspaceMetadataVersion' +> & { + user: { + identityProviderId: string; + email: string; + firstName?: string | null; + lastName?: string | null; + }; +}; @Injectable() export class OIDCAuthStrategy extends PassportStrategy( @@ -30,7 +45,7 @@ export class OIDCAuthStrategy extends PassportStrategy( }); } - async authenticate(req: any, options: any) { + async authenticate(req: Request, options: any) { return super.authenticate(req, { ...options, state: JSON.stringify({ @@ -39,37 +54,50 @@ export class OIDCAuthStrategy extends PassportStrategy( }); } - validate: StrategyVerifyCallbackReq<{ + private extractState(req: Request): { identityProviderId: string; - user: { - email?: string; - firstName?: string | null; - lastName?: string | null; - }; - }> = async (req, tokenset, done) => { + } { try { const state = JSON.parse( - 'query' in req && - req.query && - typeof req.query === 'object' && - 'state' in req.query && - req.query.state && - typeof req.query.state === 'string' + req.query.state && typeof req.query.state === 'string' ? req.query.state : '{}', ); + if (!state.identityProviderId) { + throw new Error(); + } + + return { + identityProviderId: state.identityProviderId, + }; + } catch (err) { + throw new AuthException('Invalid state', AuthExceptionCode.INVALID_INPUT); + } + } + + async validate( + req: Request, + tokenset: TokenSet, + done: (err: any, user?: OIDCRequest['user']) => void, + ) { + try { + const state = this.extractState(req); + const userinfo = await this.client.userinfo(tokenset); - const user = { + if (!userinfo.email || !isEmail(userinfo.email)) { + return done(new Error('Invalid email')); + } + + done(null, { email: userinfo.email, + identityProviderId: state.identityProviderId, ...(userinfo.given_name ? { firstName: userinfo.given_name } : {}), ...(userinfo.family_name ? { lastName: userinfo.family_name } : {}), - }; - - done(null, { user, identityProviderId: state.identityProviderId }); + }); } catch (err) { done(err); } - }; + } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/saml.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/saml.auth.strategy.ts index 822b80f5ef3d..c3c9e5e7ee01 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/saml.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/saml.auth.strategy.ts @@ -15,6 +15,20 @@ import { isEmail } from 'class-validator'; import { Request } from 'express'; import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; + +export type SAMLRequest = Omit< + Request, + 'user' | 'workspace' | 'workspaceMetadataVersion' +> & { + user: { + identityProviderId: string; + email: string; + }; +}; @Injectable() export class SamlAuthStrategy extends PassportStrategy( @@ -69,6 +83,24 @@ export class SamlAuthStrategy extends PassportStrategy( }); } + private extractState(req: Request): { + identityProviderId: string; + } { + try { + if ('RelayState' in req.body && typeof req.body.RelayState === 'string') { + const RelayState = JSON.parse(req.body.RelayState); + + return { + identityProviderId: RelayState.identityProviderId, + }; + } + + throw new Error(); + } catch (err) { + throw new AuthException('Invalid state', AuthExceptionCode.INVALID_INPUT); + } + } + validate: VerifyWithRequest = async (request, profile, done) => { try { if (!profile) { @@ -80,20 +112,11 @@ export class SamlAuthStrategy extends PassportStrategy( if (!isEmail(email)) { return done(new Error('Invalid email')); } + const state = this.extractState(request); - const result: { - user: Record; - identityProviderId?: string; - } = { user: { email } }; - - if ( - 'RelayState' in request.body && - typeof request.body.RelayState === 'string' - ) { - const RelayState = JSON.parse(request.body.RelayState); - - result.identityProviderId = RelayState.identityProviderId; - } + const result: Pick = { + user: { ...state, email }, + }; done(null, result); } catch (err) { diff --git a/packages/twenty-ui/src/display/icon/components/IconMicrosoftOutlook.tsx b/packages/twenty-ui/src/display/icon/components/IconMicrosoftOutlook.tsx index 153d0af09049..913eb6c4678a 100644 --- a/packages/twenty-ui/src/display/icon/components/IconMicrosoftOutlook.tsx +++ b/packages/twenty-ui/src/display/icon/components/IconMicrosoftOutlook.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@emotion/react'; -import IconMicrosoftOutlookRaw from '../assets/microsoft-outlook.svg?react'; +import IconMicrosoftRaw from '../assets/microsoft.svg?react'; interface IconMicrosoftOutlookProps { size?: number; @@ -10,5 +10,5 @@ export const IconMicrosoftOutlook = (props: IconMicrosoftOutlookProps) => { const theme = useTheme(); const size = props.size ?? theme.icon.size.lg; - return ; + return ; };