Initial Project Commit

This commit is contained in:
Orlando M Guerreiro 2025-05-22 19:23:40 +01:00
commit a6dea9c888
2148 changed files with 173870 additions and 0 deletions

View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDyxkb2UrPwNyh4
LFdNhmvP/wNUNnymDfZbUsigT3Klsw1fM15V21g89C6pqFS2fP8R9bHDK5Q9xGdj
1EpdmHZzrbuY3x4MadIThuUgTuRrdNl/TzeRAy/UeAHwWJhtc+XQomhwZp5sLOcm
yEe8MifopyyVcVGn48wDXE+da2VxBsidE4fGyNYO+VnL/37n1FdVPR8FiLgWWYq2
EbWT5Vg7N6JRaV5VIUPcpnHrITG64+Xo9wQYCx8PqmcEe0RHK/8J4SvsNkVqADSC
kCg4wsg5AK7xbcXzPcxdwmnikLWT1HJy6MiPCqrmKW9rcv4rlxNq7etU5VeEMLr4
QSe/wgmnAgMBAAECggEAQWB8vpuh4jfwWYBTWEixIteBHX34zjznUUt3RJhwfse7
e54ZMtS5K9zz7fMrMONzSvJXlv/W0VVhJESIbDEBAQDRiobXEC+1B1YlwLAOGhPi
+EIsbAwoJrbUitVI4vy5cBg0OMSht+7Vpp97leYJ0kCmpG3aN/SDvYnv1KwVqrxL
JtODOyCuBlttIH6uo1e6aU6HKWTwRDp177RSLuYOjBmQi6Hxwa+w31zQ6Q8Scubb
S0z61C3L8Azdrb8B4/LfoARIDhy/gVvq7W3gIRkUZCl9oFBehpV+TT1BFPVGiKdm
3vasOeo7BEORglbj+IaCovBooIGsUkDA1aZFCVx/3QKBgQD3e+owizU9Jp1j+8vl
FtzHmbjBrNFIy4wiiMN0L1cKQzzVig7vb1JIwbK+uDg9csulObsdtva1FU4wFJOk
R8JULJ0y4DRY1piT2p2bQA16IvSLvMrUlZ5KT1TqimsWxmkiodlBm3UM+q7rivca
hSftmmayuNcjHj9FWKBM/6J/vQKBgQD7IOBRv2OgHcibQ5LdGX8TQqw55XKAmiEq
nQI4Jk1XsTwdsdd+aqo81gp6Fdk3NdXM2x+kMDsP6IziaXyubI4w8Kkj6YJTL59p
V3PACdfqToEkTblKdYVE7yN6gsuOT4qi0aedSAsRmeQpb8GMHjalgc5pv4Y34W8+
E4LY4nbjMwKBgQCcLZjW1aLdWlcM18QOaGUfmUTdBEB2ne1rhb9CvPVCxrfHUn6m
XywgOgyhCwSC0sTtGgeZcvMxx6Y19WZOz/I0yIrTpmWigpp7BAVeCgf3QcPtw1CE
436nCnVeJcf68W87qcO/AWnWrQRiJKpYFBvkeAHDW554zQfErW9L6C8WSQKBgAoF
Ms4wO9Jsvc9sL9UAqnBjTan1vM7i14Xyw97nsFhaaxKoQPf7W5WX2M0sSAGK9V/6
MlYD0qd82PpDyUTQchAD2kvjil61XMAATE8SVXo07bQ8IbOV4t5wSFMgGu0vwVFj
2jNNZ5upL1Bz9B4aKoYKGulfSgS6ywyIDMWIq8O/AoGAC1Ax4BQyB4/Jp2+oWtKF
tkTdYPR3kmZtrtwhhYVDC+zXl/zSRlmRLNaHUpRyND/JRm/ngsYFrM1ZiRuiLTR+
aYbKwy1AJh0HU2heH/5AL+mDbOkvHPkxiKRA+b58mUclMpll5x2l7sZ2hBg/Qspw
6NjlEMHxyqq9Viz4DNxbQ20=
-----END PRIVATE KEY-----

View file

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUJGNdh9cxePuEzWTUlEuSOkwfwyUwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDQyNDA5MjI1MFoXDTI2MDQy
NDA5MjI1MFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEA8sZG9lKz8DcoeCxXTYZrz/8DVDZ8pg32W1LIoE9ypbMN
XzNeVdtYPPQuqahUtnz/EfWxwyuUPcRnY9RKXZh2c627mN8eDGnSE4blIE7ka3TZ
f083kQMv1HgB8FiYbXPl0KJocGaebCznJshHvDIn6KcslXFRp+PMA1xPnWtlcQbI
nROHxsjWDvlZy/9+59RXVT0fBYi4FlmKthG1k+VYOzeiUWleVSFD3KZx6yExuuPl
6PcEGAsfD6pnBHtERyv/CeEr7DZFagA0gpAoOMLIOQCu8W3F8z3MXcJp4pC1k9Ry
cujIjwqq5ilva3L+K5cTau3rVOVXhDC6+EEnv8IJpwIDAQABo1MwUTAdBgNVHQ4E
FgQURDfHxj6ACRLLPcVRTgvlwhJSnyAwHwYDVR0jBBgwFoAURDfHxj6ACRLLPcVR
TgvlwhJSnyAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABioO
OCok/9JigKTupTEnJLxgueSIqk0lWXa7E6zwHLJh+DUUSlS1kECL2+s+HlGz7oCj
uiemCLMssTd0ZOn95GN5b4IbEl8r+12hVaREKDC7qlydkFnKKomvEVQJUbAKPgqv
EhNmJsp9TPKQzCdwTz00g0mmZCtc4cJoXEiOR+TKf/AwxXbh8/++9big1hIAnpHC
zQgfebBg+I+1FPuUsqC6+zp4DrAflpQ2q0/4SNGyaPumf2K3ZjxLd9MPi+acd/HI
USLnLV8fq77N+WxIqo20Az16biPxKL5jCq+NDqy0nF/J7ITBhMMCoYhPVxUhfjZ5
7XLrSTjTVq7Mi1c5IQ==
-----END CERTIFICATE-----

