1 package org.collectionspace.services.common.security;
3 import java.util.ArrayList;
6 import org.apache.commons.lang3.StringUtils;
7 import org.collectionspace.authentication.CSpaceUser;
8 import org.collectionspace.authentication.spring.CSpaceSaml2Authentication;
9 import org.collectionspace.authentication.spring.CSpaceUserDetailsService;
10 import org.collectionspace.services.common.config.ConfigUtils;
11 import org.collectionspace.services.config.AssertionProbesType;
12 import org.collectionspace.services.config.SAMLRelyingPartyType;
13 import org.collectionspace.services.config.ServiceConfig;
14 import org.collectionspace.services.common.ServiceMain;
15 import org.opensaml.saml.saml2.core.Assertion;
16 import org.slf4j.Logger;
17 import org.slf4j.LoggerFactory;
18 import org.springframework.core.convert.converter.Converter;
19 import org.springframework.security.core.userdetails.UsernameNotFoundException;
20 import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
21 import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider.ResponseToken;
22 import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
24 public class CSpaceSaml2ResponseAuthenticationConverter implements Converter<ResponseToken, CSpaceSaml2Authentication> {
25 private final Logger logger = LoggerFactory.getLogger(CSpaceSaml2ResponseAuthenticationConverter.class);
27 private CSpaceUserDetailsService userDetailsService;
29 public CSpaceSaml2ResponseAuthenticationConverter(CSpaceUserDetailsService userDetailsService) {
30 this.userDetailsService = userDetailsService;
34 public CSpaceSaml2Authentication convert(ResponseToken responseToken) {
35 Saml2Authentication authentication = OpenSamlAuthenticationProvider
36 .createDefaultResponseAuthenticationConverter()
37 .convert(responseToken);
39 String registrationId = responseToken.getToken().getRelyingPartyRegistration().getRegistrationId();
40 ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig();
41 SAMLRelyingPartyType relyingPartyRegistration = ConfigUtils.getSAMLRelyingPartyRegistration(serviceConfig, registrationId);
42 CSpaceUser user = findUser(relyingPartyRegistration, responseToken);
45 return new CSpaceSaml2Authentication(user, authentication);
52 * Attempt to find a CSpace user for a SAML response.
54 * @param relyingPartyRegistration
55 * @param responseToken
58 private CSpaceUser findUser(SAMLRelyingPartyType relyingPartyRegistration, ResponseToken responseToken) {
59 AssertionProbesType assertionSsoIdProbes = (
60 relyingPartyRegistration != null
61 ? relyingPartyRegistration.getAssertionSsoIdProbes()
65 AssertionProbesType assertionUsernameProbes = (
66 relyingPartyRegistration != null
67 ? relyingPartyRegistration.getAssertionUsernameProbes()
71 List<String> attemptedUsernames = new ArrayList<>();
72 List<Assertion> assertions = responseToken.getResponse().getAssertions();
74 SecurityUtils.logSamlAssertions(assertions);
76 for (Assertion assertion : assertions) {
77 CSpaceUser user = null;
78 String ssoId = SecurityUtils.getSamlAssertionSsoId(assertion, assertionSsoIdProbes);
80 // First, look for a CSpace user whose SSO ID is the ID in the assertion.
84 user = (CSpaceUser) userDetailsService.loadUserBySsoId(ssoId);
86 catch (UsernameNotFoundException e) {
94 // Next, look for a CSpace user whose username is the email address in the assertion.
96 Set<String> candidateUsernames = SecurityUtils.findSamlAssertionCandidateUsernames(assertion, assertionUsernameProbes);
98 for (String candidateUsername : candidateUsernames) {
100 user = (CSpaceUser) userDetailsService.loadUserByUsername(candidateUsername);
103 String expectedSsoId = user.getSsoId();
105 if (expectedSsoId == null) {
106 // Store the ID from the IdP to use in future log ins. Note that this does not save
107 // the SSO ID to the database. That happens in CSpaceAuthenticationSuccessEvent.
109 user.setSsoId(ssoId);
111 // TODO: If the email address in the assertion differs from the CSpace user's email,
112 // update the CSpace user.
113 } else if (!StringUtils.equals(expectedSsoId, ssoId)) {
114 // If the user previously logged in via SSO, but they had a different ID from the
115 // IdP, something's wrong. (Did an account on the IdP get assigned an email that
116 // previously belonged to a different account on the IdP?)
118 logger.warn("User with username {} has expected SSO ID {}, but received {} in SAML assertion",
119 candidateUsername, expectedSsoId, ssoId);
129 catch(UsernameNotFoundException e) {
133 attemptedUsernames.addAll(candidateUsernames);
136 // No CSpace user was found for this SAML response.
137 // TODO: Auto-create a CSpace user, using the display name, email address, and ID in the response.
139 String errorMessage = attemptedUsernames.size() == 0
140 ? "The SAML response did not contain a CollectionSpace username."
141 : "No CollectionSpace account found for " + StringUtils.join(attemptedUsernames, " / ") + ".";
143 throw(new UsernameNotFoundException(errorMessage));