Also fix single logout error when the CSpace username comes from an attribute in the SAML assertion instead of the NameID.
@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(
super(principal, saml2Response, authorities);
this.user = user;
+ this.principal = principal;
this.setAuthenticated(true);
}
public Collection<GrantedAuthority> getAuthorities() {
return user.getAuthorities();
}
+
+ public Saml2Authentication getWrappedAuthentication() {
+ return new Saml2Authentication(this.principal, getSaml2Response(), getAuthorities());
+ }
}
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;
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;
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;
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;
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;
.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.
.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));
}
});
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);
+ }
+ });
}
});
}
*/
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;
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;
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;
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
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;
+ }
}
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);
</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>