View file

@ -0,0 +1,3 @@
To create the idp-private.key and idp-public.cert:
> openssl req -x509 -newkey rsa:2048 -keyout idp-private.key -out idp-public.cert -days 365 -nodes -subj "/CN=localhost"

View file

@ -0,0 +1,315 @@
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 apps 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(`
<html>
<body onload="document.forms[0].submit()">
<form method="POST" action="${acsUrl}">
<input type="hidden" name="SAMLResponse" value="${samlResponse.replace(/"/g, '&quot;')}" />
<input type="hidden" name="RelayState" value="${relayState.replace(/"/g, '&quot;')}" />
</form>
</body>
</html>
`);
});
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 = `
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="${assertionId}" IssueInstant="${now}" Version="2.0">
<saml:Issuer>${issuer}</saml:Issuer>
<saml:Subject>
<saml:NameID>${nameId}</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="${fiveMinutesLater}" Recipient="${recipient}" />
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="${now}" NotOnOrAfter="${fiveMinutesLater}">
<saml:AudienceRestriction>
<saml:Audience>${audience}</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="${now}">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="email">
<saml:AttributeValue>${email}</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="organization_code">
<saml:AttributeValue>${orgCode}</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="security_group">
<saml:AttributeValue>${securityGroupCode}</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="roles">
<saml:AttributeValue>${roles}</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>`;
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: () => `<ds:X509Data><ds:X509Certificate>${certBase64}</ds:X509Certificate></ds:X509Data>`
};
sig.computeSignature(assertion, {
prefix: 'ds',
location: { reference: "//*[local-name(.)='Issuer']", action: 'after' }
});
const signedAssertion = sig.getSignedXml();
const samlResponse = `<?xml version="1.0" encoding="UTF-8"?><samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="${responseId}" Version="2.0" IssueInstant="${now}" Destination="${destination}"><saml:Issuer>${issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>${signedAssertion}</samlp:Response>`;
const samlResponseToUse = samlResponse.trim().replace(/^\uFEFF/, '');
return Buffer.from(samlResponseToUse, 'utf-8').toString('base64');
}
function createLoginFormOLD() {
const form = `
<html>
<body>
<h2>Mock Login</h2>
<form method="POST" action="/saml/login">
Username: <input name="username" value="test-user"/><br/>
Email: <input name="email" value="test@example.com"/><br/>
Org Code: <input name="orgCode" value="NOVA"/><br/>
Security Group: <input name="securityGroup" value="GRP_USER"/><br/>
Roles: <select name="roles" multiple size="4">
<option value="ROLE_USER">ROLE_USER</option>
<option value="ROLE_COORDINATOR">ROLE_COORDINATOR</option>
<option value="ROLE_MANAGER">ROLE_MANAGER</option>
<option value="ROLE_ADMIN">ROLE_ADMIN</option>
</select><br/><br/>
<button type="submit">Login</button>
</form>
</body>
</html>
`;
return form;
}
function createLoginForm() {
const form = `
<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.form-container {
background: #fff;
padding: 30px 40px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
width: 400px;
}
h2 {
text-align: center;
margin-bottom: 20px;
}
label {
display: block;
margin: 10px 0 5px;
font-weight: bold;
}
input[type="text"],
input[type="email"],
select {
width: 100%;
padding: 8px;
box-sizing: border-box;
border-radius: 4px;
border: 1px solid #ccc;
}
button {
margin-top: 20px;
width: 100%;
padding: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="form-container">
<h2>Mock Login</h2>
<form method="POST" action="/saml/login">
<label for="username">Username</label>
<input id="username" name="username" value="test-user" type="text"/>
<label for="email">Email</label>
<input id="email" name="email" value="test@example.com" type="email"/>
<label for="orgCode">Org Code</label>
<input id="orgCode" name="orgCode" value="NOVA" type="text"/>
<label for="securityGroup">Security Group</label>
<input id="securityGroup" name="securityGroup" value="GRP_USER" type="text"/>
<label for="roles">Roles</label>
<select id="roles" name="roles" multiple size="4">
<option value="ROLE_USER">ROLE_USER</option>
<option value="ROLE_COORDINATOR">ROLE_COORDINATOR</option>
<option value="ROLE_MANAGER">ROLE_MANAGER</option>
<option value="ROLE_ADMIN">ROLE_ADMIN</option>
</select>
<button type="submit">Login</button>
</form>
</div>
</body>
</html>
`;
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}`);
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,20 @@
{
"name": "mock-idp2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^2.2.0",
"express": "^5.1.0",
"express-session": "^1.18.1",
"saml2-js": "^4.0.3",
"xml2js": "^0.6.2",
"zlib": "^1.0.5"
}
}

View file

@ -0,0 +1,14 @@
/* ***************************************************** */
/* To run : > node idp.js */
/* > Mock IDP running at http://localhost:3000 */
/* Starts an IDP server, allowing SAMLv2 auth testing */
/* ***************************************************** */
mock-idp\certs
Generated selfsigned cert for testing
* idp-private.key - Used for signing the SAML XML Response
* idp-public.cert - Included in the SAML XML Response
* openssl_command.txt - A file containing the command used to generate this certificate files
idp.js
The mock idp developed in JS