Skip to content

Commit 4fc38a0

Browse files
authored
feat(ui): Add support for custom SAML provider in <ConfigureSSO /> (#8564)
1 parent 097ad4a commit 4fc38a0

13 files changed

Lines changed: 978 additions & 337 deletions

File tree

.changeset/public-parts-chew.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@clerk/localizations': patch
3+
'@clerk/clerk-js': patch
4+
'@clerk/shared': patch
5+
'@clerk/ui': patch
6+
---
7+
8+
Add support for custom SAML provider in `<ConfigureSSO />`

packages/localizations/src/en-US.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,9 +379,9 @@ export const enUS: LocalizationResource = {
379379
},
380380
},
381381
samlOkta: {
382-
title: 'Configure Okta Workforce',
383-
subtitle: 'Create a new enterprise application in your Okta Dashboard',
382+
headerTitle: 'Configure Okta Workforce',
384383
createApp: {
384+
headerSubtitle: 'Create a new enterprise application in your Okta Dashboard',
385385
title: 'Create a new enterprise application in Okta',
386386
step1: 'Sign in to Okta and go to <bold>Admin → Applications.</bold>',
387387
step2: 'Click <bold>Create App Integration.</bold>',
@@ -402,6 +402,7 @@ export const enUS: LocalizationResource = {
402402
step2: 'Complete the form with any comments and select <bold>Finish</bold>.',
403403
},
404404
configureAttributes: {
405+
headerSubtitle: 'Map users attributes from Okta to Clerk',
405406
step1: 'In the Okta dashboard, find the <bold>Attribute Statements</bold> section.',
406407
step2:
407408
'Select <bold>Add Expression</bold> for each attribute, and enter the following name and expression pairs:',
@@ -422,6 +423,7 @@ export const enUS: LocalizationResource = {
422423
},
423424
},
424425
assignUsers: {
426+
headerSubtitle: 'Assign users to the enterprise app',
425427
title: 'Assign selected user or group in Okta',
426428
paragraph: 'You need to assign users or groups to your enterprise app before they can use it to sign in.',
427429
step1: 'In the Okta dashboard, select the <bold>Assignments</bold> tab.',
@@ -432,6 +434,7 @@ export const enUS: LocalizationResource = {
432434
step5: 'Select the <bold>Done</bold> button to complete the assignment.',
433435
},
434436
metadataUrl: {
437+
headerSubtitle: 'Configure identity provider metadata',
435438
label: 'Metadata URL',
436439
placeholder: 'Paste URL here...',
437440
description: 'In your Okta SAML app, go to the Sign On tab and retrieve the metadata URL. Paste it below.',
@@ -463,6 +466,57 @@ export const enUS: LocalizationResource = {
463466
},
464467
},
465468
},
469+
samlCustom: {
470+
headerTitle: 'Configure your identity provider (IdP)',
471+
createApp: {
472+
headerSubtitle:
473+
'Register Clerk as a service provider in your IdP, then add your identity provider configuration.',
474+
title: 'Create a SAML application on your identity provider',
475+
subtitle:
476+
'In your identity provider’s admin dashboard, create a new SAML 2.0 application and use the following service provider details:',
477+
},
478+
configureAttributes: {
479+
headerSubtitle: 'Map user attributes from your identity provider to Clerk.',
480+
title: 'We expect your SAML responses to have the following specific attributes:',
481+
},
482+
assignUsers: {
483+
headerSubtitle: 'Assign users to the enterprise app',
484+
title: 'Assign selected user or group',
485+
paragraph: 'You need to assign users or groups to your enterprise app before they can use it to sign in.',
486+
},
487+
metadataUrl: {
488+
headerSubtitle: 'Configure identity provider metadata',
489+
label: 'Metadata URL',
490+
placeholder: 'Paste URL here...',
491+
description: 'In your enterprise app, retrieve the metadata URL. Paste it below.',
492+
},
493+
modes: {
494+
ariaLabel: 'Configuration mode',
495+
metadataUrl: 'Add via metadata',
496+
manual: 'Configure manually',
497+
},
498+
submitSamlConfig: {
499+
title: 'Fill in your SAML application details',
500+
},
501+
manual: {
502+
description: 'In your SAML app, retrieve these values.',
503+
signOnUrl: {
504+
label: 'Sign on URL',
505+
placeholder: 'Paste URL here...',
506+
},
507+
issuer: {
508+
label: 'Issuer',
509+
placeholder: 'Paste URL here...',
510+
},
511+
signingCertificate: {
512+
label: 'Signing certificate',
513+
uploadFile: 'Upload file',
514+
replaceFile: 'Replace file',
515+
removeFile: 'Remove file',
516+
fileUploaded: 'File uploaded',
517+
},
518+
},
519+
},
466520
},
467521
},
468522
createOrganization: {

packages/shared/src/types/localization.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,9 +1436,9 @@ export type __internal_LocalizationResource = {
14361436
};
14371437
};
14381438
samlOkta: {
1439-
title: LocalizationValue;
1440-
subtitle: LocalizationValue;
1439+
headerTitle: LocalizationValue;
14411440
createApp: {
1441+
headerSubtitle: LocalizationValue;
14421442
title: LocalizationValue;
14431443
step1: LocalizationValue;
14441444
step2: LocalizationValue;
@@ -1457,6 +1457,7 @@ export type __internal_LocalizationResource = {
14571457
step2: LocalizationValue;
14581458
};
14591459
configureAttributes: {
1460+
headerSubtitle: LocalizationValue;
14601461
step1: LocalizationValue;
14611462
step2: LocalizationValue;
14621463
pairs: {
@@ -1476,6 +1477,7 @@ export type __internal_LocalizationResource = {
14761477
};
14771478
};
14781479
assignUsers: {
1480+
headerSubtitle: LocalizationValue;
14791481
title: LocalizationValue;
14801482
paragraph: LocalizationValue;
14811483
step1: LocalizationValue;
@@ -1485,6 +1487,56 @@ export type __internal_LocalizationResource = {
14851487
step5: LocalizationValue;
14861488
};
14871489
metadataUrl: {
1490+
headerSubtitle: LocalizationValue;
1491+
label: LocalizationValue;
1492+
placeholder: LocalizationValue;
1493+
description: LocalizationValue;
1494+
};
1495+
modes: {
1496+
ariaLabel: LocalizationValue;
1497+
metadataUrl: LocalizationValue;
1498+
manual: LocalizationValue;
1499+
};
1500+
submitSamlConfig: {
1501+
title: LocalizationValue;
1502+
};
1503+
manual: {
1504+
description: LocalizationValue;
1505+
signOnUrl: {
1506+
label: LocalizationValue;
1507+
placeholder: LocalizationValue;
1508+
};
1509+
issuer: {
1510+
label: LocalizationValue;
1511+
placeholder: LocalizationValue;
1512+
};
1513+
signingCertificate: {
1514+
label: LocalizationValue;
1515+
uploadFile: LocalizationValue;
1516+
replaceFile: LocalizationValue;
1517+
removeFile: LocalizationValue;
1518+
fileUploaded: LocalizationValue;
1519+
};
1520+
};
1521+
};
1522+
samlCustom: {
1523+
headerTitle: LocalizationValue;
1524+
createApp: {
1525+
headerSubtitle: LocalizationValue;
1526+
title: LocalizationValue;
1527+
subtitle: LocalizationValue;
1528+
};
1529+
configureAttributes: {
1530+
headerSubtitle: LocalizationValue;
1531+
title: LocalizationValue;
1532+
};
1533+
assignUsers: {
1534+
headerSubtitle: LocalizationValue;
1535+
title: LocalizationValue;
1536+
paragraph: LocalizationValue;
1537+
};
1538+
metadataUrl: {
1539+
headerSubtitle: LocalizationValue;
14881540
label: LocalizationValue;
14891541
placeholder: LocalizationValue;
14901542
description: LocalizationValue;

packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { __internal_useUserEnterpriseConnections, useSession } from '@clerk/shared/react';
2-
import type { __experimental_ConfigureSSOProps } from '@clerk/shared/types';
1+
import {
2+
__internal_useEnterpriseConnectionTestRuns,
3+
__internal_useUserEnterpriseConnections,
4+
useSession,
5+
} from '@clerk/shared/react';
6+
import type { __experimental_ConfigureSSOProps, EnterpriseConnectionResource } from '@clerk/shared/types';
37
import React from 'react';
48

59
import { useProtect } from '@/common';
610
import { withCoreUserGuard } from '@/contexts';
711
import { Col, descriptors, Flex, Flow, Heading, Icon, localizationKeys, Text } from '@/customizables';
8-
import { withCardStateProvider } from '@/elements/contexts';
12+
import { useCardState, withCardStateProvider } from '@/elements/contexts';
913
import { ProfileCard } from '@/elements/ProfileCard';
1014
import { ExclamationTriangle } from '@/icons';
1115
import { Route, Switch } from '@/router';
@@ -16,7 +20,7 @@ import { ConfigureSSONavbar } from './ConfigureSSONavbar';
1620
import { ConfigureSSOSkeleton } from './ConfigureSSOSkeleton';
1721
import { ProfileCardFooter, ProfileCardHeader } from './elements/ProfileCard';
1822
import { Step } from './elements/Step';
19-
import { Wizard } from './elements/Wizard';
23+
import { useWizard, Wizard } from './elements/Wizard';
2024
import { ConfigureStep, ConfirmationStep, SelectProviderStep, TestConfigurationStep, VerifyDomainStep } from './steps';
2125

2226
const ConfigureSSOInternal = () => {
@@ -64,19 +68,31 @@ const AuthenticatedContent = withCoreUserGuard(() => {
6468
});
6569

6670
const ConfigureSSOCardContent = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
67-
const { data: enterpriseConnections, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true });
68-
71+
const {
72+
data: enterpriseConnections,
73+
isLoading: isLoadingEnterpriseConnections,
74+
createEnterpriseConnection,
75+
updateEnterpriseConnection,
76+
deleteEnterpriseConnection,
77+
} = __internal_useUserEnterpriseConnections({ enabled: true });
6978
// Currently FAPI only supports one enterprise connection per user
7079
const enterpriseConnection = enterpriseConnections?.[0];
7180

72-
if (isLoading && !enterpriseConnection) {
81+
const { hasSuccessfulTestRun, isLoading: isLoadingTestRuns } = useHasSuccessfulTestRun(enterpriseConnection);
82+
83+
const isLoading = isLoadingEnterpriseConnections || isLoadingTestRuns;
84+
if (isLoading) {
7385
return <ConfigureSSOSkeleton />;
7486
}
7587

7688
return (
7789
<ConfigureSSOProvider
90+
hasSuccessfulTestRun={hasSuccessfulTestRun}
7891
enterpriseConnection={enterpriseConnection}
7992
contentRef={contentRef}
93+
createEnterpriseConnection={createEnterpriseConnection}
94+
updateEnterpriseConnection={updateEnterpriseConnection}
95+
deleteEnterpriseConnection={deleteEnterpriseConnection}
8096
>
8197
<ConfigureSSOSteps />
8298
</ConfigureSSOProvider>
@@ -88,6 +104,7 @@ const ConfigureSSOSteps = () => {
88104

89105
return (
90106
<Wizard initialStepId={initialStepId}>
107+
<ResetCardErrorOnStepChange />
91108
<ConfigureSSOHeader />
92109

93110
<Wizard.Step id='select-provider'>
@@ -183,5 +200,45 @@ const MissingManageEnterpriseConnectionsPermission = () => (
183200
</>
184201
);
185202

203+
/**
204+
* Sentinel component rendered inside `<Wizard>`
205+
*
206+
* Clears any card-level error whenever the active step transitions, so a stale failure from one step
207+
* doesn't leak into the next
208+
*/
209+
const ResetCardErrorOnStepChange = (): null => {
210+
const { currentStep } = useWizard();
211+
const card = useCardState();
212+
const previousStepIdRef = React.useRef(currentStep?.id);
213+
214+
React.useEffect(() => {
215+
if (previousStepIdRef.current === currentStep?.id) {
216+
return;
217+
}
218+
219+
previousStepIdRef.current = currentStep?.id;
220+
card.setError(undefined);
221+
}, [currentStep?.id, card]);
222+
223+
return null;
224+
};
225+
226+
/**
227+
* Fetches a single successful test run for the given connection on mount
228+
*/
229+
const useHasSuccessfulTestRun = (
230+
enterpriseConnection: EnterpriseConnectionResource | undefined,
231+
): { hasSuccessfulTestRun: boolean; isLoading: boolean } => {
232+
const { data: successfulTestRuns, isLoading } = __internal_useEnterpriseConnectionTestRuns({
233+
enterpriseConnectionId: enterpriseConnection?.id ?? null,
234+
params: { initialPage: 1, pageSize: 1, status: ['success'] },
235+
});
236+
237+
return {
238+
hasSuccessfulTestRun: (successfulTestRuns?.length ?? 0) > 0,
239+
isLoading,
240+
};
241+
};
242+
186243
export const ConfigureSSO: React.ComponentType<__experimental_ConfigureSSOProps> =
187244
withCardStateProvider(ConfigureSSOInternal);

0 commit comments

Comments
 (0)