feat(sso): fix saml + allow to use public invite with sso + fix invite page with multiple sso provider (#9963)

- Fix SAML issue
- Fix the wrong state on the Invite page when multiple SSO provider
exists
- Allow to signup with SSO and public invite link
- For OIDC, use the property upn to guess email for Microsoft and enable
oidc with a specific context in azure
- Improve error in OIDC flow when email not found
This commit is contained in:
Antoine Moreaux
2025-02-03 18:48:25 +01:00
committed by GitHub
parent 253a3eb83f
commit 47487f5d1c
14 changed files with 122 additions and 92 deletions

View File

@ -612,6 +612,7 @@ export type FullName = {
export type GetAuthorizationUrlInput = {
identityProviderId: Scalars['String']['input'];
workspaceInviteHash?: InputMaybe<Scalars['String']['input']>;
};
export type GetAuthorizationUrlOutput = {

View File

@ -600,6 +600,7 @@ export type FullName = {
export type GetAuthorizationUrlInput = {
identityProviderId: Scalars['String'];
workspaceInviteHash?: InputMaybe<Scalars['String']>;
};
export type GetAuthorizationUrlOutput = {

View File

@ -13,7 +13,6 @@ import {
Route,
} from 'react-router-dom';
import { Authorize } from '~/pages/auth/Authorize';
import { Invite } from '~/pages/auth/Invite';
import { PasswordReset } from '~/pages/auth/PasswordReset';
import { SignInUp } from '~/pages/auth/SignInUp';
import { NotFound } from '~/pages/not-found/NotFound';
@ -43,7 +42,7 @@ export const useCreateAppRouter = (
<Route path={AppPath.Verify} element={<VerifyEffect />} />
<Route path={AppPath.VerifyEmail} element={<VerifyEmailEffect />} />
<Route path={AppPath.SignInUp} element={<SignInUp />} />
<Route path={AppPath.Invite} element={<Invite />} />
<Route path={AppPath.Invite} element={<SignInUp />} />
<Route path={AppPath.ResetPassword} element={<PasswordReset />} />
<Route path={AppPath.CreateWorkspace} element={<CreateWorkspace />} />
<Route path={AppPath.CreateProfile} element={<CreateProfile />} />

View File

@ -5,11 +5,13 @@ import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useApolloClient } from '@apollo/client';
import { useParams } from 'react-router-dom';
export const useSSO = () => {
const apolloClient = useApolloClient();
const { enqueueSnackBar } = useSnackBar();
const workspaceInviteHash = useParams().workspaceInviteHash;
const { enqueueSnackBar } = useSnackBar();
const { redirect } = useRedirect();
const redirectToSSOLoginPage = async (identityProviderId: string) => {
@ -18,7 +20,7 @@ export const useSSO = () => {
authorizationUrlForSSOResult = await apolloClient.mutate({
mutation: GET_AUTHORIZATION_URL,
variables: {
input: { identityProviderId },
input: { identityProviderId, workspaceInviteHash },
},
});
} catch (error: any) {

View File

@ -24,6 +24,7 @@ export const useWorkspaceFromInviteHash = () => {
);
const { data: workspaceFromInviteHash, loading } =
useGetWorkspaceFromInviteHashQuery({
skip: !workspaceInviteHash,
variables: { inviteHash: workspaceInviteHash || '' },
onError: (error) => {
enqueueSnackBar(error.message, {

View File

@ -1,27 +0,0 @@
import { Logo } from '@/auth/components/Logo';
import { Title } from '@/auth/components/Title';
import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
import { useMemo } from 'react';
import { AnimatedEaseIn } from 'twenty-ui';
import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeFormEffect';
export const Invite = () => {
const { workspace: workspaceFromInviteHash } = useWorkspaceFromInviteHash();
const title = useMemo(() => {
return `Join ${workspaceFromInviteHash?.displayName ?? ''} team`;
}, [workspaceFromInviteHash?.displayName]);
return (
<>
<AnimatedEaseIn>
<Logo secondaryLogo={workspaceFromInviteHash?.logo} />
</AnimatedEaseIn>
<Title animate>{title}</Title>
<SignInUpWorkspaceScopeFormEffect />
<SignInUpWorkspaceScopeForm />
</>
);
};

View File

@ -23,29 +23,25 @@ import { AnimatedEaseIn } from 'twenty-ui';
import { useSearchParams } from 'react-router-dom';
import { PublicWorkspaceDataOutput } from '~/generated-metadata/graphql';
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
const StandardContent = ({
workspacePublicData,
signInUpForm,
signInUpStep,
title,
}: {
workspacePublicData: PublicWorkspaceDataOutput | null;
signInUpForm: JSX.Element | null;
signInUpStep: SignInUpStep;
title: string;
}) => {
return (
<>
<AnimatedEaseIn>
<Logo secondaryLogo={workspacePublicData?.logo} />
</AnimatedEaseIn>
<Title animate>
Welcome to{' '}
{!isDefined(workspacePublicData?.displayName)
? DEFAULT_WORKSPACE_NAME
: workspacePublicData?.displayName === ''
? 'Your Workspace'
: workspacePublicData?.displayName}
</Title>
<Title animate>{title}</Title>
{signInUpForm}
{signInUpStep !== SignInUpStep.Password && <FooterNote />}
</>
@ -61,9 +57,29 @@ export const SignInUp = () => {
const workspacePublicData = useRecoilValue(workspacePublicDataState);
const { loading } = useGetPublicWorkspaceDataBySubdomain();
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const { workspaceInviteHash, workspace: workspaceFromInviteHash } =
useWorkspaceFromInviteHash();
const [searchParams] = useSearchParams();
const title = useMemo(() => {
if (isDefined(workspaceInviteHash)) {
return `Join ${workspaceFromInviteHash?.displayName ?? ''} team`;
}
return `Welcome to ${
!isDefined(workspacePublicData?.displayName)
? DEFAULT_WORKSPACE_NAME
: workspacePublicData?.displayName === ''
? 'Your Workspace'
: workspacePublicData?.displayName
}`;
}, [
workspaceFromInviteHash?.displayName,
workspaceInviteHash,
workspacePublicData?.displayName,
]);
const signInUpForm = useMemo(() => {
if (loading) return null;
@ -110,6 +126,7 @@ export const SignInUp = () => {
workspacePublicData={workspacePublicData}
signInUpForm={signInUpForm}
signInUpStep={signInUpStep}
title={title}
/>
);
};

View File

@ -12,11 +12,11 @@ import {
import { graphqlMocks } from '~/testing/graphqlMocks';
import { AppPath } from '@/types/AppPath';
import { Invite } from '../Invite';
import { SignInUp } from '../SignInUp';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Auth/Invite',
component: Invite,
component: SignInUp,
decorators: [PageDecorator],
args: {
routePath: AppPath.Invite,
@ -67,7 +67,7 @@ const meta: Meta<PageDecoratorArgs> = {
export default meta;
export type Story = StoryObj<typeof Invite>;
export type Story = StoryObj<typeof SignInUp>;
export const Default: Story = {
play: async ({ canvasElement }) => {