diff --git a/src/main/resources/mock-idp/idp.js b/src/main/resources/mock-idp/idp.js index 1501495..23b908b 100644 --- a/src/main/resources/mock-idp/idp.js +++ b/src/main/resources/mock-idp/idp.js @@ -7,6 +7,7 @@ const path = require('path'); const bodyParser = require('body-parser'); const zlib = require('zlib'); const xml2js = require('xml2js'); +const xpath = require('xpath'); const app = express(); const port = 3000; @@ -34,12 +35,15 @@ app.get('/saml/sso', async (req, res) => { storedSamlRequest = samlRequest; storedRelayState = relayState; + const authnRequestId = await extractAuthnRequestId(storedSamlRequest); + console.log('SAML2 AuthnRequestId:', authnRequestId); + // Show simple login form res.send(createLoginForm()); }); // Step 2: Handle login form and send SAMLResponse -app.post('/saml/login', (req, res) => { +app.post('/saml/login', async (req, res) => { const { username, email, orgCode, securityGroup, roles } = req.body; if (!storedSamlRequest) { return res.status(400).send('No SAMLRequest stored'); @@ -60,7 +64,8 @@ app.post('/saml/login', (req, res) => { console.log('ServiceProvider URL :', serviceProviderUrl); console.log('ServiceProvider ACS :', acsUrl); - const samlResponse = createFakeSamlResponse(username, email, orgCode, securityGroup, roles, serviceProviderUrl, issuerUrl); + const inResponseTo = await extractAuthnRequestId(storedSamlRequest); + const samlResponse = createFakeSamlResponse(username, email, orgCode, securityGroup, roles, serviceProviderUrl, issuerUrl, inResponseTo); const relayState = storedRelayState; res.send(` @@ -75,7 +80,41 @@ app.post('/saml/login', (req, res) => { `); }); -function createFakeSamlResponse(nameId, email, orgCode, securityGroupCode, roles, serviceProviderUrl, issuerUrl) { +/** + * Extracts the AuthnRequest ID from a base64-encoded SAMLRequest. + * @param {string} samlRequest - Base64-encoded SAMLRequest parameter from the URL. + * @returns {Promise} - Promise resolving to the AuthnRequest ID, or null if not found. + */ +function extractAuthnRequestId(samlRequest) { + return new Promise((resolve, reject) => { + try { + const decoded = Buffer.from(samlRequest, 'base64'); + + zlib.inflateRaw(decoded, (err, inflated) => { + if (err) return reject(new Error('Failed to inflate SAMLRequest: ' + err.message)); + + const xml = inflated.toString('utf8'); + const doc = new DOMParser().parseFromString(xml, 'text/xml'); + + // Define namespace mappings for XPath + const select = xpath.useNamespaces({ + saml2p: 'urn:oasis:names:tc:SAML:2.0:protocol', + }); + + const node = select('//saml2p:AuthnRequest', doc)[0]; + if (node) { + resolve(node.getAttribute('ID')); + } else { + resolve(null); + } + }); + } catch (error) { + reject(error); + } + }); +} + +function createFakeSamlResponse(nameId, email, orgCode, securityGroupCode, roles, serviceProviderUrl, issuerUrl, inResponseTo) { const issuer = issuerUrl; // const audience = "https://localhost:8443/saml2/service-provider-metadata/mock-idp"; const audience = serviceProviderUrl + "/saml2/service-provider-metadata/mock-idp"; @@ -152,7 +191,7 @@ function createFakeSamlResponse(nameId, email, orgCode, securityGroupCode, roles }); const signedAssertion = sig.getSignedXml(); - const samlResponse = `${issuer}${signedAssertion}`; + const samlResponse = `${issuer}${signedAssertion}`; const samlResponseToUse = samlResponse.trim().replace(/^\uFEFF/, ''); return Buffer.from(samlResponseToUse, 'utf-8').toString('base64');