From 519b450114c0e9e19e9d7234f68650d77b8356e4 Mon Sep 17 00:00:00 2001 From: Mitchell Wise Date: Fri, 12 Jul 2024 14:57:05 -0400 Subject: [PATCH 1/5] IDP metadata trys url first, if failure then try from configs --- src/app.js | 4 +- src/idpMetadata.js | 227 ++++++++++++++++++++++++++------------------- 2 files changed, 135 insertions(+), 96 deletions(-) diff --git a/src/app.js b/src/app.js index 817460a1..7b22ab80 100644 --- a/src/app.js +++ b/src/app.js @@ -72,7 +72,7 @@ const handleMetadata = (argv) => { */ function runServer(argv) { const strategies = new Map(); - IdPMetadata.fetch(argv.spIdpMetaUrl) + IdPMetadata.fetch(argv.spIdpMetaUrl, argv.spIdpMetadata) .then(handleMetadata(argv)) .then(() => { const app = express(); @@ -83,7 +83,7 @@ function runServer(argv) { app.use(passport.initialize()); if (argv.idpSamlLoginsEnabled) { argv.idpSamlLogins.forEach((spIdpEntry) => { - IdPMetadata.fetch(spIdpEntry.spIdpMetaUrl) + IdPMetadata.fetch(spIdpEntry.spIdpMetaUrl, spIdpEntry.spIdpMetadata) .then(handleMetadata(spIdpEntry)) .then(() => { spIdpEntry.spKey = argv.spKey; diff --git a/src/idpMetadata.js b/src/idpMetadata.js index 85dccb13..6d675cb0 100644 --- a/src/idpMetadata.js +++ b/src/idpMetadata.js @@ -1,8 +1,15 @@ "use strict"; - +const fs = require("fs"); const xml2js = require("xml2js"); -const logger = require("./logger"); const axios = require("axios"); + +// XML parser configuration +const parserConfig = { + explicitRoot: true, + explicitCharkey: true, + tagNameProcessors: [xml2js.processors.stripPrefix], +}; + /** * Creates a check to receive the binding location * using serviceEl and the bindingUri @@ -43,118 +50,150 @@ function getFirstCert(keyEl) { } return null; } + +/** + * Function to read and parse XML file + * + * @param {string} filePath Path to the XML file + * @returns {*} Parsed XML object + */ +function parseXML(filePath) { + const parser = new xml2js.Parser(parserConfig); + + return new Promise((resolve, reject) => { + fs.readFile(filePath, "utf8", (err, data) => { + if (err) { + return reject(err); + } + + parser.parseString(data, (err, result) => { + if (err) { + return reject(err); + } + resolve(result); + }); + }); + }); +} + /** * Creates the check for fetching url by requesting the url, * parsing the config parameters, getting the binding location and * parsing the RoleDescriptor metadata * * @param {*} url fetch url + * @param {*} idpMetadataFallback Path to the fallback IDP metadata XML file * @returns {*} returns the RoleDescriptor metadata with parameters sso, slo, nameIdFormat and signingKeys */ -export function fetch(url) { +export function fetch(url, idpMetadataFallback) { return new Promise((resolve, reject) => { const metadata = { sso: {}, slo: {}, nameIdFormats: [], signingKeys: [] }; - if (typeof url === "undefined" || url === null) { - return resolve(metadata); - } - axios .get(url) .then((response) => { const responseData = response.data; - { - const parserConfig = { - explicitRoot: true, - explicitCharkey: true, - tagNameProcessors: [xml2js.processors.stripPrefix], - }, - parser = new xml2js.Parser(parserConfig); - - parser.parseString(responseData, (err, docEl) => { - if (err) { - return reject(err); - } + const parser = new xml2js.Parser(parserConfig); - if (docEl.EntityDescriptor) { - metadata.issuer = docEl.EntityDescriptor.$.entityID; - - if ( - docEl.EntityDescriptor.IDPSSODescriptor && - docEl.EntityDescriptor.IDPSSODescriptor.length === 1 - ) { - metadata.protocol = "samlp"; - let ssoEl = docEl.EntityDescriptor.IDPSSODescriptor[0]; - metadata.signRequest = ssoEl.$.WantAuthnRequestsSigned; - - ssoEl.KeyDescriptor.forEach((keyEl) => { - if ( - keyEl.$.use && - keyEl.$.use.toLowerCase() !== "encryption" - ) { - const signingKey = {}; - signingKey.cert = getFirstCert(keyEl); - if (keyEl.$.active && keyEl.$.active === "true") { - signingKey.active = true; - } - metadata.signingKeys.push(signingKey); - } - }); - - if (ssoEl.NameIDFormat) { - ssoEl.NameIDFormat.forEach((element) => { - if (element._) { - metadata.nameIdFormats.push(element._); - } - }); - } - - metadata.sso.redirectUrl = getBindingLocation( - ssoEl.SingleSignOnService, - "urn:oasis:names:tc:saml:2.0:bindings:http-redirect" - ); - metadata.sso.postUrl = getBindingLocation( - ssoEl.SingleSignOnService, - "urn:oasis:names:tc:saml:2.0:bindings:http-post" - ); - - metadata.slo.redirectUrl = getBindingLocation( - ssoEl.SingleLogoutService, - "urn:oasis:names:tc:saml:2.0:bindings:http-redirect" - ); - metadata.slo.postUrl = getBindingLocation( - ssoEl.SingleLogoutService, - "urn:oasis:names:tc:saml:2.0:bindings:http-post" - ); - } - } + parser.parseString(responseData, (err, docEl) => { + if (err || !docEl) { + // Error parsing response data, fallback to idpMetadataFallback + parseXML(idpMetadataFallback) + .then((xmlData) => processMetadata(xmlData, metadata)) + .then(resolve) + .catch(reject); + } else { + processMetadata(docEl, metadata).then(resolve).catch(reject); + } + }); + }) + .catch(() => { + // Error fetching metadata from URL, fallback to idpMetadataFallback + parseXML(idpMetadataFallback) + .then((xmlData) => processMetadata(xmlData, metadata)) + .then(resolve) + .catch(reject); + }); + }); +} + +/** + * Process IDP metadata from XML data + * + * @param {*} docEl Parsed XML object + * @param {*} metadata Metadata object to populate + * @returns {*} RoleDescriptor metadata with parameters sso, slo, nameIdFormat, and signingKeys + */ +async function processMetadata(docEl, metadata) { + return new Promise((resolve, reject) => { + try { + if (docEl.EntityDescriptor) { + metadata.issuer = docEl.EntityDescriptor.$.entityID; + + if ( + docEl.EntityDescriptor.IDPSSODescriptor && + docEl.EntityDescriptor.IDPSSODescriptor.length === 1 + ) { + metadata.protocol = "samlp"; + let ssoEl = docEl.EntityDescriptor.IDPSSODescriptor[0]; + metadata.signRequest = ssoEl.$.WantAuthnRequestsSigned; - if (docEl.EntityDescriptor.RoleDescriptor) { - metadata.protocol = "wsfed"; - try { - let roleEl = docEl.EntityDescriptor.RoleDescriptor.find( - (el) => { - return el.$["xsi:type"].endsWith( - ":SecurityTokenServiceType" - ); - } - ); - metadata.sso.redirectUrl = - roleEl.PassiveRequestorEndpoint[0].EndpointReference[0].Address[0]._; - - roleEl.KeyDescriptor.forEach((keyEl) => { - metadata.signingKeys.push(getFirstCert(keyEl)); - }); - } catch (e) { - logger.error("unable to parse RoleDescriptor metadata", e); + ssoEl.KeyDescriptor.forEach((keyEl) => { + if (keyEl.$.use && keyEl.$.use.toLowerCase() !== "encryption") { + const signingKey = {}; + signingKey.cert = getFirstCert(keyEl); + if (keyEl.$.active && keyEl.$.active === "true") { + signingKey.active = true; } + metadata.signingKeys.push(signingKey); } - return resolve(metadata); }); + + if (ssoEl.NameIDFormat) { + ssoEl.NameIDFormat.forEach((element) => { + if (element._) { + metadata.nameIdFormats.push(element._); + } + }); + } + + metadata.sso.redirectUrl = getBindingLocation( + ssoEl.SingleSignOnService, + "urn:oasis:names:tc:saml:2.0:bindings:http-redirect" + ); + metadata.sso.postUrl = getBindingLocation( + ssoEl.SingleSignOnService, + "urn:oasis:names:tc:saml:2.0:bindings:http-post" + ); + + metadata.slo.redirectUrl = getBindingLocation( + ssoEl.SingleLogoutService, + "urn:oasis:names:tc:saml:2.0:bindings:http-redirect" + ); + metadata.slo.postUrl = getBindingLocation( + ssoEl.SingleLogoutService, + "urn:oasis:names:tc:saml:2.0:bindings:http-post" + ); } - }) - .catch(() => { - logger.error("Error receiving metadata"); - }); + } + + if (docEl.EntityDescriptor.RoleDescriptor) { + metadata.protocol = "wsfed"; + let roleEl = docEl.EntityDescriptor.RoleDescriptor.find((el) => + el.$["xsi:type"].endsWith(":SecurityTokenServiceType") + ); + + metadata.sso.redirectUrl = + roleEl.PassiveRequestorEndpoint[0].EndpointReference[0].Address[0]._; + + roleEl.KeyDescriptor.forEach((keyEl) => { + metadata.signingKeys.push(getFirstCert(keyEl)); + }); + } + + resolve(metadata); + } catch (error) { + reject(error); + } }); } From 89e70391cb5ebc28caaeb27bd5ec4f065a79f99a Mon Sep 17 00:00:00 2001 From: Mitchell Wise Date: Fri, 12 Jul 2024 15:02:22 -0400 Subject: [PATCH 2/5] jsdoc update --- src/idpMetadata.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/idpMetadata.js b/src/idpMetadata.js index 6d675cb0..09ee4bbc 100644 --- a/src/idpMetadata.js +++ b/src/idpMetadata.js @@ -52,7 +52,7 @@ function getFirstCert(keyEl) { } /** - * Function to read and parse XML file + * Read and parse XML file * * @param {string} filePath Path to the XML file * @returns {*} Parsed XML object @@ -118,7 +118,7 @@ export function fetch(url, idpMetadataFallback) { } /** - * Process IDP metadata from XML data + * Process IDP XML metadata * * @param {*} docEl Parsed XML object * @param {*} metadata Metadata object to populate From a4118bfb9e5163052c9c32a2cbf4fa610766a88c Mon Sep 17 00:00:00 2001 From: Mitchell Wise Date: Fri, 12 Jul 2024 15:41:05 -0400 Subject: [PATCH 3/5] dev-config.base update --- dev-config.base.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev-config.base.json b/dev-config.base.json index aa37159d..60692467 100644 --- a/dev-config.base.json +++ b/dev-config.base.json @@ -4,6 +4,7 @@ "idpAudience": "https://idp.example.com/audience", "idpBaseUrl": "https://idp.example.com/base/url", "spIdpMetaUrl": "https://sp.example.com/idp/meta/url", + "spIdpMetadata": "./spIdpMetadataExample.xml", "spNameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "spAudience": "sp-audience.example.com", "spIdpIssuer": "sp-idp-issuer.example.com", @@ -33,6 +34,7 @@ { "category": "example2SamlIdp", "spIdpMetaUrl": "https://saml-idp.example2.com/metadata", + "spIdpMetadata": "./spIdpMetadataExample2.xml", "spNameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "spAudience": "https://saml-idp.example2.com", "spAuthnContextClassRef": "http://sp.example2.com/authn/context/class/ref", From 75fcea2105c000c2a76a92e3d6554b419fd7aa32 Mon Sep 17 00:00:00 2001 From: Mitchell Wise Date: Fri, 12 Jul 2024 16:14:27 -0400 Subject: [PATCH 4/5] logging errors when url get fails, or parsing errors occur --- src/idpMetadata.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/idpMetadata.js b/src/idpMetadata.js index 09ee4bbc..e71477ab 100644 --- a/src/idpMetadata.js +++ b/src/idpMetadata.js @@ -97,7 +97,9 @@ export function fetch(url, idpMetadataFallback) { parser.parseString(responseData, (err, docEl) => { if (err || !docEl) { - // Error parsing response data, fallback to idpMetadataFallback + console.error( + "Error parsing response data, fallback to idpMetadataFallback" + ); parseXML(idpMetadataFallback) .then((xmlData) => processMetadata(xmlData, metadata)) .then(resolve) @@ -108,7 +110,9 @@ export function fetch(url, idpMetadataFallback) { }); }) .catch(() => { - // Error fetching metadata from URL, fallback to idpMetadataFallback + console.error( + "Error fetching metadata from URL, fallback to idpMetadataFallback" + ); parseXML(idpMetadataFallback) .then((xmlData) => processMetadata(xmlData, metadata)) .then(resolve) From 8c94de5bf97209e0fd088ccfad374863c4eea1ca Mon Sep 17 00:00:00 2001 From: Mitchell Wise Date: Thu, 18 Jul 2024 12:24:35 -0400 Subject: [PATCH 5/5] index spIdpMetadata additions --- src/cli/index.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/cli/index.js b/src/cli/index.js index 704da48d..d6f3c573 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -230,6 +230,11 @@ export function processArgs() { required: false, string: true, }, + spIdpMetadata: { + description: "IdP SAML Metadata URL", + required: false, + string: true, + }, spAudience: { description: "SP Audience URI / RP Realm", required: false, @@ -477,6 +482,11 @@ export function processArgs() { required: false, string: true, }, + spIdpMetadata: { + description: "IdP SAML Metadata URL", + required: false, + string: true, + }, spAudience: { description: "SP Audience URI / RP Realm", required: false,