Skip to content

Commit

Permalink
feat(sso): add support for identityProviderId in SAML flow (#9411)
Browse files Browse the repository at this point in the history
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 #9323
#9325
  • Loading branch information
AMoreaux authored Jan 7, 2025
1 parent 9392acb commit 00e7147
Show file tree
Hide file tree
Showing 6 changed files with 31 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -27,7 +27,7 @@ const StyledInputsContainer = styled.div`
`;

export const SettingsSSOIdentitiesProvidersForm = () => {
const { control, getValues } =
const { control, watch } =
useFormContext<SettingSecurityNewSSOIdentityFormValues>();

const IdentitiesProvidersMap: Record<
Expand Down Expand Up @@ -62,16 +62,22 @@ export const SettingsSSOIdentitiesProvidersForm = () => {
},
};

const getFormByType = (type: Uppercase<IdentityProviderType> | undefined) => {
switch (type) {
const selectedType = watch('type');

const formByType = useMemo(() => {
switch (selectedType) {
case IdentityProviderType.Oidc:
return IdentitiesProvidersMap.OIDC.form;
case IdentityProviderType.Saml:
return IdentitiesProvidersMap.SAML.form;
default:
return null;
}
};
}, [
IdentitiesProvidersMap.OIDC.form,
IdentitiesProvidersMap.SAML.form,
selectedType,
]);

return (
<SettingsPageContainer>
Expand Down Expand Up @@ -115,7 +121,7 @@ export const SettingsSSOIdentitiesProvidersForm = () => {
/>
</StyledInputsContainer>
</Section>
{getFormByType(getValues().type)}
{formByType}
</SettingsPageContainer>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>) => {
if (isDefined(e.target.files)) {
Expand All @@ -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<HTMLInputElement>(null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class SSOAuthController {
type: IdentityProviderType.SAML,
}),
callbackUrl: this.ssoService.buildCallbackUrl({
id: req.params.identityProviderId,
type: IdentityProviderType.SAML,
}),
});
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,11 @@ export class SSOService {
}

buildCallbackUrl(
identityProvider: Pick<WorkspaceSSOIdentityProvider, 'type'>,
identityProvider: Pick<WorkspaceSSOIdentityProvider, 'type' | 'id'>,
) {
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();
}
Expand Down

0 comments on commit 00e7147

Please sign in to comment.