Initial Project Commit
This commit is contained in:
commit
a6dea9c888
2148 changed files with 173870 additions and 0 deletions
315
src/main/resources/mock-idp/idp.js
Normal file
315
src/main/resources/mock-idp/idp.js
Normal 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 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(`
|
||||
<html>
|
||||
<body onload="document.forms[0].submit()">
|
||||
<form method="POST" action="${acsUrl}">
|
||||
<input type="hidden" name="SAMLResponse" value="${samlResponse.replace(/"/g, '"')}" />
|
||||
<input type="hidden" name="RelayState" value="${relayState.replace(/"/g, '"')}" />
|
||||
</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}`);
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue