refactor(security): extend SAML prefix handling (#10047)

Revised parsing logic to handle multiple XML prefixes for SAML metadata,
improving flexibility in handling diverse metadata structures. Added
corresponding test case to ensure robustness of the implementation.
This commit is contained in:
Antoine Moreaux
2025-02-05 21:46:38 +01:00
committed by GitHub
parent 700eb2d473
commit f6ce27b61e
2 changed files with 43 additions and 9 deletions

View File

@ -28,6 +28,31 @@ describe('parseSAMLMetadataFromXMLFile', () => {
},
});
});
it('should parse SAML metadata from XML file with prefix', () => {
const xmlString = `<?xml version="1.0" encoding="UTF-8"?><ns0:EntityDescriptor xmlns:ns0="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://test.com" validUntil="2026-02-04T17:46:23.000Z">
<ns0:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<ns0:KeyDescriptor use="signing">
<ns2:KeyInfo xmlns:ns2="http://www.w3.org/2000/09/xmldsig#">
<ns2:X509Data>
<ns2:X509Certificate>test</ns2:X509Certificate>
</ns2:X509Data>
</ns2:KeyInfo>
</ns0:KeyDescriptor>
<ns0:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</ns0:NameIDFormat>
<ns0:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://test.com"/>
<ns0:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://test.com"/>
</ns0:IDPSSODescriptor>
</ns0:EntityDescriptor>`;
const result = parseSAMLMetadataFromXMLFile(xmlString);
expect(result).toEqual({
success: true,
data: {
entityID: 'https://test.com',
ssoUrl: 'https://test.com',
certificate: 'test',
},
});
});
it('should return error if XML is invalid', () => {
const xmlString = 'invalid xml';
const result = parseSAMLMetadataFromXMLFile(xmlString);

View File

@ -8,26 +8,36 @@ const validator = z.object({
certificate: z.string().min(1),
});
const allPrefix = ['md', 'ns0', 'ns2', 'dsig', 'ds'];
const getByPrefixAndKey = (
xmlDoc: Document | Element,
key: string,
prefix = 'md',
prefixList = [...allPrefix],
): Element | undefined => {
if (prefixList.length === 0) return undefined;
return (
xmlDoc.getElementsByTagName(`${prefix}:${key}`)?.[0] ??
xmlDoc.getElementsByTagName(`${key}`)?.[0]
xmlDoc.getElementsByTagName(`${prefixList[0]}:${key}`)?.[0] ??
getByPrefixAndKey(xmlDoc, key, prefixList.slice(1)) ??
xmlDoc.getElementsByTagName(key)?.[0]
);
};
const getAllByPrefixAndKey = (
xmlDoc: Document | Element,
key: string,
prefix = 'md',
) => {
const withPrefix = xmlDoc.getElementsByTagName(`${prefix}:${key}`);
prefixList = [...allPrefix],
): Array<Element> => {
const withPrefix = xmlDoc.getElementsByTagName(`${prefixList[0]}:${key}`);
if (withPrefix.length !== 0) {
return Array.from(withPrefix);
}
if (prefixList.length > 0) {
return getAllByPrefixAndKey(xmlDoc, key, prefixList.slice(1));
}
return Array.from(xmlDoc.getElementsByTagName(`${key}`));
};
@ -52,16 +62,15 @@ export const parseSAMLMetadataFromXMLFile = (
const keyDescriptors = getByPrefixAndKey(IDPSSODescriptor, 'KeyDescriptor');
if (!keyDescriptors) throw new Error('No KeyDescriptor found');
const keyInfo = getByPrefixAndKey(keyDescriptors, 'KeyInfo', 'ds');
const keyInfo = getByPrefixAndKey(keyDescriptors, 'KeyInfo');
if (!keyInfo) throw new Error('No KeyInfo found');
const x509Data = getByPrefixAndKey(keyInfo, 'X509Data', 'ds');
const x509Data = getByPrefixAndKey(keyInfo, 'X509Data');
if (!x509Data) throw new Error('No X509Data found');
const x509Certificate = getByPrefixAndKey(
x509Data,
'X509Certificate',
'ds',
)?.textContent?.trim();
if (!x509Certificate) throw new Error('No X509Certificate found');