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

@ -144,14 +144,14 @@ resilient:
enabled: false
port: 8081
mock-idp:
enabled: true
enabled: false
path: classpath:mock-idp/idp.js
security:
saml2: # ADDED to support SAMLv2 authentication to IDP.
# Metadata endpoint ${base-url}/saml2/service-provider-metadata/mock-idp
enabled: true
idp-id: unl-idp # The id of the IDP to use. One from the collection in relyingparty.registration
base-url: https://resilient.localhost # old: https://localhost:8443
idp-id: mock-idp # The id of the IDP to use. One from the collection in relyingparty.registration
base-url: http://resilient.localhost # old: https://localhost:8443
success-url: http://resilient.localhost/
failure-url: http://resilient.localhost/login
relyingparty:
@ -163,10 +163,22 @@ resilient:
url: http://mock-idp.localhost/saml/sso # old: http://localhost:3000/saml/sso
# OPTIONAL. A list of query parameters to add to single-sign-on.url. This is usefull for mock-idp, to give instructions on how to behave
query-parameters:
spUrl: https://resilient.localhost # The callback to Service Provider, after IDP authentication (OK | KO). Appends the encoded url: acs=https%3A%2F%2Fresilient.localhost%2Flogin%2Fsaml2%2Fsso%2Fmock-idp
spUrl: http://resilient.localhost # The callback to Service Provider, after IDP authentication (OK | KO). Appends the encoded url: acs=https%3A%2F%2Fresilient.localhost%2Flogin%2Fsaml2%2Fsso%2Fmock-idp
issuerUrl: http://mock-idp.localhost/saml/metadata # The IDP entity-id. This is needed for mock-idp to build saml2 response
single-logout:
url: http://mock-idp.localhost/saml/slo # old: http://localhost:3000/saml/slo
attributes: # This is a mapping between the needed attributes, and the names of the attributes in the SAML2 Response
# Leave BLANK if SAML doesn't provide that attribute'
name: name # the user display name [OPTIONAL]
username: urn:mace:dir:attribute-def:mail # the username, typically for authentication. Fallsback to email. [MANDATORY]
email: email # the user email [MANDATORY]
organization-code: organization_code # organization unit code [OPTIONAL]
security-group-code: security_group # security group code [OPTIONAL]
role: roles # a single role is expected [OPTIONAL]
defaults: # For some attributes defaults can be given. This will be used if SAML2 response doesn't have them
organization-code: NOVA # default organization unit code
security-group-code: GRP_USER # default security group code
role: ROLE_USER # default role
verification:
credentials:
- certificate-location: classpath:saml/idp-public.cert
@ -183,15 +195,28 @@ resilient:
url: http://unl-idp.localhost/saml/sso
# OPTIONAL. A list of query parameters to add to single-sign-on.url. This is usefull for mock-idp, to give instructions on how to behave
query-parameters:
spUrl: https://resilient.localhost # The callback to Service Provider, after IDP authentication (OK | KO). Appends the encoded url: acs=https%3A%2F%2Fresilient.localhost%2Flogin%2Fsaml2%2Fsso%2Fmock-idp
spUrl: http://resilient.localhost # The callback to Service Provider, after IDP authentication (OK | KO). Appends the encoded url: acs=https%3A%2F%2Fresilient.localhost%2Flogin%2Fsaml2%2Fsso%2Fmock-idp
issuerUrl: http://unl-idp.localhost/saml/metadata # The IDP entity-id. This is needed for mock-idp to build saml2 response
single-logout:
url: http://unl-idp.localhost/saml/slo # old: http://localhost:3000/saml/slo
attributes: # This is a mapping between the needed attributes, and the names of the attributes in the SAML2 Response
# Leave BLANK if SAML doesn't provide that attribute'
name: urn:mace:dir:attribute-def:displayName # the user display name [OPTIONAL]
username: urn:mace:dir:attribute-def:mail # the username, typically for authentication. Fallsback to email. [MANDATORY]
email: urn:mace:dir:attribute-def:mail # the user email [MANDATORY]
organization-code: # organization unit code [OPTIONAL]
security-group-code: # security group code [OPTIONAL]
role: # a single role is expected [OPTIONAL]
defaults: # For some attributes defaults can be given. This will be used if SAML2 response doesn't have them
organization-code: NOVA # default organization unit code
security-group-code: GRP_USER # default security group code
role: ROLE_USER # default role
verification:
credentials:
- certificate-location: classpath:saml/idp-public.cert
want-authn-signed: false # Validate signature in entire message response (true-validates/false-doesn't validate)
want-assertion-signed: true # Validate signature in assertions message response (true-validates/false-doesn't validate)
check-in-response-to: false # The UNL IDP doesn't implement this. Must be false.
signing:
credentials:
- private-key-location: classpath:saml/private.key

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