Skip to content

Commit 770bd07

Browse files
committed
Add identity provider metadata step
1 parent 0776f5d commit 770bd07

3 files changed

Lines changed: 281 additions & 1 deletion

File tree

packages/localizations/src/en-US.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,40 @@ export const enUS: LocalizationResource = {
669669
},
670670
},
671671
},
672+
identityProviderMetadataStep: {
673+
headerSubtitle: 'Configure identity provider metadata',
674+
modes: {
675+
title: 'Fill in your Microsoft Entra application details',
676+
ariaLabel: 'Configuration ',
677+
metadataUrl: 'Add via metadata',
678+
manual: 'Configure manually',
679+
},
680+
metadataUrl: {
681+
label: 'Metadata URL',
682+
placeholder: 'Paste URL here...',
683+
description:
684+
'On the <bold>SAML-based Sign-on</bold> page, find the <bold>SAML Certificates</bold> section. Add the <bold>App Federation Metadata Url</bold> below.',
685+
},
686+
manual: {
687+
description:
688+
'On the <bold>SAML-based Sign-on</bold> page, find the <bold>SAML Certificates</bold> section. Retrieve these values and add them below.',
689+
signOnUrl: {
690+
label: 'Single Sign-On URL',
691+
placeholder: 'Paste URL here...',
692+
},
693+
issuer: {
694+
label: 'Issuer',
695+
placeholder: 'Paste URL here...',
696+
},
697+
signingCertificate: {
698+
label: 'Signing certificate',
699+
uploadFile: 'Upload file',
700+
replaceFile: 'Replace file',
701+
removeFile: 'Remove file',
702+
fileUploaded: 'File uploaded',
703+
},
704+
},
705+
},
672706
},
673707
},
674708
},

