feat(sso): allow to use OIDC and SAML (#7246)

## What it does
### Backend
- [x] Add a mutation to create OIDC and SAML configuration
- [x] Add a mutation to delete an SSO config
- [x] Add a feature flag to toggle SSO
- [x] Add a mutation to activate/deactivate an SSO config
- [x] Add a mutation to delete an SSO config
- [x] Add strategy to use OIDC or SAML
- [ ] Improve error management

### Frontend
- [x] Add section "security" in settings
- [x] Add page to list SSO configurations
- [x] Add page and forms to create OIDC or SAML configuration
- [x] Add field to "connect with SSO" in the signin/signup process
- [x] Trigger auth when a user switch to a workspace with SSO enable
- [x] Add an option on the security page to activate/deactivate the
global invitation link
- [ ] Add new Icons for SSO Identity Providers (okta, Auth0, Azure,
Microsoft)

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Antoine Moreaux
2024-10-21 20:07:08 +02:00
committed by GitHub
parent 11c3f1c399
commit 0f0a7966b1
132 changed files with 5245 additions and 306 deletions

View File

@ -91,25 +91,7 @@ export const Invite = () => {
fullWidth
/>
</StyledContentContainer>
<FooterNote>
By using Twenty, you agree to the{' '}
<a
href="https://twenty.com/legal/terms"
target="_blank"
rel="noopener noreferrer"
>
Terms of Service
</a>{' '}
and{' '}
<a
href="https://twenty.com/legal/privacy"
target="_blank"
rel="noopener noreferrer"
>
Privacy Policy
</a>
.
</FooterNote>
<FooterNote />
</>
) : (
<SignInUpForm />

View File

@ -0,0 +1,69 @@
/* @license Enterprise */
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator';
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO';
import { guessSSOIdentityProviderIconByUrl } from '@/settings/security/utils/guessSSOIdentityProviderIconByUrl';
import { MainButton } from '@/ui/input/button/components/MainButton';
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)};
margin-top: ${({ theme }) => theme.spacing(4)};
`;
const StyledTitle = styled.h2`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin: 0;
`;
export const SSOWorkspaceSelection = () => {
const availableSSOIdentityProviders = useRecoilValue(
availableSSOIdentityProvidersState,
);
const { redirectToSSOLoginPage } = useSSO();
const availableWorkspacesForSSOGroupByWorkspace =
availableSSOIdentityProviders.reduce(
(acc, idp) => {
acc[idp.workspace.id] = [...(acc[idp.workspace.id] ?? []), idp];
return acc;
},
{} as Record<string, typeof availableSSOIdentityProviders>,
);
return (
<>
<StyledContentContainer>
{Object.values(availableWorkspacesForSSOGroupByWorkspace).map(
(idps) => (
<>
<StyledTitle>
{idps[0].workspace.displayName ?? DEFAULT_WORKSPACE_NAME}
</StyledTitle>
<HorizontalSeparator visible={false} />
{idps.map((idp) => (
<>
<MainButton
title={idp.name}
onClick={() => redirectToSSOLoginPage(idp.id)}
Icon={guessSSOIdentityProviderIconByUrl(idp.issuer)}
fullWidth
/>
<HorizontalSeparator visible={false} />
</>
))}
</>
),
)}
</StyledContentContainer>
<FooterNote />
</>
);
};

View File

@ -4,15 +4,14 @@ import { useRecoilValue } from 'recoil';
import { Logo } from '@/auth/components/Logo';
import { Title } from '@/auth/components/Title';
import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
import {
SignInUpMode,
SignInUpStep,
useSignInUp,
} from '@/auth/sign-in-up/hooks/useSignInUp';
import { SignInUpMode, useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
import { isDefined } from '~/utils/isDefined';
import { SignInUpStep } from '@/auth/states/signInUpStepState';
import { IconLockCustom } from '@ui/display/icon/components/IconLock';
import { SSOWorkspaceSelection } from './SSOWorkspaceSelection';
export const SignInUp = () => {
const { form } = useSignInUpForm();
@ -27,6 +26,9 @@ export const SignInUp = () => {
) {
return 'Welcome to Twenty';
}
if (signInUpStep === SignInUpStep.SSOWorkspaceSelection) {
return 'Choose SSO connection';
}
return signInUpMode === SignInUpMode.SignIn
? 'Sign in to Twenty'
: 'Sign up to Twenty';
@ -39,10 +41,18 @@ export const SignInUp = () => {
return (
<>
<AnimatedEaseIn>
<Logo />
{signInUpStep === SignInUpStep.SSOWorkspaceSelection ? (
<IconLockCustom size={40} />
) : (
<Logo />
)}
</AnimatedEaseIn>
<Title animate>{title}</Title>
<SignInUpForm />
{signInUpStep === SignInUpStep.SSOWorkspaceSelection ? (
<SSOWorkspaceSelection />
) : (
<SignInUpForm />
)}
</>
);
};