]> git.aero2k.de Git - tmp/jakarta-migration.git/commitdiff
Allow configuring probe of SAML assertion for CSpace username.
authorRay Lee <ray.lee@lyrasis.org>
Mon, 6 Nov 2023 15:50:31 +0000 (10:50 -0500)
committerRay Lee <ray.lee@lyrasis.org>
Tue, 7 Nov 2023 04:10:47 +0000 (23:10 -0500)
Also fix single logout error when the CSpace username comes from an attribute in the SAML assertion instead of the NameID.

services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceSaml2Authentication.java
services/common/src/main/java/org/collectionspace/services/common/security/SecurityConfig.java
services/common/src/main/java/org/collectionspace/services/common/security/SecurityUtils.java
services/config/src/main/java/org/collectionspace/services/common/config/ConfigUtils.java
services/config/src/main/resources/service-config.xsd

index 60488ac20e284eb0b65cf9add4c25d93f329da83..2adb0174cbc7fb1ca53313849e61856c8977e684 100644 (file)
@@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
 @JsonIgnoreProperties(value = { "authenticated" }, ignoreUnknown = true)
 public class CSpaceSaml2Authentication extends Saml2Authentication {
   private final CSpaceUser user;
+  private final AuthenticatedPrincipal principal;
 
   public CSpaceSaml2Authentication(CSpaceUser user, Saml2Authentication authentication) {
     this(
@@ -60,6 +61,7 @@ public class CSpaceSaml2Authentication extends Saml2Authentication {
     super(principal, saml2Response, authorities);
 
     this.user = user;
+    this.principal = principal;
 
     this.setAuthenticated(true);
   }
@@ -73,4 +75,8 @@ public class CSpaceSaml2Authentication extends Saml2Authentication {
   public Collection<GrantedAuthority> getAuthorities() {
     return user.getAuthorities();
   }
+
+  public Saml2Authentication getWrappedAuthentication() {
+    return new Saml2Authentication(this.principal, getSaml2Response(), getAuthorities());
+  }
 }
index fc7403106f076c062f26fddd565fedb5ceefc9c8..46ac92944871366cc1e04b431929c3e05704f1e9 100644 (file)
@@ -33,6 +33,7 @@ import javax.servlet.http.HttpServletRequest;
 import javax.sql.DataSource;
 
 import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.collectionspace.authentication.CSpaceUser;
 import org.collectionspace.authentication.spring.CSpaceDaoAuthenticationProvider;
 import org.collectionspace.authentication.spring.CSpaceJwtAuthenticationToken;
@@ -47,6 +48,7 @@ import org.collectionspace.services.common.ServiceMain;
 import org.collectionspace.services.common.config.ConfigUtils;
 import org.collectionspace.services.common.config.TenantBindingConfigReaderImpl;
 import org.collectionspace.services.config.AssertingPartyDetailsType;
+import org.collectionspace.services.config.AssertionProbesType;
 import org.collectionspace.services.config.OAuthAuthorizationGrantTypeEnum;
 import org.collectionspace.services.config.OAuthClientAuthenticationMethodEnum;
 import org.collectionspace.services.config.OAuthClientSettingsType;
@@ -94,6 +96,7 @@ import org.springframework.security.config.annotation.web.configurers.LogoutConf
 import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
 import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer;
 import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -112,6 +115,7 @@ import org.springframework.security.oauth2.server.authorization.settings.TokenSe
 import org.springframework.security.saml2.core.Saml2X509Credential;
 import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
 import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
 import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider.ResponseToken;
 import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
 import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
@@ -124,6 +128,8 @@ import org.springframework.security.saml2.provider.service.web.DefaultRelyingPar
 import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
 import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
 import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutRequestResolver;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
 import org.springframework.security.web.authentication.logout.LogoutFilter;
@@ -529,7 +535,7 @@ public class SecurityConfig {
                        .addFilterBefore(new CSpaceUserAttributeFilter(), LogoutFilter.class);
 
                if (relyingPartyRegistrationRepository != null) {
-                       RelyingPartyRegistrationResolver relyingPartyRegistrationResolver =
+                       final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver =
                                new DefaultRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository);
 
                        // TODO: Use OpenSaml4AuthenticationProvider (requires Java 11) instead of deprecated OpenSamlAuthenticationProvider.
@@ -542,19 +548,32 @@ public class SecurityConfig {
                                                .createDefaultResponseAuthenticationConverter()
                                                .convert(responseToken);
 
+                                       String registrationId = responseToken.getToken().getRelyingPartyRegistration().getRegistrationId();
+                                       ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig();
+                                       SAMLRelyingPartyType registration = ConfigUtils.getSAMLRelyingPartyRegistration(serviceConfig, registrationId);
+
+                                       AssertionProbesType assertionProbes = (
+                                               registration != null
+                                                       ? registration.getAssertionUsernameProbes()
+                                                       : null
+                                       );
+
                                        Assertion assertion = responseToken.getResponse().getAssertions().get(0);
-                                       String username = SecurityUtils.getSamlAssertionUsername(assertion, EMAIL_ATTR_NAMES);
+                                       List<String> candidateUsernames = SecurityUtils.findSamlAssertionCandidateUsernames(assertion, assertionProbes);
 
-                                       try {
-                                               CSpaceUser user = (CSpaceUser) userDetailsService.loadUserByUsername(username);
+                                       for (String candidateUsername : candidateUsernames) {
+                                               try {
+                                                       CSpaceUser user = (CSpaceUser) userDetailsService.loadUserByUsername(candidateUsername);
 
-                                               return new CSpaceSaml2Authentication(user, authentication);
+                                                       return new CSpaceSaml2Authentication(user, authentication);
+                                               }
+                                               catch(UsernameNotFoundException e) {
+                                               }
                                        }
-                                       catch(UsernameNotFoundException e) {
-                                               String errorMessage = "No CollectionSpace account was found for " + username + ".";
 
-                                               throw(new UsernameNotFoundException(errorMessage, e));
-                                       }
+                                       String errorMessage = "No CollectionSpace account was found for " + StringUtils.join(candidateUsernames, " / ") + ".";
+
+                                       throw(new UsernameNotFoundException(errorMessage));
                                }
                        });
 
@@ -589,7 +608,25 @@ public class SecurityConfig {
                                                        configurer.logoutRequest(new Customizer<Saml2LogoutConfigurer<HttpSecurity>.LogoutRequestConfigurer>() {
                                                                @Override
                                                                public void customize(Saml2LogoutConfigurer<HttpSecurity>.LogoutRequestConfigurer configurer) {
-                                                                       configurer.logoutRequestRepository(new CSpaceSaml2LogoutRequestRepository());
+                                                                       configurer
+                                                                               .logoutRequestRepository(new CSpaceSaml2LogoutRequestRepository())
+                                                                               .logoutRequestResolver(new Saml2LogoutRequestResolver() {
+                                                                                       @Override
+                                                                                       public Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentication) {
+                                                                                               // TODO: Use OpenSaml4LogoutRequestResolver (requires Java 11).
+                                                                                               Saml2LogoutRequestResolver resolver = new OpenSaml3LogoutRequestResolver(relyingPartyRegistrationResolver);
+
+                                                                                               // The name of the authenticated principal in our CSpaceSaml2Authentication
+                                                                                               // may have come from an attribute of the SAML assertion instead of the
+                                                                                               // NameID, but the logout request needs to send the NameID.
+                                                                                               // CSpaceSaml2Authentication.getWrappedAuthentication will get the
+                                                                                               // authentication whose principal is the NameID of the assertion.
+
+                                                                                               Saml2Authentication wrappedAuthentication = ((CSpaceSaml2Authentication) authentication).getWrappedAuthentication();
+
+                                                                                               return resolver.resolve(request, wrappedAuthentication);
+                                                                                       }
+                                                                               });
                                                                }
                                                        });
                                                }
index 79d1ddb4cb6a3743db4873a400d351d581f2bf80..0c55309471069d3c7fd946e71c28370f4ad3a010 100644 (file)
  */
 package org.collectionspace.services.common.security;
 
-import java.security.MessageDigest;
 import java.util.ArrayList;
 import java.util.List;
-import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
 import java.util.StringTokenizer;
 
@@ -36,6 +34,9 @@ import org.collectionspace.services.client.CollectionSpaceClient;
 import org.collectionspace.services.client.index.IndexClient;
 import org.collectionspace.services.client.workflow.WorkflowClient;
 import org.collectionspace.services.common.api.Tools;
+import org.collectionspace.services.config.AssertionAttributeProbeType;
+import org.collectionspace.services.config.AssertionNameIDProbeType;
+import org.collectionspace.services.config.AssertionProbesType;
 import org.collectionspace.services.config.service.ServiceBindingType;
 import org.collectionspace.authentication.AuthN;
 import org.collectionspace.authentication.spring.CSpacePasswordEncoderFactory;
@@ -47,10 +48,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.springframework.security.crypto.password.PasswordEncoder;
-import org.jboss.crypto.digest.DigestCallback;
 import org.jboss.resteasy.spi.HttpRequest;
-import org.jboss.security.Base64Encoder;
-import org.jboss.security.Base64Utils;
 import org.opensaml.core.xml.XMLObject;
 import org.opensaml.core.xml.schema.XSString;
 import org.opensaml.saml.saml2.core.Assertion;
@@ -74,6 +72,25 @@ public class SecurityUtils {
     public static final String RFC2617_ENCODING = "RFC2617";
     private static char MD5_HEX[] = "0123456789abcdef".toCharArray();
 
+    private static final List<Object> DEFAULT_SAML_ASSERTION_USERNAME_PROBES = new ArrayList<>();
+
+    static {
+        DEFAULT_SAML_ASSERTION_USERNAME_PROBES.add(new AssertionNameIDProbeType());
+
+        String[] attributeNames = new String[]{
+            "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
+            "email",
+            "mail"
+        };
+
+        for (String attributeName : attributeNames) {
+            AssertionAttributeProbeType attributeProbe = new AssertionAttributeProbeType();
+            attributeProbe.setName(attributeName);
+
+            DEFAULT_SAML_ASSERTION_USERNAME_PROBES.add(attributeProbe);
+        }
+    }
+
     /**
      * createPasswordHash creates password has using configured digest algorithm
      * and encoding
@@ -325,50 +342,67 @@ public class SecurityUtils {
         return result;
     }
 
-       /*
-        * Retrieve the CSpace username from a SAML assertion. If the assertion's subject nameID is an
-        * email address, it is returned. Otherwise, the first value of the given attribute name is
-        * returned.
-        */
-       public static String getSamlAssertionUsername(Assertion assertion, List<String> attributeNames) {
-               String subjectNameID = assertion.getSubject().getNameID().getValue();
-
-               if (subjectNameID.contains("@")) {
-                       return subjectNameID;
-               }
-
-               for (String attributeName : attributeNames) {
-                       String value = findSamlAssertionAttribute(assertion, attributeName);
+    /*
+     * Retrieve the possible CSpace usernames from a SAML assertion.
+     */
+    public static List<String> findSamlAssertionCandidateUsernames(Assertion assertion, AssertionProbesType assertionProbes) {
+        List<String> candidateUsernames = new ArrayList<>();
+        List<Object> probes = null;
 
-                       if (value != null) {
-                               return value;
-                       }
-               }
+        if (assertionProbes != null) {
+            probes = assertionProbes.getNameIdOrAttribute();
+        }
 
-               return null;
-       }
+        if (probes == null || probes.size() == 0) {
+            probes = DEFAULT_SAML_ASSERTION_USERNAME_PROBES;
+        }
 
-       private static String findSamlAssertionAttribute(Assertion assertion, String attributeName) {
-               for (AttributeStatement statement : assertion.getAttributeStatements()) {
-                       for (Attribute attribute : statement.getAttributes()) {
-                               String name = attribute.getName();
+        for (Object probe : probes) {
+            if (probe instanceof AssertionNameIDProbeType) {
+                String subjectNameID = assertion.getSubject().getNameID().getValue();
 
-                               if (name.equals(attributeName)) {
-                                       List<XMLObject> attributeValues = attribute.getAttributeValues();
+                if (subjectNameID != null && subjectNameID.length() > 0) {
+                    candidateUsernames.add(subjectNameID);
+                }
+            } else if (probe instanceof AssertionAttributeProbeType) {
+                String attributeName = ((AssertionAttributeProbeType) probe).getName();
+                List<String> values = getSamlAssertionAttributeValues(assertion, attributeName);
 
-                                       if (attributeValues != null && attributeValues.size() > 0) {
-                                               XMLObject value = attributeValues.get(0);
+                if (values != null) {
+                    candidateUsernames.addAll(values);
+                }
+            }
+        }
 
-                                               if (value instanceof XSString) {
-                                                       XSString stringValue = (XSString) value;
+        return candidateUsernames;
+    }
 
-                                                       return stringValue.getValue();
-                                               }
-                                       }
-                               }
-                       }
-               }
+    private static List<String> getSamlAssertionAttributeValues(Assertion assertion, String attributeName) {
+        List<String> values = new ArrayList<>();
+
+        for (AttributeStatement statement : assertion.getAttributeStatements()) {
+            for (Attribute attribute : statement.getAttributes()) {
+                String name = attribute.getName();
+
+                if (name.equals(attributeName)) {
+                    List<XMLObject> attributeValues = attribute.getAttributeValues();
+
+                    if (attributeValues != null) {
+                        for (XMLObject value : attributeValues) {
+                            if (value instanceof XSString) {
+                                XSString stringValue = (XSString) value;
+                                String candidateValue = stringValue.getValue();
+
+                                if (candidateValue != null && candidateValue.length() > 0) {
+                                    values.add(candidateValue);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
 
-               return null;
-       }
+        return values;
+    }
 }
index 8e4c848bb935af0c213b7906d59c679253359eb7..b3b253b005eb7e95d88867fd432e3609574d12ca 100644 (file)
@@ -211,6 +211,20 @@ public class ConfigUtils {
                return null;
        }
 
+       public static SAMLRelyingPartyType getSAMLRelyingPartyRegistration(ServiceConfig serviceConfig, String registrationId) {
+               List<SAMLRelyingPartyType> registrations = getSAMLRelyingPartyRegistrations(serviceConfig);
+
+               if (registrations != null) {
+                       for (SAMLRelyingPartyType registration : registrations) {
+                               if (registration.getId().equals(registrationId)) {
+                                       return registration;
+                               }
+                       }
+               }
+
+               return null;
+       }
+
        public static boolean isSsoAvailable(ServiceConfig serviceConfig) {
                List<SAMLRelyingPartyType> samlRegistrations = getSAMLRelyingPartyRegistrations(serviceConfig);
 
index 051f37d42e9cd4a66ba39244f0f5015eee0e30ee..9598ba0fdc560848d44900b3200b85f9aefa01f1 100644 (file)
                     </xs:documentation>
                 </xs:annotation>
             </xs:element>
+
+            <xs:element name="assertion-username-probes" type="AssertionProbesType" minOccurs="0" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>
+                        <![CDATA[
+                        Configures how a SAML assertion is probed to find the CollectionSpace
+                        username. Defaults to:
+
+                        <name-id />
+                        <attribute name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" />
+                        <attribute name="email" />
+                        <attribute name="mail" />
+                        ]]>
+                    </xs:documentation>
+                </xs:annotation>
+            </xs:element>
         </xs:sequence>
 
         <xs:attribute name="id" type="xs:string" use="required">
         </xs:attribute>
     </xs:complexType>
 
+    <xs:complexType name="AssertionProbesType">
+        <xs:annotation>
+            <xs:documentation>
+                Configures probes in a SAML assertion.
+            </xs:documentation>
+        </xs:annotation>
+
+        <xs:sequence>
+            <xs:choice minOccurs="0" maxOccurs="unbounded">
+                <xs:element name="name-id" type="AssertionNameIDProbeType" />
+                <xs:element name="attribute" type="AssertionAttributeProbeType" />
+            </xs:choice>
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="AssertionAttributeProbeType">
+        <xs:annotation>
+            <xs:documentation>
+                Configures probing of an attribute in a SAML assertion.
+            </xs:documentation>
+        </xs:annotation>
+
+        <xs:attribute name="name" type="xs:string" use="required">
+            <xs:annotation>
+                <xs:documentation>
+                    The name of the SAML attribute to probe.
+                </xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+
+    <xs:complexType name="AssertionNameIDProbeType">
+        <xs:annotation>
+            <xs:documentation>Configures probing of the NameID in a SAML assertion.</xs:documentation>
+        </xs:annotation>
+    </xs:complexType>
+
     <xs:complexType name="IconType">
         <xs:annotation>
             <xs:documentation>