packages/shared/src/types/localization.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1712,6 +1712,38 @@ export type __internal_LocalizationResource = {
17121712
};
17131713
};
17141714
};
1715+
identityProviderMetadataStep: {
1716+
headerSubtitle: LocalizationValue;
1717+
modes: {
1718+
title: LocalizationValue;
1719+
ariaLabel: LocalizationValue;
1720+
metadataUrl: LocalizationValue;
1721+
manual: LocalizationValue;
1722+
};
1723+
metadataUrl: {
1724+
label: LocalizationValue;
1725+
placeholder: LocalizationValue;
1726+
description: LocalizationValue;
1727+
};
1728+
manual: {
1729+
description: LocalizationValue;
1730+
signOnUrl: {
1731+
label: LocalizationValue;
1732+
placeholder: LocalizationValue;
1733+
};
1734+
issuer: {
1735+
label: LocalizationValue;
1736+
placeholder: LocalizationValue;
1737+
};
1738+
signingCertificate: {
1739+
label: LocalizationValue;
1740+
uploadFile: LocalizationValue;
1741+
replaceFile: LocalizationValue;
1742+
removeFile: LocalizationValue;
1743+
fileUploaded: LocalizationValue;
1744+
};
1745+
};
1746+
};
17151747
};
17161748
};
17171749
confirmation: {

packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlMicrosoftConfigureSteps.tsx

Lines changed: 215 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { type JSX } from 'react';
1+
import React, { type JSX } from 'react';
22

33
import { Box, Col, descriptors, Heading, localizationKeys, Text } from '@/customizables';
44
import { ClipboardInput } from '@/elements/ClipboardInput';
5+
import { useCardState } from '@/elements/contexts';
56
import { Form } from '@/elements/Form';
67
import { Checkmark, Clipboard } from '@/icons';
78
import { useFormControl } from '@/ui/utils/useFormControl';
@@ -10,6 +11,16 @@ import { useConfigureSSO } from '../../../ConfigureSSOContext';
1011
import { Step } from '../../../elements/Step';
1112
import { useWizard, Wizard } from '../../../elements/Wizard';
1213
import { InnerStepCounter } from '../../../elements/Wizard/InnerStepCounter';
14+
import {
15+
applySamlSubmitError,
16+
buildSamlConfigurationPayload,
17+
IdentityProviderConfigurationForm,
18+
type IdentityProviderConfigurationFormProps,
19+
} from './shared/IdentityProviderConfigurationForm';
20+
import {
21+
IdentityProviderConfigurationModes,
22+
type IdpConfigurationMode,
23+
} from './shared/IdentityProviderConfigurationModes';
1324

1425
export const SamlMicrosoftConfigureSteps = (): JSX.Element => {
1526
return (
@@ -33,6 +44,18 @@ export const SamlMicrosoftConfigureSteps = (): JSX.Element => {
3344
</Step.Header>
3445
<SamlMicrosoftServiceProviderStep />
3546
</Wizard.Step>
47+
48+
<Wizard.Step id='identity-provider-metadata'>
49+
<Step.Header
50+
title={localizationKeys('configureSSO.configureStep.samlMicrosoft.mainHeaderTitle')}
51+
description={localizationKeys(
52+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.headerSubtitle',
53+
)}
54+
>
55+
<InnerStepCounter />
56+
</Step.Header>
57+
<SamlMicrosoftIdentityProviderMetadataStep />
58+
</Wizard.Step>
3659
</>
3760
);
3861
};
@@ -370,3 +393,194 @@ const SamlMicrosoftServiceProviderStep = (): JSX.Element => {
370393
</>
371394
);
372395
};
396+
397+
const MICROSOFT_SAML_IDP_MODES = ['metadataUrl', 'manual'] as const satisfies readonly IdpConfigurationMode[];
398+
399+
const SamlMicrosoftIdentityProviderMetadataStep = (): JSX.Element => {
400+
const card = useCardState();
401+
const { goNext, goPrev, isFirstStep } = useWizard();
402+
const { enterpriseConnection, updateEnterpriseConnection } = useConfigureSSO();
403+
404+
const samlConnection = enterpriseConnection?.samlConnection;
405+
const hasExistingConfig = Boolean(
406+
samlConnection?.idpSsoUrl ||
407+
samlConnection?.idpEntityId ||
408+
samlConnection?.idpCertificate ||
409+
samlConnection?.idpMetadataUrl,
410+
);
411+
const existingCertPresent = Boolean(samlConnection?.idpCertificate);
412+
413+
const [mode, setMode] = React.useState<IdpConfigurationMode>(hasExistingConfig ? 'manual' : 'metadataUrl');
414+
const [certFile, setCertFile] = React.useState<File | null>(null);
415+
416+
const metadataUrlField = useFormControl('idpMetadataUrl', samlConnection?.idpMetadataUrl ?? '', {
417+
type: 'text',
418+
label: localizationKeys('configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.metadataUrl.label'),
419+
placeholder: localizationKeys(
420+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.metadataUrl.placeholder',
421+
),
422+
isRequired: true,
423+
});
424+
425+
const signOnUrlField = useFormControl('idpSsoUrl', samlConnection?.idpSsoUrl ?? '', {
426+
type: 'text',
427+
label: localizationKeys(
428+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.signOnUrl.label',
429+
),
430+
placeholder: localizationKeys(
431+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.signOnUrl.placeholder',
432+
),
433+
isRequired: true,
434+
});
435+
436+
const issuerField = useFormControl('idpEntityId', samlConnection?.idpEntityId ?? '', {
437+
type: 'text',
438+
label: localizationKeys(
439+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.issuer.label',
440+
),
441+
placeholder: localizationKeys(
442+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.issuer.placeholder',
443+
),
444+
isRequired: true,
445+
});
446+
447+
const certificateField = useFormControl('idpCertificate', '', {
448+
type: 'text',
449+
label: localizationKeys(
450+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.signingCertificate.label',
451+
),
452+
isRequired: true,
453+
});
454+
455+
const trimmedMetadataUrl = metadataUrlField.value.trim();
456+
const trimmedSignOnUrl = signOnUrlField.value.trim();
457+
const trimmedIssuer = issuerField.value.trim();
458+
const hasCert = certFile !== null || existingCertPresent;
459+
460+
const isValid =
461+
mode === 'metadataUrl'
462+
? trimmedMetadataUrl.length > 0
463+
: trimmedSignOnUrl.length > 0 && trimmedIssuer.length > 0 && hasCert;
464+
465+
const canSubmit = isValid && !card.isLoading;
466+
467+
const formProps: IdentityProviderConfigurationFormProps =
468+
mode === 'metadataUrl'
469+
? {
470+
mode: 'metadataUrl',
471+
form: { field: metadataUrlField },
472+
labels: {
473+
description: localizationKeys(
474+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.metadataUrl.description',
475+
),
476+
},
477+
}
478+
: {
479+
mode: 'manual',
480+
form: {
481+
signOnUrlField,
482+
issuerField,
483+
certificateField,
484+
certFile,
485+
onCertFileChange: setCertFile,
486+
existingCertPresent,
487+
},
488+
labels: {
489+
description: localizationKeys(
490+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.description',
491+
),
492+
uploadFile: localizationKeys(
493+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.signingCertificate.uploadFile',
494+
),
495+
replaceFile: localizationKeys(
496+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.signingCertificate.replaceFile',
497+
),
498+
removeFile: localizationKeys(
499+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.signingCertificate.removeFile',
500+
),
501+
fileUploaded: localizationKeys(
502+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.manual.signingCertificate.fileUploaded',
503+
),
504+
},
505+
};
506+
507+
const handleContinue = async (): Promise<void> => {
508+
if (!enterpriseConnection || !canSubmit) {
509+
return;
510+
}
511+
512+
card.setError(undefined);
513+
card.setLoading();
514+
515+
try {
516+
const saml = await buildSamlConfigurationPayload({
517+
mode,
518+
metadataUrl: { value: metadataUrlField.value },
519+
manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certFile },
520+
});
521+
522+
await updateEnterpriseConnection(enterpriseConnection.id, { saml });
523+
void goNext();
524+
} catch (err) {
525+
if (mode === 'metadataUrl') {
526+
applySamlSubmitError(err, card, metadataUrlField);
527+
} else {
528+
applySamlSubmitError(err, card, signOnUrlField, [issuerField, certificateField]);
529+
}
530+
} finally {
531+
card.setIdle();
532+
}
533+
};
534+
535+
return (
536+
<>
537+
<Step.Body>
538+
<Step.Section
539+
fill
540+
gap={5}
541+
>
542+
<Heading
543+
elementDescriptor={descriptors.configureSSOInstructionsHeading}
544+
as='h3'
545+
textVariant='subtitle'
546+
localizationKey={localizationKeys(
547+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.modes.title',
548+
)}
549+
/>
550+
<IdentityProviderConfigurationModes
551+
modes={MICROSOFT_SAML_IDP_MODES}
552+
value={mode}
553+
onChange={next => {
554+
card.setError(undefined);
555+
setMode(next);
556+
}}
557+
labels={{
558+
ariaLabel: localizationKeys(
559+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.modes.ariaLabel',
560+
),
561+
metadataUrl: localizationKeys(
562+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.modes.metadataUrl',
563+
),
564+
manual: localizationKeys(
565+
'configureSSO.configureStep.samlMicrosoft.identityProviderMetadataStep.modes.manual',
566+
),
567+
}}
568+
/>
569+
<IdentityProviderConfigurationForm {...formProps} />
570+
</Step.Section>
571+
</Step.Body>
572+
573+
<Step.Footer>
574+
<Step.Footer.Previous
575+
onClick={() => goPrev()}
576+
isDisabled={isFirstStep || card.isLoading}
577+
/>
578+
<Step.Footer.Continue
579+
onClick={handleContinue}
580+
isLoading={card.isLoading}
581+
isDisabled={!canSubmit}
582+
/>
583+
</Step.Footer>
584+
</>
585+
);
586+
};

0 commit comments

Comments
 (0)