Skip to content

Commit 56a0934

Browse files
committed
Implement identity provider config for Google
1 parent cd8261d commit 56a0934

2 files changed

Lines changed: 212 additions & 2 deletions

File tree

packages/shared/src/types/elementIds.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export type FieldId =
2828
| 'apiKeySecret'
2929
| 'idpCertificate'
3030
| 'idpEntityId'
31+
| 'idpMetadata'
3132
| 'idpMetadataUrl'
3233
| 'idpSsoUrl'
3334
| 'acsUrl'

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

Lines changed: 211 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
1-
import { type JSX } from 'react';
1+
import React, { type JSX } from 'react';
22

33
import { Col, descriptors, Heading, Text } from '@/customizables';
4+
import { useCardState } from '@/elements/contexts';
45
import { localizationKeys } from '@/localization';
6+
import { useFormControl } from '@/ui/utils/useFormControl';
57

8+
import { useConfigureSSO } from '../../../ConfigureSSOContext';
69
import { Step } from '../../../elements/Step';
710
import { useWizard, Wizard } from '../../../elements/Wizard';
811
import { InnerStepCounter } from '../../../elements/Wizard/InnerStepCounter';
12+
import {
13+
applySamlSubmitError,
14+
buildSamlConfigurationPayload,
15+
IdentityProviderConfigurationForm,
16+
type IdentityProviderConfigurationFormProps,
17+
} from './shared/IdentityProviderConfigurationForm';
18+
import {
19+
IdentityProviderConfigurationModes,
20+
type IdpConfigurationMode,
21+
} from './shared/IdentityProviderConfigurationModes';
922

