fix(auth): Improve error management with sso + fix microsoft saml (#9799)

Fix #9760 #9758
This commit is contained in:
Antoine Moreaux
2025-01-24 10:36:18 +01:00
committed by GitHub
parent 3c85516f77
commit 5783c41df2
49 changed files with 505 additions and 309 deletions

View File

@ -62,12 +62,12 @@ export const SettingsSSOSAMLForm = () => {
if (isDefined(e.target.files)) {
const text = await e.target.files[0].text();
const samlMetadataParsed = parseSAMLMetadataFromXMLFile(text);
e.target.value = '';
if (!samlMetadataParsed.success) {
enqueueSnackBar('Invalid File', {
return enqueueSnackBar('Invalid File', {
variant: SnackBarVariant.Error,
duration: 2000,
});
return;
}
setValue('ssoURL', samlMetadataParsed.data.ssoUrl);
setValue('certificate', samlMetadataParsed.data.certificate);

View File

@ -8,6 +8,29 @@ const validator = z.object({
certificate: z.string().min(1),
});
const getByPrefixAndKey = (
xmlDoc: Document | Element,
key: string,
prefix = 'md',
): Element | undefined => {
return (
xmlDoc.getElementsByTagName(`${prefix}:${key}`)?.[0] ??
xmlDoc.getElementsByTagName(`${key}`)?.[0]
);
};
const getAllByPrefixAndKey = (
xmlDoc: Document | Element,
key: string,
prefix = 'md',
) => {
const withPrefix = xmlDoc.getElementsByTagName(`${prefix}:${key}`);
if (withPrefix.length !== 0) {
return Array.from(withPrefix);
}
return Array.from(xmlDoc.getElementsByTagName(`${key}`));
};
export const parseSAMLMetadataFromXMLFile = (
xmlString: string,
):
@ -20,33 +43,44 @@ export const parseSAMLMetadataFromXMLFile = (
throw new Error('Error parsing XML');
}
const entityDescriptor = xmlDoc.getElementsByTagName(
'md:EntityDescriptor',
)?.[0];
const idpSSODescriptor = xmlDoc.getElementsByTagName(
'md:IDPSSODescriptor',
)?.[0];
const keyDescriptor = xmlDoc.getElementsByTagName('md:KeyDescriptor')[0];
const keyInfo = keyDescriptor?.getElementsByTagName('ds:KeyInfo')[0];
const x509Data = keyInfo?.getElementsByTagName('ds:X509Data')[0];
const x509Certificate = x509Data
?.getElementsByTagName('ds:X509Certificate')?.[0]
.textContent?.trim();
const entityDescriptor = getByPrefixAndKey(xmlDoc, 'EntityDescriptor');
if (!entityDescriptor) throw new Error('No EntityDescriptor found');
const singleSignOnServices = Array.from(
idpSSODescriptor.getElementsByTagName('md:SingleSignOnService'),
).map((service) => ({
Binding: service.getAttribute('Binding'),
Location: service.getAttribute('Location'),
}));
const IDPSSODescriptor = getByPrefixAndKey(xmlDoc, 'IDPSSODescriptor');
if (!IDPSSODescriptor) throw new Error('No IDPSSODescriptor found');
const keyDescriptors = getByPrefixAndKey(IDPSSODescriptor, 'KeyDescriptor');
if (!keyDescriptors) throw new Error('No KeyDescriptor found');
const keyInfo = getByPrefixAndKey(keyDescriptors, 'KeyInfo', 'ds');
if (!keyInfo) throw new Error('No KeyInfo found');
const x509Data = getByPrefixAndKey(keyInfo, 'X509Data', 'ds');
if (!x509Data) throw new Error('No X509Data found');
const x509Certificate = getByPrefixAndKey(
x509Data,
'X509Certificate',
'ds',
)?.textContent?.trim();
if (!x509Certificate) throw new Error('No X509Certificate found');
const singleSignOnServices = getAllByPrefixAndKey(
IDPSSODescriptor,
'SingleSignOnService',
);
const result = {
ssoUrl: singleSignOnServices.find((singleSignOnService) => {
return (
singleSignOnService.Binding ===
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
);
})?.Location,
ssoUrl: singleSignOnServices
.map((service) => ({
Binding: service.getAttribute('Binding'),
Location: service.getAttribute('Location'),
}))
.find(
(singleSignOnService) =>
singleSignOnService.Binding ===
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
)?.Location,
certificate: x509Certificate,
entityID: entityDescriptor?.getAttribute('entityID'),
};

View File

@ -5,17 +5,17 @@ import { z } from 'zod';
export const SSOIdentitiesProvidersOIDCParamsSchema = z
.object({
type: z.literal('OIDC'),
clientID: z.string().optional(),
clientSecret: z.string().optional(),
clientID: z.string().nonempty(),
clientSecret: z.string().nonempty(),
})
.required();
export const SSOIdentitiesProvidersSAMLParamsSchema = z
.object({
type: z.literal('SAML'),
id: z.string().optional(),
ssoURL: z.string().url().optional(),
certificate: z.string().optional(),
id: z.string().nonempty(),
ssoURL: z.string().url().nonempty(),
certificate: z.string().nonempty(),
})
.required();
@ -27,8 +27,8 @@ export const SSOIdentitiesProvidersParamsSchema = z
.and(
z
.object({
name: z.string().min(1),
issuer: z.string().url().optional(),
name: z.string().nonempty(),
issuer: z.string().url().nonempty(),
})
.required(),
);