From: Ray Lee Date: Mon, 6 Nov 2023 15:50:31 +0000 (-0500) Subject: Allow configuring probe of SAML assertion for CSpace username. X-Git-Url: https://git.aero2k.de/?a=commitdiff_plain;h=c355df00dd85dcba4e7c885a2f37c96b630eacaf;p=tmp%2Fjakarta-migration.git Allow configuring probe of SAML assertion for CSpace username. Also fix single logout error when the CSpace username comes from an attribute in the SAML assertion instead of the NameID. --- diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceSaml2Authentication.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceSaml2Authentication.java index 60488ac20..2adb0174c 100644 --- a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceSaml2Authentication.java +++ b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceSaml2Authentication.java @@ -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 getAuthorities() { return user.getAuthorities(); } + + public Saml2Authentication getWrappedAuthentication() { + return new Saml2Authentication(this.principal, getSaml2Response(), getAuthorities()); + } } diff --git a/services/common/src/main/java/org/collectionspace/services/common/security/SecurityConfig.java b/services/common/src/main/java/org/collectionspace/services/common/security/SecurityConfig.java index fc7403106..46ac92944 100644 --- a/services/common/src/main/java/org/collectionspace/services/common/security/SecurityConfig.java +++ b/services/common/src/main/java/org/collectionspace/services/common/security/SecurityConfig.java @@ -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 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.LogoutRequestConfigurer>() { @Override public void customize(Saml2LogoutConfigurer.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); + } + }); } }); } diff --git a/services/common/src/main/java/org/collectionspace/services/common/security/SecurityUtils.java b/services/common/src/main/java/org/collectionspace/services/common/security/SecurityUtils.java index 79d1ddb4c..0c5530947 100644 --- a/services/common/src/main/java/org/collectionspace/services/common/security/SecurityUtils.java +++ b/services/common/src/main/java/org/collectionspace/services/common/security/SecurityUtils.java @@ -22,10 +22,8 @@ */ 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 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 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 findSamlAssertionCandidateUsernames(Assertion assertion, AssertionProbesType assertionProbes) { + List candidateUsernames = new ArrayList<>(); + List 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 attributeValues = attribute.getAttributeValues(); + if (subjectNameID != null && subjectNameID.length() > 0) { + candidateUsernames.add(subjectNameID); + } + } else if (probe instanceof AssertionAttributeProbeType) { + String attributeName = ((AssertionAttributeProbeType) probe).getName(); + List 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 getSamlAssertionAttributeValues(Assertion assertion, String attributeName) { + List values = new ArrayList<>(); + + for (AttributeStatement statement : assertion.getAttributeStatements()) { + for (Attribute attribute : statement.getAttributes()) { + String name = attribute.getName(); + + if (name.equals(attributeName)) { + List 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; + } } diff --git a/services/config/src/main/java/org/collectionspace/services/common/config/ConfigUtils.java b/services/config/src/main/java/org/collectionspace/services/common/config/ConfigUtils.java index 8e4c848bb..b3b253b00 100644 --- a/services/config/src/main/java/org/collectionspace/services/common/config/ConfigUtils.java +++ b/services/config/src/main/java/org/collectionspace/services/common/config/ConfigUtils.java @@ -211,6 +211,20 @@ public class ConfigUtils { return null; } + public static SAMLRelyingPartyType getSAMLRelyingPartyRegistration(ServiceConfig serviceConfig, String registrationId) { + List 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 samlRegistrations = getSAMLRelyingPartyRegistrations(serviceConfig); diff --git a/services/config/src/main/resources/service-config.xsd b/services/config/src/main/resources/service-config.xsd index 051f37d42..9598ba0fd 100644 --- a/services/config/src/main/resources/service-config.xsd +++ b/services/config/src/main/resources/service-config.xsd @@ -260,6 +260,22 @@ + + + + + + + + + ]]> + + + @@ -272,6 +288,43 @@ + + + + Configures probes in a SAML assertion. + + + + + + + + + + + + + + + Configures probing of an attribute in a SAML assertion. + + + + + + + The name of the SAML attribute to probe. + + + + + + + + Configures probing of the NameID in a SAML assertion. + + +