From eb3a621b17d5ae6561d9c0f9b53e36aafe4d07aa Mon Sep 17 00:00:00 2001
From: Orlando M Guerreiro
Date: Tue, 3 Jun 2025 08:16:24 +0100
Subject: [PATCH] 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
---
.../config/SecurityConfiguration.java | 8 +
...tSaml2AuthenticationRequestRepository.java | 86 ++++++++++
.../saml2/ResilientSaml2Properties.java | 22 +++
.../saml2/Saml2AuthenticationAttributes.java | 110 +++++++++++++
.../saml2/Saml2AuthenticationHandler.java | 152 +++++++++++++-----
src/main/resources/config/application-dev.yml | 35 +++-
src/main/resources/mock-idp/idp.js | 120 ++++++++++----
7 files changed, 458 insertions(+), 75 deletions(-)
create mode 100644 src/main/java/com/oguerreiro/resilient/repository/security/ResilientSaml2AuthenticationRequestRepository.java
create mode 100644 src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationAttributes.java
diff --git a/src/main/java/com/oguerreiro/resilient/config/SecurityConfiguration.java b/src/main/java/com/oguerreiro/resilient/config/SecurityConfiguration.java
index 4909e58..ce677fb 100644
--- a/src/main/java/com/oguerreiro/resilient/config/SecurityConfiguration.java
+++ b/src/main/java/com/oguerreiro/resilient/config/SecurityConfiguration.java
@@ -18,8 +18,10 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
+import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
@@ -38,6 +40,7 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
+import com.oguerreiro.resilient.repository.security.ResilientSaml2AuthenticationRequestRepository;
import com.oguerreiro.resilient.security.AuthoritiesConstants;
import com.oguerreiro.resilient.security.saml2.Saml2AuthenticationHandler;
import com.oguerreiro.resilient.security.saml2.Saml2ResponseLoggingFilter;
@@ -70,6 +73,11 @@ public class SecurityConfiguration {
return new BCryptPasswordEncoder();
}
+ @Bean
+ Saml2AuthenticationRequestRepository authenticationRequestRepository() {
+ return new ResilientSaml2AuthenticationRequestRepository();
+ }
+
/**
* For future knowledge in SAMLv2 config, its necessary to :
*
diff --git a/src/main/java/com/oguerreiro/resilient/repository/security/ResilientSaml2AuthenticationRequestRepository.java b/src/main/java/com/oguerreiro/resilient/repository/security/ResilientSaml2AuthenticationRequestRepository.java
new file mode 100644
index 0000000..fb5b500
--- /dev/null
+++ b/src/main/java/com/oguerreiro/resilient/repository/security/ResilientSaml2AuthenticationRequestRepository.java
@@ -0,0 +1,86 @@
+package com.oguerreiro.resilient.repository.security;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
+import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ *
+ * This replaces the standard Saml2 in-memory/session request cache. This is actually almost the same, storing the SAML2
+ * requests in a HashMap. I was experiencing a problem that SAML2 Request were being lost between SAML request and
+ * response (probably because of a Angular error that tries user+password login between calls). Nevertheless, the SAML
+ * should work, but didn't.
+ *
+ *
+ * THEN, I tried to implement SAML request persistence in database... WHAT A MESS. SpringSecurity 6.3+ SAML2 really
+ * doesn't allow this. The needed AbstractSaml2AuthenticationRequest instance CAN'T be (re)created nor it can be
+ * serialized. In the end, I'm unable to implement the repository loadAuthenticationRequest() method and return a valid
+ * AbstractSaml2AuthenticationRequest.
+ * Quick more stable solution, a custom in-memory/session request cache.
+ *
+ *
+ * But this has limits. If I need a multi-instance server (load-balancing) this will fail. The proposed solution is a
+ * distributed cache. It will work almost like this, but instead of a in-memory cache per instance, it will be
+ * centralized. When needed, investigate : "centralized distributed cache (Redis, Hazelcast, etc.)"
+ *
+ *
+ * NOTE: This is NOT a bean it self (@Component) because I need to register this in a special way for the SpringSecurity
+ * SAML2 auto-binds. In SecurityConfiguration a bean is declared this way:
+ *
+ *
+ *
+ @Bean
+ Saml2AuthenticationRequestRepository authenticationRequestRepository() {
+ return new ResilientSaml2AuthenticationRequestRepository();
+ }
+ *
+ *
+ *
+ */
+//@Component - KEEP commented
+public class ResilientSaml2AuthenticationRequestRepository
+ implements Saml2AuthenticationRequestRepository {
+
+ // TODO: Add the creation timestamp, to improve security. Like a 5 second time resolver. Between initiation (request) and authentication (response)
+ private Map inMemory = new HashMap();
+
+ public ResilientSaml2AuthenticationRequestRepository() {
+ }
+
+ @Override
+ public AbstractSaml2AuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) {
+ String relayState = request.getParameter("RelayState");
+ if (relayState != null) {
+ AbstractSaml2AuthenticationRequest authRequest = inMemory.get(relayState);
+
+ return authRequest;
+ }
+
+ return null;
+ }
+
+ @Override
+ public void saveAuthenticationRequest(AbstractSaml2AuthenticationRequest authenticationRequest,
+ HttpServletRequest request, HttpServletResponse response) {
+ String relayState = authenticationRequest.getRelayState();
+ if (relayState != null) {
+ inMemory.put(relayState, authenticationRequest);
+ }
+ }
+
+ @Override
+ public AbstractSaml2AuthenticationRequest removeAuthenticationRequest(HttpServletRequest request,
+ HttpServletResponse response) {
+ String relayState = request.getParameter("RelayState");
+ if (relayState != null) {
+ inMemory.remove(relayState);
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/com/oguerreiro/resilient/security/saml2/ResilientSaml2Properties.java b/src/main/java/com/oguerreiro/resilient/security/saml2/ResilientSaml2Properties.java
index 4542fab..205a574 100644
--- a/src/main/java/com/oguerreiro/resilient/security/saml2/ResilientSaml2Properties.java
+++ b/src/main/java/com/oguerreiro/resilient/security/saml2/ResilientSaml2Properties.java
@@ -58,12 +58,34 @@ public class ResilientSaml2Properties {
private boolean wantAuthnSigned;
private boolean wantAssertionSigned;
+ // Attributes
+ private Map attributes;
+
+ // Attributes defaults
+ private Map defaults;
+
// Getters and Setters
+ public Map getDefaults() {
+ return defaults;
+ }
+
+ public void setDefaults(Map defaults) {
+ this.defaults = defaults;
+ }
+
public String getEntityId() {
return entityId;
}
+ public Map getAttributes() {
+ return attributes;
+ }
+
+ public void setAttributes(Map attributes) {
+ this.attributes = attributes;
+ }
+
public void setEntityId(String entityId) {
this.entityId = entityId;
}
diff --git a/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationAttributes.java b/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationAttributes.java
new file mode 100644
index 0000000..1aa4ca5
--- /dev/null
+++ b/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationAttributes.java
@@ -0,0 +1,110 @@
+package com.oguerreiro.resilient.security.saml2;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class Saml2AuthenticationAttributes {
+ public static final String ATTR_NAME = "name";
+ public static final String ATTR_USERNAME = "username";
+ public static final String ATTR_EMAIL = "email";
+ public static final String ATTR_ORGANIZATION_CODE = "organization-code";
+ public static final String ATTR_SECURITY_GROUP_CODE = "security-group-code";
+ public static final String ATTR_ROLE = "role";
+
+ private String name;
+ private String username;
+ private String email;
+ private String organizationCode;
+ private String securityGroupCode;
+ private String role;
+ private Map additional = new HashMap();
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getOrganizationCode() {
+ return organizationCode;
+ }
+
+ public void setOrganizationCode(String organizationCode) {
+ this.organizationCode = organizationCode;
+ }
+
+ public String getSecurityGroupCode() {
+ return securityGroupCode;
+ }
+
+ public void setSecurityGroupCode(String securityGroupCode) {
+ this.securityGroupCode = securityGroupCode;
+ }
+
+ public String getRole() {
+ return role;
+ }
+
+ public void setRole(String role) {
+ this.role = role;
+ }
+
+ public void addAttribute(String key, String value, String defaultValue) {
+ // Apply default value, if needed an present
+ if ((value == null || value.isBlank()) && (defaultValue != null && !defaultValue.isBlank())) {
+ value = defaultValue;
+ }
+
+ switch (key) {
+ case ATTR_NAME: {
+ this.setName(value);
+ break;
+ }
+ case ATTR_USERNAME: {
+ this.setUsername(value);
+ break;
+ }
+ case ATTR_EMAIL: {
+ this.setEmail(value);
+ break;
+ }
+ case ATTR_ORGANIZATION_CODE: {
+ this.setOrganizationCode(value);
+ break;
+ }
+ case ATTR_SECURITY_GROUP_CODE: {
+ this.setSecurityGroupCode(value);
+ break;
+ }
+ case ATTR_ROLE: {
+ this.setRole(value);
+ break;
+ }
+ }
+
+ // Always add. Its a sugar code when using getAttribute(String key)
+ this.additional.put(key, value);
+ }
+
+ public String getAttribute(String key) {
+ return this.additional.get(key);
+ }
+}
diff --git a/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationHandler.java b/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationHandler.java
index 1f31979..6de36c4 100644
--- a/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationHandler.java
+++ b/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationHandler.java
@@ -4,8 +4,12 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.security.core.Authentication;
@@ -20,10 +24,13 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand
import org.springframework.stereotype.Component;
import com.oguerreiro.resilient.domain.Organization;
+import com.oguerreiro.resilient.domain.User;
import com.oguerreiro.resilient.repository.OrganizationRepository;
+import com.oguerreiro.resilient.repository.UserRepository;
import com.oguerreiro.resilient.repository.security.SecurityGroupRepository;
import com.oguerreiro.resilient.security.custom.ResilientUserDetails;
import com.oguerreiro.resilient.security.resillient.SecurityGroup;
+import com.oguerreiro.resilient.security.saml2.ResilientSaml2Properties.Registration;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@@ -31,11 +38,7 @@ import jakarta.servlet.http.HttpServletResponse;
@Component
public class Saml2AuthenticationHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler {
-
- private static final String ATTR_SECURITYGROUP = "security_group";
- private static final String ATTR_EMAIL = "email";
- private static final String ATTR_ORGANIZATION = "organization_code";
- private static final String ATTR_ROLES = "roles";
+ protected final Logger log = LoggerFactory.getLogger(this.getClass());
@Autowired
private Environment environment;
@@ -49,6 +52,9 @@ public class Saml2AuthenticationHandler implements AuthenticationSuccessHandler,
@Autowired
private ResilientSaml2Properties resilientSaml2Properties;
+ @Autowired
+ private UserRepository userRepository;
+
/**
* AuthenticationSuccessHandler implementation
*/
@@ -58,50 +64,87 @@ public class Saml2AuthenticationHandler implements AuthenticationSuccessHandler,
String samlResponse = request.getParameter("SAMLResponse");
String samlXMLResponse = Saml2ResponseLoggingFilter.decodeSamlResponse(samlResponse);
- // Read needed information about user security environment
- Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal();
- String username = principal.getName();
- String email = getPrincipalAttribute(principal, ATTR_EMAIL);
- String securityGroupCode = getPrincipalAttribute(principal, ATTR_SECURITYGROUP);
- String organizationCode = getPrincipalAttribute(principal, ATTR_ORGANIZATION);
- List roles = principal.getAttribute(ATTR_ROLES);
+ // Declarations
Collection extends GrantedAuthority> authorities;
-
- if (roles == null || roles.isEmpty()) {
- authorities = authentication.getAuthorities();
- } else {
- authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
- }
-
Organization userOrganization = null;
SecurityGroup securityGroup = null;
- // Organization exists?
- if (organizationCode == null || organizationCode.isBlank()) {
- // TODO: Make this configurable
- Long defaultOrg = 1L;
- userOrganization = organizationRepository.findById(defaultOrg).orElse(null);
+ // read SAML2 attributes
+ Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal();
+ Saml2AuthenticationAttributes attr = this.readSaml2Attributes(principal);
+
+ // Search user. If the user (by username) is registered in Resilient, load definitions from Resilient and NOT the ones received from SAML2
+ String username = attr.getUsername();
+ if (username == null | username.isBlank()) {
+ // This is NOT allowed. A username must be provided. If from SAML2, most probably, will be an email
+ log.error("The attribute username is expected for SAML2 authentication, but it was NULL or Empty.");
+ this.invalidateLogin(request, response);
+ return;
+ }
+
+ User user = userRepository.findOneByLogin(username).orElse(null);
+ if (user != null) {
+ //@formatter:off
+ // User found. Setup permissions from resilient config
+ authorities = user.getAuthorities().stream()
+ .map(auth -> new SimpleGrantedAuthority(auth.getName()))
+ .collect(Collectors.toList());
+ //@formatter:on
+ securityGroup = user.getSecurityGroup();
+ userOrganization = organizationRepository.findOneByUser(user).orElse(null);
+ if (userOrganization == null) {
+ // This is NOT allowed. Bad user configuration.
+ log.error(
+ "The username is registered in Resilient, but without reference to an Organization. This is a configuration error.");
+ this.invalidateLogin(request, response);
+ return;
+ }
} else {
- // Find or NULL
- userOrganization = organizationRepository.findOneByCode(organizationCode).orElse(null);
- }
- if (userOrganization == null) {
- this.invalidateLogin(request, response);
- return;
- }
+ // User not found? Use SAML2 values or defaults.
+ if (attr.getRole() == null || attr.getRole().isBlank()) {
+ // This is NOT allowed. SAML2 or Defaults must provide a Role.
+ log.error(
+ "A ROLE must be given to a user, but none was calculated. Nor by SAML2 XML, nor defined in defaults.");
+ this.invalidateLogin(request, response);
+ return;
+ }
+ authorities = List.of(attr.getRole()).stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
- // SecurityGroup exists?
- String defaultSecurityGroupCode = "default"; // TODO: Make this configurable
- if (securityGroupCode == null || securityGroupCode.isBlank()) {
- securityGroupCode = defaultSecurityGroupCode;
- }
+ // Organization not found ?
+ if (attr.getOrganizationCode() == null || attr.getOrganizationCode().isBlank()) {
+ // This is NOT allowed. SAML2 or Defaults must provide a Organization code.
+ log.error(
+ "An OrganizationCode must be given to a user, but none was calculated. Nor by SAML2 XML, nor defined in defaults.");
+ this.invalidateLogin(request, response);
+ return;
+ } else {
+ // Find or NULL
+ userOrganization = organizationRepository.findOneByCode(attr.getOrganizationCode()).orElse(null);
+ }
+ if (userOrganization == null) {
+ log.error("The Organization code [" + attr.getOrganizationCode() + "] wasn't found in database.");
+ this.invalidateLogin(request, response);
+ return;
+ }
- // Find or fail
- securityGroup = securityGroupRepository.findOneByCodeWithEagerRelationships(securityGroupCode).orElse(null);
+ // SecurityGroup exists?
+ if (attr.getSecurityGroupCode() == null || attr.getSecurityGroupCode().isBlank()) {
+ // This is NOT allowed. SAML2 or Defaults must provide a SecurityGroup code.
+ log.error(
+ "A SecurityGroup code must be given to a user, but none was calculated. Nor by SAML2 XML, nor defined in defaults.");
+ this.invalidateLogin(request, response);
+ return;
+ }
- if (securityGroup == null) {
- this.invalidateLogin(request, response);
- return;
+ // Find or fail
+ securityGroup = securityGroupRepository.findOneByCodeWithEagerRelationships(attr.getSecurityGroupCode()).orElse(
+ null);
+
+ if (securityGroup == null) {
+ log.error("The SecurityGroup code [" + attr.getSecurityGroupCode() + "] wasn't found in database.");
+ this.invalidateLogin(request, response);
+ return;
+ }
}
// Create a ResilientUserDetails and replace Principal
@@ -173,4 +216,31 @@ public class Saml2AuthenticationHandler implements AuthenticationSuccessHandler,
}
}
}
+
+ /**
+ * Extract the expected user attributes from SAML2Response. There's some standard expected attributes, those will be
+ * mapped to properties, the remaining will be put into a MAP.
+ */
+ private Saml2AuthenticationAttributes readSaml2Attributes(Saml2AuthenticatedPrincipal principal) {
+ // Get the RelyingParty ID (IDP)
+ String registrationId = principal.getRelyingPartyRegistrationId();
+
+ // Get registration, from config properties for the RelyingParty ID
+ Registration registration = this.resilientSaml2Properties.getRelyingparty().getRegistration().get(registrationId);
+
+ if (registration == null) {
+ // This is VERY strange
+ }
+
+ Saml2AuthenticationAttributes authAttr = new Saml2AuthenticationAttributes();
+ Map atributes = registration.getAssertingparty().getAttributes();
+ Map defaults = registration.getAssertingparty().getDefaults();
+
+ for (Entry entry : atributes.entrySet()) {
+ String attrValue = this.getPrincipalAttribute(principal, entry.getValue());
+ authAttr.addAttribute(entry.getKey(), attrValue, defaults.get(entry.getKey()));
+ }
+
+ return authAttr;
+ }
}
diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml
index 91c2b32..0108b50 100644
--- a/src/main/resources/config/application-dev.yml
+++ b/src/main/resources/config/application-dev.yml
@@ -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
diff --git a/src/main/resources/mock-idp/idp.js b/src/main/resources/mock-idp/idp.js
index 23b908b..6627a63 100644
--- a/src/main/resources/mock-idp/idp.js
+++ b/src/main/resources/mock-idp/idp.js
@@ -132,6 +132,91 @@ function createFakeSamlResponse(nameId, email, orgCode, securityGroupCode, roles
console.log('SAML Destination :', destination);
console.log('SAML Issuer :', issuer);
+ const assertion = `
+
+ ${issuer}
+
+ ${nameId}
+
+
+
+
+
+
+ ${audience}
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
+
+
+
+
+ ${email}
+
+
+ ${orgCode}
+
+
+ ${securityGroupCode}
+
+
+ ${roles}
+
+
+ Pedro Reis
+
+
+ Pedro Reis
+
+
+ pedro.reis@unl.pt
+
+
+ pedro.reis@unl.pt
+
+
+ `;
+
+ 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: () => `${certBase64}`
+ };
+ sig.computeSignature(assertion, {
+ prefix: 'ds',
+ location: { reference: "//*[local-name(.)='Issuer']", action: 'after' }
+ });
+
+ const signedAssertion = sig.getSignedXml();
+ const samlResponse = `${issuer}${signedAssertion}`;
+
+ 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 = `
${issuer}
@@ -166,35 +251,6 @@ function createFakeSamlResponse(nameId, email, orgCode, securityGroupCode, roles
`;
-
- 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: () => `${certBase64}`
- };
- sig.computeSignature(assertion, {
- prefix: 'ds',
- location: { reference: "//*[local-name(.)='Issuer']", action: 'after' }
- });
-
- const signedAssertion = sig.getSignedXml();
- const samlResponse = `${issuer}${signedAssertion}`;
-
- 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})`);
});