Added custom InMemory SAML2 Authentication Request Repository

Added attributes mapping in application*.yml config
Added attributes default values in application*.yml config
Corrected MOCK-IDP to return InResponseTo and complex attribute names
This commit is contained in:
Orlando M Guerreiro 2025-06-03 08:16:24 +01:00
parent f548a0b31e
commit eb3a621b17
7 changed files with 458 additions and 75 deletions

View file

@ -132,6 +132,91 @@ function createFakeSamlResponse(nameId, email, orgCode, securityGroupCode, roles
console.log('SAML Destination :', destination);
console.log('SAML Issuer :', issuer);
const assertion = `
<saml:Assertion
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
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:Attribute Name="urn:mace:dir:attribute-def:displayName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xsi:type="xs:string">Pedro Reis</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="urn:oid:2.16.840.1.113730.3.1.241" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xsi:type="xs:string">Pedro Reis</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="urn:mace:dir:attribute-def:mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xsi:type="xs:string">pedro.reis@unl.pt</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xsi:type="xs:string">pedro.reis@unl.pt</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}" InResponseTo="${inResponseTo}"><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');
}
// OLD assertion string. This contains also some extra attributes
function assertionOLD() {
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>
@ -166,35 +251,6 @@ function createFakeSamlResponse(nameId, email, orgCode, securityGroupCode, roles
</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}" InResponseTo="${inResponseTo}"><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() {
@ -350,5 +406,11 @@ function extractIssuerFromSAMLRequest(samlRequestBase64) {
}
app.listen(port, () => {
const now = new Date();
const localISOTime = now.toLocaleString('pt-PT'); // ISO-like, local time
const nowUTC = new Date().toISOString();
console.log(`Mock IDP running at http://localhost:${port}`);
console.log(`Started at ${localISOTime} (UTC is ${now})`);
});