resilient/src/main/resources/mock-idp/idp.js

315 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}`);
});