1023
export const SamlGoogleConfigureSteps = (): JSX.Element => {
1124
return (
@@ -121,6 +134,202 @@ const SamlGoogleCreateAppStep = (): JSX.Element => {
121134
);
122135
};
123136

137+
const GOOGLE_IDP_MODES = ['metadataFile', 'manual'] as const satisfies readonly IdpConfigurationMode[];
138+
124139
const SamlGoogleIdentityProviderMetadataStep = (): JSX.Element => {
125-
return <div>SamlGoogleIdentityProviderMetadataStep</div>;
140+
const card = useCardState();
141+
const { goNext, goPrev, isFirstStep } = useWizard();
142+
const { enterpriseConnection, updateEnterpriseConnection } = useConfigureSSO();
143+
144+
const samlConnection = enterpriseConnection?.samlConnection;
145+
const hasExistingManualConfig = Boolean(
146+
samlConnection?.idpSsoUrl || samlConnection?.idpEntityId || samlConnection?.idpCertificate,
147+
);
148+
const existingCertPresent = Boolean(samlConnection?.idpCertificate);
149+
const existingMetadataPresent = Boolean(samlConnection?.idpMetadata);
150+
151+
const [mode, setMode] = React.useState<IdpConfigurationMode>(hasExistingManualConfig ? 'manual' : 'metadataFile');
152+
const [metadataFile, setMetadataFile] = React.useState<File | null>(null);
153+
const [certFile, setCertFile] = React.useState<File | null>(null);
154+
155+
const metadataFileField = useFormControl('idpMetadata', '', {
156+
type: 'text',
157+
label: localizationKeys('configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.metadataFile.label'),
158+
isRequired: true,
159+
});
160+
161+
const signOnUrlField = useFormControl('idpSsoUrl', samlConnection?.idpSsoUrl ?? '', {
162+
type: 'text',
163+
label: localizationKeys(
164+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.manual.signOnUrl.label',
165+
),
166+
placeholder: localizationKeys(
167+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.manual.signOnUrl.placeholder',
168+
),
169+
isRequired: true,
170+
});
171+
172+
const issuerField = useFormControl('idpEntityId', samlConnection?.idpEntityId ?? '', {
173+
type: 'text',
174+
label: localizationKeys('configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.manual.issuer.label'),
175+
placeholder: localizationKeys(
176+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.manual.issuer.placeholder',
177+
),
178+
isRequired: true,
179+
});
180+
181+
const certificateField = useFormControl('idpCertificate', '', {
182+
type: 'text',
183+
label: localizationKeys(
184+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.manual.signingCertificate.label',
185+
),
186+
isRequired: true,
187+
});
188+
189+
const trimmedSignOnUrl = signOnUrlField.value.trim();
190+
const trimmedIssuer = issuerField.value.trim();
191+
const hasCert = certFile !== null || existingCertPresent;
192+
const hasMetadataFile = metadataFile !== null || existingMetadataPresent;
193+
194+
const isValid =
195+
mode === 'metadataFile' ? hasMetadataFile : trimmedSignOnUrl.length > 0 && trimmedIssuer.length > 0 && hasCert;
196+
197+
const canSubmit = isValid && !card.isLoading;
198+
199+
const formProps: IdentityProviderConfigurationFormProps =
200+
mode === 'metadataFile'
201+
? {
202+
mode: 'metadataFile',
203+
form: {
204+
field: metadataFileField,
205+
file: metadataFile,
206+
onFileChange: setMetadataFile,
207+
existingFilePresent: existingMetadataPresent,
208+
},
209+
labels: {
210+
description: localizationKeys(
211+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.metadataFile.description',
212+
),
213+
uploadFile: localizationKeys(
214+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.metadataFile.uploadFile',
215+
),
216+
replaceFile: localizationKeys(
217+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.metadataFile.replaceFile',
218+
),
219+
removeFile: localizationKeys(
220+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.metadataFile.removeFile',
221+
),
222+
fileUploaded: localizationKeys(
223+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.metadataFile.fileUploaded',
224+
),
225+
},
226+
}
227+
: {
228+
mode: 'manual',
229+
form: {
230+
signOnUrlField,
231+
issuerField,
232+
certificateField,
233+
certFile,
234+
onCertFileChange: setCertFile,
235+
existingCertPresent,
236+
},
237+
labels: {
238+
description: localizationKeys(
239+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.manual.description',
240+
),
241+
uploadFile: localizationKeys(
242+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.manual.signingCertificate.uploadFile',
243+
),
244+
replaceFile: localizationKeys(
245+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.manual.signingCertificate.replaceFile',
246+
),
247+
removeFile: localizationKeys(
248+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.manual.signingCertificate.removeFile',
249+
),
250+
fileUploaded: localizationKeys(
251+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.manual.signingCertificate.fileUploaded',
252+
),
253+
},
254+
};
255+
256+
const handleContinue = async (): Promise<void> => {
257+
if (!enterpriseConnection || !canSubmit) {
258+
return;
259+
}
260+
261+
card.setError(undefined);
262+
card.setLoading();
263+
264+
try {
265+
const saml = await buildSamlConfigurationPayload({
266+
mode,
267+
metadataFile: { file: metadataFile },
268+
manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certFile },
269+
});
270+
271+
await updateEnterpriseConnection(enterpriseConnection.id, { saml });
272+
void goNext();
273+
} catch (err) {
274+
if (mode === 'metadataFile') {
275+
applySamlSubmitError(err, card, metadataFileField);
276+
} else {
277+
applySamlSubmitError(err, card, signOnUrlField, [issuerField, certificateField]);
278+
}
279+
} finally {
280+
card.setIdle();
281+
}
282+
};
283+
284+
return (
285+
<>
286+
<Step.Body>
287+
<Step.Section
288+
fill
289+
gap={5}
290+
>
291+
<Heading
292+
elementDescriptor={descriptors.configureSSOInstructionsHeading}
293+
as='h3'
294+
textVariant='subtitle'
295+
localizationKey={localizationKeys(
296+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.modes.title',
297+
)}
298+
/>
299+
<IdentityProviderConfigurationModes
300+
modes={GOOGLE_IDP_MODES}
301+
value={mode}
302+
onChange={next => {
303+
card.setError(undefined);
304+
setMode(next);
305+
}}
306+
labels={{
307+
ariaLabel: localizationKeys(
308+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.modes.ariaLabel',
309+
),
310+
metadataFile: localizationKeys(
311+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.modes.metadataFile',
312+
),
313+
manual: localizationKeys(
314+
'configureSSO.configureStep.samlGoogle.identityProviderMetadataStep.modes.manual',
315+
),
316+
}}
317+
/>
318+
<IdentityProviderConfigurationForm {...formProps} />
319+
</Step.Section>
320+
</Step.Body>
321+
322+
<Step.Footer>
323+
<Step.Footer.Previous
324+
onClick={() => goPrev()}
325+
isDisabled={isFirstStep || card.isLoading}
326+
/>
327+
<Step.Footer.Continue
328+
onClick={handleContinue}
329+
isLoading={card.isLoading}
330+
isDisabled={!canSubmit}
331+
/>
332+
</Step.Footer>
333+
</>
334+
);
126335
};

0 commit comments

Comments
 (0)