diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java index 9ff528d30..ed25da18a 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java @@ -7,11 +7,10 @@ import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; import edu.internet2.tier.shibboleth.admin.ui.service.EmailService; -import org.apache.commons.lang.RandomStringUtils; -import org.apache.http.entity.ContentType; +import org.apache.commons.lang3.RandomStringUtils; +import org.pac4j.saml.profile.SAML2Profile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.bcrypt.BCrypt; @@ -25,6 +24,8 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.List; +import java.util.Map; import java.util.Optional; /** @@ -40,32 +41,51 @@ public class AddNewUserFilter implements Filter { private RoleRepository roleRepository; private EmailService emailService; - public AddNewUserFilter(UserRepository userRepository, RoleRepository roleRepository, EmailService emailService) { + private Pac4jConfigurationProperties pac4jConfigurationProperties; + + private Pac4jConfigurationProperties.SAML2ProfileMapping saml2ProfileMapping; + + public AddNewUserFilter(Pac4jConfigurationProperties pac4jConfigurationProperties, UserRepository userRepository, RoleRepository roleRepository, EmailService emailService) { this.userRepository = userRepository; this.roleRepository = roleRepository; this.emailService = emailService; + this.pac4jConfigurationProperties = pac4jConfigurationProperties; + saml2ProfileMapping = this.pac4jConfigurationProperties.getSaml2ProfileMapping(); } @Override public void init(FilterConfig filterConfig) throws ServletException { } + private User buildAndPersistNewUserFromProfile(SAML2Profile profile) { + Role noRole = roleRepository.findByName(ROLE_NONE).orElse(new Role(ROLE_NONE)); + roleRepository.save(noRole); + + User user = new User(); + user.getRoles().add(noRole); + user.setUsername(getAttributeFromProfile(profile, "username")); + user.setPassword(BCrypt.hashpw(RandomStringUtils.randomAlphanumeric(20), BCrypt.gensalt())); + user.setFirstName(getAttributeFromProfile(profile, "firstName")); + user.setLastName(getAttributeFromProfile(profile, "lastName")); + user.setEmailAddress(getAttributeFromProfile(profile, "email")); + User persistedUser = userRepository.save(user); + if (logger.isDebugEnabled()) { + logger.debug("Persisted new user:\n" + user); + } + return persistedUser; + } + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null) { - String username = authentication.getName(); + SAML2Profile profile = (SAML2Profile) authentication.getPrincipal(); + if (profile != null) { + String username = getAttributeFromProfile(profile, "username"); if (username != null) { Optional persistedUser = userRepository.findByUsername(username); User user; if (!persistedUser.isPresent()) { - user = new User(); - user.setUsername(username); - user.setPassword(BCrypt.hashpw(RandomStringUtils.randomAlphanumeric(20), BCrypt.gensalt())); - Role noRole = roleRepository.findByName(ROLE_NONE).orElse(new Role(ROLE_NONE)); - roleRepository.save(noRole); - user.getRoles().add(noRole); - userRepository.save(user); + user = buildAndPersistNewUserFromProfile(profile); try { emailService.sendNewUserMail(username); } catch (MessagingException e) { @@ -87,6 +107,28 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha public void destroy() { } + private String getAttributeFromProfile(SAML2Profile profile, String stringKey) { + String attribute = null; + switch (stringKey) { + case "username": + attribute = saml2ProfileMapping.getUsername(); + break; + case "firstName": + attribute = saml2ProfileMapping.getFirstName(); + break; + case "lastName": + attribute = saml2ProfileMapping.getLastName(); + break; + case "email": + attribute = saml2ProfileMapping.getEmail(); + break; + default: + // do we care? Not yet. + } + List attributeList = (List) profile.getAttribute(attribute); + return attributeList.size() < 1 ? null : attributeList.get(0); + } + private byte[] getJsonResponseBytes(ErrorResponse eErrorResponse) throws IOException { String errorResponseJson = new ObjectMapper().writeValueAsString(eErrorResponse); return errorResponseJson.getBytes(); diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java index 1c28b1ee1..5ee638178 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java @@ -22,6 +22,7 @@ public Config config(final Pac4jConfigurationProperties pac4jConfigurationProper saml2ClientConfiguration.setServiceProviderMetadataPath(pac4jConfigurationProperties.getServiceProviderMetadataPath()); saml2ClientConfiguration.setForceServiceProviderMetadataGeneration(pac4jConfigurationProperties.isForceServiceProviderMetadataGeneration()); saml2ClientConfiguration.setWantsAssertionsSigned(pac4jConfigurationProperties.isWantAssertionsSigned()); + saml2ClientConfiguration.setAttributeAsId(pac4jConfigurationProperties.getSaml2ProfileMapping().getUsername()); final SAML2Client saml2Client = new SAML2Client(saml2ClientConfiguration); saml2Client.setName("Saml2Client"); diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java index afb1369a1..cf36d8e65 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java @@ -1,10 +1,12 @@ package net.unicon.shibui.pac4j; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "shibui.pac4j") +@EnableConfigurationProperties public class Pac4jConfigurationProperties { private String keystorePath = "/tmp/samlKeystore.jks"; private String keystorePassword = "changeit"; @@ -16,6 +18,46 @@ public class Pac4jConfigurationProperties { private boolean forceServiceProviderMetadataGeneration = false; private String callbackUrl; private boolean wantAssertionsSigned = true; + private SAML2ProfileMapping saml2ProfileMapping; + + public static class SAML2ProfileMapping { + private String username; + private String email; + private String firstName; + private String lastName; + + 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 getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + } public String getKeystorePath() { return keystorePath; @@ -96,4 +138,12 @@ public boolean isWantAssertionsSigned() { public void setWantAssertionsSigned(boolean wantAssertionsSigned) { this.wantAssertionsSigned = wantAssertionsSigned; } + + public SAML2ProfileMapping getSaml2ProfileMapping() { + return saml2ProfileMapping; + } + + public void setSaml2ProfileMapping(SAML2ProfileMapping saml2ProfileMapping) { + this.saml2ProfileMapping = saml2ProfileMapping; + } } diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java index 4b1c549f7..120a45f36 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java @@ -20,8 +20,8 @@ @AutoConfigureOrder(-1) public class WebSecurity { @Bean("webSecurityConfig") - public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter(final Config config, UserRepository userRepository, RoleRepository roleRepository, EmailService emailService) { - return new Pac4jWebSecurityConfigurerAdapter(config, userRepository, roleRepository, emailService); + public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter(final Config config, UserRepository userRepository, RoleRepository roleRepository, EmailService emailService, Pac4jConfigurationProperties pac4jConfigurationProperties) { + return new Pac4jWebSecurityConfigurerAdapter(config, userRepository, roleRepository, emailService, pac4jConfigurationProperties); } @Configuration @@ -57,12 +57,14 @@ public static class Pac4jWebSecurityConfigurerAdapter extends WebSecurityConfigu private UserRepository userRepository; private RoleRepository roleRepository; private EmailService emailService; + private Pac4jConfigurationProperties pac4jConfigurationProperties; - public Pac4jWebSecurityConfigurerAdapter(final Config config, UserRepository userRepository, RoleRepository roleRepository, EmailService emailService) { + public Pac4jWebSecurityConfigurerAdapter(final Config config, UserRepository userRepository, RoleRepository roleRepository, EmailService emailService, Pac4jConfigurationProperties pac4jConfigurationProperties) { this.config = config; this.userRepository = userRepository; this.roleRepository = roleRepository; this.emailService = emailService; + this.pac4jConfigurationProperties = pac4jConfigurationProperties; } @Override @@ -72,7 +74,7 @@ protected void configure(HttpSecurity http) throws Exception { final CallbackFilter callbackFilter = new CallbackFilter(this.config); http.antMatcher("/**").addFilterBefore(callbackFilter, BasicAuthenticationFilter.class) .addFilterBefore(securityFilter, BasicAuthenticationFilter.class) - .addFilterAfter(new AddNewUserFilter(userRepository, roleRepository, emailService), SecurityFilter.class); + .addFilterAfter(new AddNewUserFilter(pac4jConfigurationProperties, userRepository, roleRepository, emailService), SecurityFilter.class); http.authorizeRequests().anyRequest().fullyAuthenticated(); diff --git a/pac4j-module/src/main/resources/META-INF/spring.factories b/pac4j-module/src/main/resources/META-INF/spring.factories index 90f792d84..f46ede845 100644 --- a/pac4j-module/src/main/resources/META-INF/spring.factories +++ b/pac4j-module/src/main/resources/META-INF/spring.factories @@ -1,4 +1,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ net.unicon.shibui.pac4j.Pac4jConfiguration,\ net.unicon.shibui.pac4j.WebSecurity,\ - net.unicon.shibui.pac4j.Pac4jConfigurationProperties \ No newline at end of file + net.unicon.shibui.pac4j.Pac4jConfigurationProperties,\ + net.unicon.shibui.pac4j.CustomPropertiesConfiguration \ No newline at end of file diff --git a/pac4j-module/src/main/resources/application.yml b/pac4j-module/src/main/resources/application.yml new file mode 100644 index 000000000..4042fd939 --- /dev/null +++ b/pac4j-module/src/main/resources/application.yml @@ -0,0 +1,7 @@ +shibui: + pac4j: + saml2ProfileMapping: + username: urn:oid:0.9.2342.19200300.100.1.3 + firstName: givenName + lastName: sn + email: mail \ No newline at end of file diff --git a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy index 16c5fa42c..9a824e97e 100644 --- a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy +++ b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy @@ -5,6 +5,10 @@ import edu.internet2.tier.shibboleth.admin.ui.security.model.User import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository import edu.internet2.tier.shibboleth.admin.ui.service.EmailService +import org.pac4j.saml.profile.SAML2Profile +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.context.SpringBootTest import org.springframework.security.core.Authentication import org.springframework.security.core.context.SecurityContext import org.springframework.security.core.context.SecurityContextHolder @@ -18,6 +22,8 @@ import javax.servlet.http.HttpServletResponse /** * @author Bill Smith (wsmith@unicon.net) */ +@SpringBootTest(classes = [Pac4jConfigurationProperties]) +@EnableConfigurationProperties([Pac4jConfigurationProperties]) class AddNewUserFilterTests extends Specification { UserRepository userRepository = Mock() @@ -30,18 +36,33 @@ class AddNewUserFilterTests extends Specification { SecurityContext securityContext = Mock() Authentication authentication = Mock() + SAML2Profile saml2Profile = Mock() + + @Autowired + Pac4jConfigurationProperties pac4jConfigurationProperties + + Pac4jConfigurationProperties.SAML2ProfileMapping saml2ProfileMapping @Subject - AddNewUserFilter addNewUserFilter = new AddNewUserFilter(userRepository, roleRepository, emailService) + AddNewUserFilter addNewUserFilter def setup() { SecurityContextHolder.setContext(securityContext) securityContext.getAuthentication() >> authentication + authentication.getPrincipal() >> saml2Profile + + addNewUserFilter = new AddNewUserFilter(pac4jConfigurationProperties, userRepository, roleRepository, emailService) + saml2ProfileMapping = pac4jConfigurationProperties.saml2ProfileMapping } def "new users are redirected"() { given: - authentication.getName() >> 'newUser' + ['Username': 'newUser', + 'FirstName': 'New', + 'LastName': 'User', + 'Email': 'newuser@institution.edu'].each { key, value -> + saml2Profile.getAttribute(saml2ProfileMapping."get${key}"()) >> [value] + } userRepository.findByUsername('newUser') >> Optional.empty() roleRepository.findByName('ROLE_NONE') >> Optional.of(new Role('ROLE_NONE')) @@ -50,14 +71,14 @@ class AddNewUserFilterTests extends Specification { then: 1 * roleRepository.save(_) - 1 * userRepository.save(_) + 1 * userRepository.save(_ as User) >> { User user -> user } 1 * emailService.sendNewUserMail('newUser') 1 * response.sendRedirect("/unsecured/error.html") } def "existing users are not redirected"() { given: - authentication.getName() >> 'existingUser' + saml2Profile.getAttribute(saml2ProfileMapping.getUsername()) >> ['existingUser'] userRepository.findByUsername('existingUser') >> Optional.of(new User().with { it.username = 'existingUser' it.roles = [new Role('ROLE_USER')]