From 00e71477d3e11c73a772313ef30f568bff93c4c9 Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Tue, 7 Jan 2025 10:30:13 +0100 Subject: [PATCH] feat(sso): add support for identityProviderId in SAML flow (#9411) Updated SAML callback URLs and relevant logic to include identityProviderId, ensuring better handling of multiple identity providers. Refactored client and server-side code to streamline form interactions and validation within the SSO module. Fix https://github.com/twentyhq/twenty/issues/9323 https://github.com/twentyhq/twenty/issues/9325 --- .../SettingsSSOIdentitiesProvidersForm.tsx | 18 ++++++++----- .../components/SettingsSSOSAMLForm.tsx | 5 ++-- .../SettingsSecuritySSOIdentifyProvider.tsx | 25 +++++++++---------- .../auth/controllers/sso-auth.controller.ts | 3 ++- .../auth/guards/saml-auth.guard.ts | 6 ----- .../core-modules/sso/services/sso.service.ts | 4 +-- 6 files changed, 31 insertions(+), 30 deletions(-) diff --git a/packages/twenty-front/src/modules/settings/security/components/SettingsSSOIdentitiesProvidersForm.tsx b/packages/twenty-front/src/modules/settings/security/components/SettingsSSOIdentitiesProvidersForm.tsx index 05b5e9cdb..55286c828 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SettingsSSOIdentitiesProvidersForm.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SettingsSSOIdentitiesProvidersForm.tsx @@ -7,7 +7,7 @@ import { SettingsSSOSAMLForm } from '@/settings/security/components/SettingsSSOS import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider'; import { TextInput } from '@/ui/input/components/TextInput'; import styled from '@emotion/styled'; -import { ReactElement } from 'react'; +import { ReactElement, useMemo } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { H2Title, IconComponent, IconKey, Section } from 'twenty-ui'; import { IdentityProviderType } from '~/generated/graphql'; @@ -27,7 +27,7 @@ const StyledInputsContainer = styled.div` `; export const SettingsSSOIdentitiesProvidersForm = () => { - const { control, getValues } = + const { control, watch } = useFormContext(); const IdentitiesProvidersMap: Record< @@ -62,8 +62,10 @@ export const SettingsSSOIdentitiesProvidersForm = () => { }, }; - const getFormByType = (type: Uppercase | undefined) => { - switch (type) { + const selectedType = watch('type'); + + const formByType = useMemo(() => { + switch (selectedType) { case IdentityProviderType.Oidc: return IdentitiesProvidersMap.OIDC.form; case IdentityProviderType.Saml: @@ -71,7 +73,11 @@ export const SettingsSSOIdentitiesProvidersForm = () => { default: return null; } - }; + }, [ + IdentitiesProvidersMap.OIDC.form, + IdentitiesProvidersMap.SAML.form, + selectedType, + ]); return ( @@ -115,7 +121,7 @@ export const SettingsSSOIdentitiesProvidersForm = () => { /> - {getFormByType(getValues().type)} + {formByType} ); }; 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 cf6d5b2f9..ee8933cff 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SettingsSSOSAMLForm.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SettingsSSOSAMLForm.tsx @@ -56,7 +56,7 @@ const StyledButtonCopy = styled.div` export const SettingsSSOSAMLForm = () => { const { enqueueSnackBar } = useSnackBar(); const theme = useTheme(); - const { setValue, getValues, watch } = useFormContext(); + const { setValue, getValues, watch, trigger } = useFormContext(); const handleFileChange = async (e: ChangeEvent) => { if (isDefined(e.target.files)) { @@ -72,11 +72,12 @@ export const SettingsSSOSAMLForm = () => { setValue('ssoURL', samlMetadataParsed.data.ssoUrl); setValue('certificate', samlMetadataParsed.data.certificate); setValue('issuer', samlMetadataParsed.data.entityID); + trigger(); } }; const entityID = `${REACT_APP_SERVER_BASE_URL}/auth/saml/login/${getValues('id')}`; - const acsUrl = `${REACT_APP_SERVER_BASE_URL}/auth/saml/callback`; + const acsUrl = `${REACT_APP_SERVER_BASE_URL}/auth/saml/callback/${getValues('id')}`; const inputFileRef = useRef(null); diff --git a/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx b/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx index 2f7cc0a07..bdff355ee 100644 --- a/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx +++ b/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx @@ -12,7 +12,7 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useEffect } from 'react'; +import pick from 'lodash.pick'; import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; @@ -31,20 +31,19 @@ export const SettingsSecuritySSOIdentifyProvider = () => { ), }); - const selectedType = formConfig.watch('type'); - - useEffect( - () => - formConfig.reset({ - ...sSOIdentityProviderDefaultValues[selectedType](), - name: formConfig.getValues('name'), - }), - [formConfig, selectedType], - ); - const handleSave = async () => { try { - await createSSOIdentityProvider(formConfig.getValues()); + const type = formConfig.getValues('type'); + + await createSSOIdentityProvider( + SSOIdentitiesProvidersParamsSchema.parse( + pick( + formConfig.getValues(), + Object.keys(sSOIdentityProviderDefaultValues[type]()), + ), + ), + ); + navigate(getSettingsPagePath(SettingsPath.Security)); } catch (error) { enqueueSnackBar((error as Error).message, { 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 eb871aeef..394c86713 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 @@ -58,6 +58,7 @@ export class SSOAuthController { type: IdentityProviderType.SAML, }), callbackUrl: this.ssoService.buildCallbackUrl({ + id: req.params.identityProviderId, type: IdentityProviderType.SAML, }), }); @@ -104,7 +105,7 @@ export class SSOAuthController { } } - @Post('saml/callback') + @Post('saml/callback/:identityProviderId') @UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard) async samlAuthCallback(@Req() req: any, @Res() res: Response) { try { 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 fba753a07..22ecb2306 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 @@ -20,12 +20,6 @@ export class SAMLAuthGuard extends AuthGuard('saml') { try { const request = context.switchToHttp().getRequest(); - const RelayState = - 'RelayState' in request.body ? JSON.parse(request.body.RelayState) : {}; - - request.params.identityProviderId = - request.params.identityProviderId ?? RelayState.identityProviderId; - if (!request.params.identityProviderId) { throw new AuthException( 'Invalid SAML identity provider', 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 6e43cacd7..2a2c5f791 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 @@ -155,11 +155,11 @@ export class SSOService { } buildCallbackUrl( - identityProvider: Pick, + identityProvider: Pick, ) { const callbackURL = new URL(this.environmentService.get('SERVER_URL')); - callbackURL.pathname = `/auth/${identityProvider.type.toLowerCase()}/callback`; + callbackURL.pathname = `/auth/${identityProvider.type.toLowerCase()}/callback/${identityProvider.id}`; return callbackURL.toString(); }