const { SignedXml } = require('xml-crypto'); const { DOMParser } = require('@xmldom/xmldom'); const crypto = require('crypto'); const express = require('express'); const fs = require('fs'); const path = require('path'); const bodyParser = require('body-parser'); const zlib = require('zlib'); const xml2js = require('xml2js'); const app = express(); const port = 3000; // Enable body parsing app.use(bodyParser.urlencoded({ extended: true })); let storedSamlRequest = null; let storedRelayState = null; let storedQueryParameters = null; // Step 1: Receive SAMLRequest and render login form app.get('/saml/sso', async (req, res) => { storedQueryParameters = req.query; console.log('Received query parameters:', req.query); // 👈 log all query parameters const samlRequest = req.query.SAMLRequest; const relayState = req.query.RelayState || ''; if (!samlRequest) { return res.status(400).send('Missing SAMLRequest'); } // Store temporarily storedSamlRequest = samlRequest; storedRelayState = relayState; // Show simple login form res.send(createLoginForm()); }); // Step 2: Handle login form and send SAMLResponse app.post('/saml/login', (req, res) => { const { username, email, orgCode, securityGroup, roles } = req.body; if (!storedSamlRequest) { return res.status(400).send('No SAMLRequest stored'); } // Get the issuer URL (the IDP ID [the application.yml = resilient.security.saml2.relyingparty.registration.mock-idp.assertingparty.entity-id]) const issuerUrlDefault = 'http://localhost:3000/saml/metadata'; const issuerUrl = storedQueryParameters['issuerUrl'] ? storedQueryParameters['issuerUrl'] : issuerUrlDefault; // Get the base url of ServiceProvider (the server APP) const serviceProviderUrlDefault = 'https://localhost:8443'; // Or 'http://localhost:8081' const serviceProviderUrl = storedQueryParameters['spUrl'] ? storedQueryParameters['spUrl'] : serviceProviderUrlDefault; // Build ACS (Assertion Consumer Service) URL — this is the Spring Boot app’s endpoint where the SAML Response is posted back after authentication. const acsUrlPath = '/login/saml2/sso/mock-idp'; const acsUrl = serviceProviderUrl + acsUrlPath; console.log('ServiceProvider URL :', serviceProviderUrl); console.log('ServiceProvider ACS :', acsUrl); const samlResponse = createFakeSamlResponse(username, email, orgCode, securityGroup, roles, serviceProviderUrl, issuerUrl); const relayState = storedRelayState; res.send(`
`); }); function createFakeSamlResponse(nameId, email, orgCode, securityGroupCode, roles, serviceProviderUrl, issuerUrl) { const issuer = issuerUrl; // const audience = "https://localhost:8443/saml2/service-provider-metadata/mock-idp"; const audience = serviceProviderUrl + "/saml2/service-provider-metadata/mock-idp"; const now = new Date().toISOString(); const fiveMinutesLater = new Date(Date.now() + 5 * 60000).toISOString(); const assertionId = `_assertion_${crypto.randomUUID()}`; const responseId = `_response_${crypto.randomUUID()}`; // const recipient = "https://localhost:8443/login/saml2/sso/mock-idp"; const recipient = serviceProviderUrl + "/login/saml2/sso/mock-idp"; // const destination = "https://localhost:8443/login/saml2/sso/mock-idp"; const destination = serviceProviderUrl + "/login/saml2/sso/mock-idp"; console.log('SAML Recipient :', recipient); console.log('SAML Destination :', destination); console.log('SAML Issuer :', issuer); const assertion = ` ${issuer} ${nameId} ${audience} urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport ${email} ${orgCode} ${securityGroupCode} ${roles} `; const privateKey = fs.readFileSync(path.join(__dirname, 'certs', 'idp-private.key'), 'utf8'); const certificate = fs.readFileSync(path.join(__dirname, 'certs', 'idp-public.cert'), 'utf8'); const certBase64 = certificate .replace(/-----BEGIN CERTIFICATE-----/, '') .replace(/-----END CERTIFICATE-----/, '') .replace(/\r?\n|\r/g, ''); const doc = new DOMParser().parseFromString(assertion); const sig = new SignedXml(); sig.prefix = 'ds'; sig.addReference("//*[local-name(.)='Assertion']", [ "http://www.w3.org/2000/09/xmldsig#enveloped-signature", "http://www.w3.org/2001/10/xml-exc-c14n#", ], "http://www.w3.org/2000/09/xmldsig#sha1"); sig.signingKey = privateKey; sig.keyInfoProvider = { getKeyInfo: () => `${certBase64}` }; sig.computeSignature(assertion, { prefix: 'ds', location: { reference: "//*[local-name(.)='Issuer']", action: 'after' } }); const signedAssertion = sig.getSignedXml(); const samlResponse = `${issuer}${signedAssertion}`; const samlResponseToUse = samlResponse.trim().replace(/^\uFEFF/, ''); return Buffer.from(samlResponseToUse, 'utf-8').toString('base64'); } function createLoginFormOLD() { const form = `

Mock Login

Username:
Email:
Org Code:
Security Group:
Roles:

`; return form; } function createLoginForm() { const form = `

Mock Login

`; return form; } // Function to extract Issuer from SAMLRequest (returns a Promise) function extractIssuerFromSAMLRequest(samlRequestBase64) { return new Promise((resolve, reject) => { const samlRequestBuffer = Buffer.from(samlRequestBase64, 'base64'); zlib.inflateRaw(samlRequestBuffer, (err, decoded) => { if (err) { return reject(new Error('Failed to inflate SAMLRequest')); } const xml = decoded.toString('utf8'); xml2js.parseString(xml, { tagNameProcessors: [xml2js.processors.stripPrefix] }, (parseErr, result) => { if (parseErr) { return reject(new Error('Failed to parse SAMLRequest XML')); } // Debugging: log the entire parsed result to see structure // console.log("Parsed XML result:", result); // Extract Issuer from the first element of the array const issuerObject = result?.AuthnRequest?.Issuer?.[0]; // Issuer is an array, so we access the first element if (!issuerObject) { return reject(new Error('Issuer not found in SAMLRequest')); } // Access the text content of the Issuer object (it may be in the '#text' property) const issuer = issuerObject._ || issuerObject['#text']; // Extract the actual string value if (!issuer) { return reject(new Error('Issuer value is missing in the SAMLRequest')); } resolve(issuer); // Return the issuer as a string }); }); }); } app.listen(port, () => { console.log(`Mock IDP running at http://localhost:${port}`); });