diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index a54edbd15c8..9274704bb44 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -74,6 +74,7 @@ dependencies { testImplementation "org.apache.directory.server:apacheds-protocol-ldap" testImplementation "org.apache.directory.server:apacheds-server-jndi" testImplementation 'org.apache.directory.shared:shared-ldap' + testImplementation "com.unboundid:unboundid-ldapsdk" testImplementation 'org.eclipse.persistence:javax.persistence' testImplementation 'org.hibernate:hibernate-entitymanager' testImplementation 'org.hsqldb:hsqldb' diff --git a/config/src/integration-test/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBeanITests.java b/config/src/integration-test/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBeanITests.java new file mode 100644 index 00000000000..d20b3e302cc --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBeanITests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.test.web.servlet.MockMvc; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; + +@ExtendWith(SpringTestContextExtension.class) +public class EmbeddedLdapServerContextSourceFactoryBeanITests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private MockMvc mockMvc; + + @Test + public void contextSourceFactoryBeanWhenEmbeddedServerThenAuthenticates() throws Exception { + this.spring.register(FromEmbeddedLdapServerConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void contextSourceFactoryBeanWhenPortZeroThenAuthenticates() throws Exception { + this.spring.register(PortZeroConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void contextSourceFactoryBeanWhenCustomLdifAndRootThenAuthenticates() throws Exception { + this.spring.register(CustomLdifAndRootConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("pg").password("password")).andExpect(authenticated().withUsername("pg")); + } + + @Test + public void contextSourceFactoryBeanWhenCustomManagerDnThenAuthenticates() throws Exception { + this.spring.register(CustomManagerDnConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void contextSourceFactoryBeanWhenManagerDnAndNoPasswordThenException() { + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> this.spring.register(CustomManagerDnNoPasswordConfig.class).autowire()) + .withRootCauseInstanceOf(IllegalStateException.class) + .withMessageContaining("managerPassword is required if managerDn is supplied"); + } + + @EnableWebSecurity + static class FromEmbeddedLdapServerConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + return EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer(); + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class PortZeroConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + EmbeddedLdapServerContextSourceFactoryBean factoryBean = EmbeddedLdapServerContextSourceFactoryBean + .fromEmbeddedLdapServer(); + factoryBean.setPort(0); + return factoryBean; + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomLdifAndRootConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + EmbeddedLdapServerContextSourceFactoryBean factoryBean = EmbeddedLdapServerContextSourceFactoryBean + .fromEmbeddedLdapServer(); + factoryBean.setLdif("classpath*:test-server2.xldif"); + factoryBean.setRoot("dc=monkeymachine,dc=co,dc=uk"); + return factoryBean; + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=gorillas"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomManagerDnConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + EmbeddedLdapServerContextSourceFactoryBean factoryBean = EmbeddedLdapServerContextSourceFactoryBean + .fromEmbeddedLdapServer(); + factoryBean.setManagerDn("uid=admin,ou=system"); + factoryBean.setManagerPassword("secret"); + return factoryBean; + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, NoOpPasswordEncoder.getInstance()); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomManagerDnNoPasswordConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + EmbeddedLdapServerContextSourceFactoryBean factoryBean = EmbeddedLdapServerContextSourceFactoryBean + .fromEmbeddedLdapServer(); + factoryBean.setManagerDn("uid=admin,ou=system"); + return factoryBean; + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, NoOpPasswordEncoder.getInstance()); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + +} diff --git a/config/src/integration-test/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactoryITests.java b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactoryITests.java new file mode 100644 index 00000000000..2b333441c09 --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactoryITests.java @@ -0,0 +1,241 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.server.ApacheDSContainer; +import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.UserDetailsContextMapper; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.Mockito.mock; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; + +@ExtendWith(SpringTestContextExtension.class) +public class LdapBindAuthenticationManagerFactoryITests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private MockMvc mockMvc; + + @Test + public void authenticationManagerFactoryWhenFromContextSourceThenAuthenticates() throws Exception { + this.spring.register(FromContextSourceConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void ldapAuthenticationProviderCustomLdapAuthoritiesPopulator() throws Exception { + CustomAuthoritiesPopulatorConfig.LAP = new DefaultLdapAuthoritiesPopulator(mock(LdapContextSource.class), + null) { + @Override + protected Set getAdditionalRoles(DirContextOperations user, String username) { + return new HashSet<>(AuthorityUtils.createAuthorityList("ROLE_EXTRA")); + } + }; + + this.spring.register(CustomAuthoritiesPopulatorConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")).andExpect( + authenticated().withAuthorities(Collections.singleton(new SimpleGrantedAuthority("ROLE_EXTRA")))); + } + + @Test + public void authenticationManagerFactoryWhenCustomAuthoritiesMapperThenUsed() throws Exception { + CustomAuthoritiesMapperConfig.AUTHORITIES_MAPPER = ((authorities) -> AuthorityUtils + .createAuthorityList("ROLE_CUSTOM")); + + this.spring.register(CustomAuthoritiesMapperConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")).andExpect( + authenticated().withAuthorities(Collections.singleton(new SimpleGrantedAuthority("ROLE_CUSTOM")))); + } + + @Test + public void authenticationManagerFactoryWhenCustomUserDetailsContextMapperThenUsed() throws Exception { + CustomUserDetailsContextMapperConfig.CONTEXT_MAPPER = new UserDetailsContextMapper() { + @Override + public UserDetails mapUserFromContext(DirContextOperations ctx, String username, + Collection authorities) { + return User.withUsername("other").password("password").roles("USER").build(); + } + + @Override + public void mapUserToContext(UserDetails user, DirContextAdapter ctx) { + } + }; + + this.spring.register(CustomUserDetailsContextMapperConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("other")); + } + + @Test + public void authenticationManagerFactoryWhenCustomUserDnPatternsThenUsed() throws Exception { + this.spring.register(CustomUserDnPatternsConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void authenticationManagerFactoryWhenCustomUserSearchThenUsed() throws Exception { + this.spring.register(CustomUserSearchConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @EnableWebSecurity + static class FromContextSourceConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomAuthoritiesMapperConfig extends BaseLdapServerConfig { + + static GrantedAuthoritiesMapper AUTHORITIES_MAPPER; + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + factory.setAuthoritiesMapper(AUTHORITIES_MAPPER); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomAuthoritiesPopulatorConfig extends BaseLdapServerConfig { + + static LdapAuthoritiesPopulator LAP; + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + factory.setLdapAuthoritiesPopulator(LAP); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomUserDetailsContextMapperConfig extends BaseLdapServerConfig { + + static UserDetailsContextMapper CONTEXT_MAPPER; + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + factory.setUserDetailsContextMapper(CONTEXT_MAPPER); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomUserDnPatternsConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomUserSearchConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserSearchFilter("uid={0}"); + factory.setUserSearchBase("ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + abstract static class BaseLdapServerConfig implements DisposableBean { + + private ApacheDSContainer container; + + @Bean + ApacheDSContainer ldapServer() throws Exception { + this.container = new ApacheDSContainer("dc=springframework,dc=org", "classpath:/test-server.ldif"); + this.container.setPort(0); + return this.container; + } + + @Bean + BaseLdapPathContextSource contextSource(ApacheDSContainer container) { + int port = container.getLocalPort(); + return new DefaultSpringSecurityContextSource("ldap://localhost:" + port + "/dc=springframework,dc=org"); + } + + @Override + public void destroy() { + this.container.stop(); + } + + } + +} diff --git a/config/src/integration-test/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactoryITests.java b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactoryITests.java new file mode 100644 index 00000000000..350cf8405ce --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactoryITests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.server.ApacheDSContainer; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; + +@ExtendWith(SpringTestContextExtension.class) +public class LdapPasswordComparisonAuthenticationManagerFactoryITests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private MockMvc mockMvc; + + @Test + public void authenticationManagerFactoryWhenCustomPasswordEncoderThenUsed() throws Exception { + this.spring.register(CustomPasswordEncoderConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bcrypt").password("password")) + .andExpect(authenticated().withUsername("bcrypt")); + } + + @Test + public void authenticationManagerFactoryWhenCustomPasswordAttributeThenUsed() throws Exception { + this.spring.register(CustomPasswordAttributeConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bob")).andExpect(authenticated().withUsername("bob")); + } + + @EnableWebSecurity + static class CustomPasswordEncoderConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, new BCryptPasswordEncoder()); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomPasswordAttributeConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, NoOpPasswordEncoder.getInstance()); + factory.setPasswordAttribute("uid"); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + abstract static class BaseLdapServerConfig implements DisposableBean { + + private ApacheDSContainer container; + + @Bean + ApacheDSContainer ldapServer() throws Exception { + this.container = new ApacheDSContainer("dc=springframework,dc=org", "classpath:/test-server.ldif"); + this.container.setPort(0); + return this.container; + } + + @Bean + BaseLdapPathContextSource contextSource(ApacheDSContainer container) { + int port = container.getLocalPort(); + return new DefaultSpringSecurityContextSource("ldap://localhost:" + port + "/dc=springframework,dc=org"); + } + + @Override + public void destroy() { + this.container.stop(); + } + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/ldap/AbstractLdapAuthenticationManagerFactory.java b/config/src/main/java/org/springframework/security/config/ldap/AbstractLdapAuthenticationManagerFactory.java new file mode 100644 index 00000000000..16069f09a47 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ldap/AbstractLdapAuthenticationManagerFactory.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.ldap.authentication.AbstractLdapAuthenticator; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.UserDetailsContextMapper; + +/** + * Creates an {@link AuthenticationManager} that can perform LDAP authentication. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public abstract class AbstractLdapAuthenticationManagerFactory { + + AbstractLdapAuthenticationManagerFactory(BaseLdapPathContextSource contextSource) { + this.contextSource = contextSource; + } + + private BaseLdapPathContextSource contextSource; + + private String[] userDnPatterns; + + private LdapAuthoritiesPopulator ldapAuthoritiesPopulator; + + private GrantedAuthoritiesMapper authoritiesMapper; + + private UserDetailsContextMapper userDetailsContextMapper; + + private String userSearchFilter; + + private String userSearchBase = ""; + + /** + * Sets the {@link BaseLdapPathContextSource} used to perform LDAP authentication. + * @param contextSource the {@link BaseLdapPathContextSource} used to perform LDAP + * authentication + */ + public void setContextSource(BaseLdapPathContextSource contextSource) { + this.contextSource = contextSource; + } + + /** + * Gets the {@link BaseLdapPathContextSource} used to perform LDAP authentication. + * @return the {@link BaseLdapPathContextSource} used to perform LDAP authentication + */ + protected final BaseLdapPathContextSource getContextSource() { + return this.contextSource; + } + + /** + * Sets the {@link LdapAuthoritiesPopulator} used to obtain a list of granted + * authorities for an LDAP user. + * @param ldapAuthoritiesPopulator the {@link LdapAuthoritiesPopulator} to use + */ + public void setLdapAuthoritiesPopulator(LdapAuthoritiesPopulator ldapAuthoritiesPopulator) { + this.ldapAuthoritiesPopulator = ldapAuthoritiesPopulator; + } + + /** + * Sets the {@link GrantedAuthoritiesMapper} used for converting the authorities + * loaded from storage to a new set of authorities which will be associated to the + * {@link UsernamePasswordAuthenticationToken}. + * @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the + * user's authorities + */ + public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { + this.authoritiesMapper = authoritiesMapper; + } + + /** + * Sets a custom strategy to be used for creating the {@link UserDetails} which will + * be stored as the principal in the {@link Authentication}. + * @param userDetailsContextMapper the strategy instance + */ + public void setUserDetailsContextMapper(UserDetailsContextMapper userDetailsContextMapper) { + this.userDetailsContextMapper = userDetailsContextMapper; + } + + /** + * If your users are at a fixed location in the directory (i.e. you can work out the + * DN directly from the username without doing a directory search), you can use this + * attribute to map directly to the DN. It maps directly to the userDnPatterns + * property of AbstractLdapAuthenticator. The value is a specific pattern used to + * build the user's DN, for example "uid={0},ou=people". The key "{0}" must be present + * and will be substituted with the username. + * @param userDnPatterns the LDAP patterns for finding the usernames + */ + public void setUserDnPatterns(String... userDnPatterns) { + this.userDnPatterns = userDnPatterns; + } + + /** + * The LDAP filter used to search for users (optional). For example "(uid={0})". The + * substituted parameter is the user's login name. + * @param userSearchFilter the LDAP filter used to search for users + */ + public void setUserSearchFilter(String userSearchFilter) { + this.userSearchFilter = userSearchFilter; + } + + /** + * Search base for user searches. Defaults to "". Only used with + * {@link #setUserSearchFilter(String)}. + * @param userSearchBase search base for user searches + */ + public void setUserSearchBase(String userSearchBase) { + this.userSearchBase = userSearchBase; + } + + /** + * Returns the configured {@link AuthenticationManager} that can be used to perform + * LDAP authentication. + * @return the configured {@link AuthenticationManager} + */ + public final AuthenticationManager createAuthenticationManager() { + LdapAuthenticationProvider ldapAuthenticationProvider = getProvider(); + return new ProviderManager(ldapAuthenticationProvider); + } + + private LdapAuthenticationProvider getProvider() { + AbstractLdapAuthenticator authenticator = getAuthenticator(); + LdapAuthenticationProvider provider; + if (this.ldapAuthoritiesPopulator != null) { + provider = new LdapAuthenticationProvider(authenticator, this.ldapAuthoritiesPopulator); + } + else { + provider = new LdapAuthenticationProvider(authenticator); + } + if (this.authoritiesMapper != null) { + provider.setAuthoritiesMapper(this.authoritiesMapper); + } + if (this.userDetailsContextMapper != null) { + provider.setUserDetailsContextMapper(this.userDetailsContextMapper); + } + return provider; + } + + private AbstractLdapAuthenticator getAuthenticator() { + AbstractLdapAuthenticator authenticator = createDefaultLdapAuthenticator(); + if (this.userSearchFilter != null) { + authenticator.setUserSearch( + new FilterBasedLdapUserSearch(this.userSearchBase, this.userSearchFilter, this.contextSource)); + } + if (this.userDnPatterns != null && this.userDnPatterns.length > 0) { + authenticator.setUserDnPatterns(this.userDnPatterns); + } + authenticator.afterPropertiesSet(); + return authenticator; + } + + /** + * Allows subclasses to supply the default {@link AbstractLdapAuthenticator}. + * @return the {@link AbstractLdapAuthenticator} that will be configured for LDAP + * authentication + */ + protected abstract T createDefaultLdapAuthenticator(); + +} diff --git a/config/src/main/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBean.java b/config/src/main/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBean.java new file mode 100644 index 00000000000..4a8c2d56d40 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBean.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import java.io.IOException; +import java.net.ServerSocket; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.Lifecycle; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.server.EmbeddedLdapServerContainer; +import org.springframework.security.ldap.server.UnboundIdContainer; +import org.springframework.util.ClassUtils; + +/** + * Creates a {@link DefaultSpringSecurityContextSource} used to perform LDAP + * authentication and starts and in-memory LDAP server. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public class EmbeddedLdapServerContextSourceFactoryBean + implements FactoryBean, DisposableBean, ApplicationContextAware { + + private static final String UNBOUNDID_CLASSNAME = "com.unboundid.ldap.listener.InMemoryDirectoryServer"; + + private static final int DEFAULT_PORT = 33389; + + private static final int RANDOM_PORT = 0; + + private Integer port; + + private String ldif = "classpath*:*.ldif"; + + private String root = "dc=springframework,dc=org"; + + private ApplicationContext context; + + private String managerDn; + + private String managerPassword; + + private EmbeddedLdapServerContainer container; + + /** + * Create an EmbeddedLdapServerContextSourceFactoryBean that will use an embedded LDAP + * server to perform LDAP authentication. This requires a dependency on + * `com.unboundid:unboundid-ldapsdk`. + * @return the EmbeddedLdapServerContextSourceFactoryBean + */ + public static EmbeddedLdapServerContextSourceFactoryBean fromEmbeddedLdapServer() { + return new EmbeddedLdapServerContextSourceFactoryBean(); + } + + /** + * Specifies an LDIF to load at startup for an embedded LDAP server. The default is + * "classpath*:*.ldif". + * @param ldif the ldif to load at startup for an embedded LDAP server. + */ + public void setLdif(String ldif) { + this.ldif = ldif; + } + + /** + * The port to connect to LDAP to (the default is 33389 or random available port if + * unavailable). Supplying 0 as the port indicates that a random available port should + * be selected. + * @param port the port to connect to + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Optional root suffix for the embedded LDAP server. Default is + * "dc=springframework,dc=org". + * @param root root suffix for the embedded LDAP server + */ + public void setRoot(String root) { + this.root = root; + } + + /** + * Username (DN) of the "manager" user identity (i.e. "uid=admin,ou=system") which + * will be used to authenticate to an LDAP server. If omitted, anonymous access will + * be used. + * @param managerDn the username (DN) of the "manager" user identity used to + * authenticate to a LDAP server. + */ + public void setManagerDn(String managerDn) { + this.managerDn = managerDn; + } + + /** + * The password for the manager DN. This is required if the + * {@link #setManagerDn(String)} is specified. + * @param managerPassword password for the manager DN + */ + public void setManagerPassword(String managerPassword) { + this.managerPassword = managerPassword; + } + + @Override + public DefaultSpringSecurityContextSource getObject() throws Exception { + if (!ClassUtils.isPresent(UNBOUNDID_CLASSNAME, getClass().getClassLoader())) { + throw new IllegalStateException("Embedded LDAP server is not provided"); + } + this.container = getContainer(); + this.port = this.container.getPort(); + DefaultSpringSecurityContextSource contextSourceFromProviderUrl = new DefaultSpringSecurityContextSource( + "ldap://127.0.0.1:" + this.port + "/" + this.root); + if (this.managerDn != null) { + contextSourceFromProviderUrl.setUserDn(this.managerDn); + if (this.managerPassword == null) { + throw new IllegalStateException("managerPassword is required if managerDn is supplied"); + } + contextSourceFromProviderUrl.setPassword(this.managerPassword); + } + contextSourceFromProviderUrl.afterPropertiesSet(); + return contextSourceFromProviderUrl; + } + + @Override + public Class getObjectType() { + return DefaultSpringSecurityContextSource.class; + } + + @Override + public void destroy() { + if (this.container instanceof Lifecycle) { + ((Lifecycle) this.container).stop(); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.context = applicationContext; + } + + private EmbeddedLdapServerContainer getContainer() { + if (!ClassUtils.isPresent(UNBOUNDID_CLASSNAME, getClass().getClassLoader())) { + throw new IllegalStateException("Embedded LDAP server is not provided"); + } + UnboundIdContainer unboundIdContainer = new UnboundIdContainer(this.root, this.ldif); + unboundIdContainer.setApplicationContext(this.context); + unboundIdContainer.setPort(getEmbeddedServerPort()); + unboundIdContainer.afterPropertiesSet(); + return unboundIdContainer; + } + + private int getEmbeddedServerPort() { + if (this.port == null) { + this.port = getDefaultEmbeddedServerPort(); + } + return this.port; + } + + private int getDefaultEmbeddedServerPort() { + try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) { + return serverSocket.getLocalPort(); + } + catch (IOException ex) { + return RANDOM_PORT; + } + } + +} diff --git a/config/src/main/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactory.java b/config/src/main/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactory.java new file mode 100644 index 00000000000..a62fbfab44a --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.ldap.authentication.BindAuthenticator; + +/** + * Creates an {@link AuthenticationManager} that can perform LDAP authentication using + * bind authentication. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public class LdapBindAuthenticationManagerFactory extends AbstractLdapAuthenticationManagerFactory { + + public LdapBindAuthenticationManagerFactory(BaseLdapPathContextSource contextSource) { + super(contextSource); + } + + @Override + protected BindAuthenticator createDefaultLdapAuthenticator() { + return new BindAuthenticator(getContextSource()); + } + +} diff --git a/config/src/main/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactory.java b/config/src/main/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactory.java new file mode 100644 index 00000000000..19c14f998df --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactory.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.ldap.authentication.PasswordComparisonAuthenticator; +import org.springframework.util.Assert; + +/** + * Creates an {@link AuthenticationManager} that can perform LDAP authentication using + * password comparison. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public class LdapPasswordComparisonAuthenticationManagerFactory + extends AbstractLdapAuthenticationManagerFactory { + + private PasswordEncoder passwordEncoder; + + private String passwordAttribute; + + public LdapPasswordComparisonAuthenticationManagerFactory(BaseLdapPathContextSource contextSource, + PasswordEncoder passwordEncoder) { + super(contextSource); + setPasswordEncoder(passwordEncoder); + } + + /** + * Specifies the {@link PasswordEncoder} to be used when authenticating with password + * comparison. + * @param passwordEncoder the {@link PasswordEncoder} to use + */ + public void setPasswordEncoder(PasswordEncoder passwordEncoder) { + Assert.notNull(passwordEncoder, "passwordEncoder must not be null."); + this.passwordEncoder = passwordEncoder; + } + + /** + * The attribute in the directory which contains the user password. Only used when + * authenticating with password comparison. Defaults to "userPassword". + * @param passwordAttribute the attribute in the directory which contains the user + * password + */ + public void setPasswordAttribute(String passwordAttribute) { + this.passwordAttribute = passwordAttribute; + } + + @Override + protected PasswordComparisonAuthenticator createDefaultLdapAuthenticator() { + PasswordComparisonAuthenticator ldapAuthenticator = new PasswordComparisonAuthenticator(getContextSource()); + if (this.passwordAttribute != null) { + ldapAuthenticator.setPasswordAttributeName(this.passwordAttribute); + } + ldapAuthenticator.setPasswordEncoder(this.passwordEncoder); + return ldapAuthenticator; + } + +} diff --git a/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/users.ldif b/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/users.ldif index 222e03793c4..ca639f10967 100644 --- a/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/users.ldif +++ b/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/users.ldif @@ -38,6 +38,16 @@ sn: Wombat uid: scott userPassword: wombat +dn: uid=bcrypt,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: BCrypt user +sn: BCrypt +uid: bcrypt +userPassword: $2a$10$FBAKClV1zBIOOC9XMXf3AO8RoGXYVYsfvUdoLxGkd/BnXEn4tqT3u + dn: cn=user,ou=groups,dc=springframework,dc=org objectclass: top objectclass: groupOfNames diff --git a/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java b/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java index eb1eb79093d..379faed9655 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java +++ b/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,7 +77,8 @@ * supported with no GA version to replace it. */ @Deprecated -public class ApacheDSContainer implements InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { +public class ApacheDSContainer + implements EmbeddedLdapServerContainer, InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { private final Log logger = LogFactory.getLog(getClass()); @@ -177,10 +178,12 @@ public void setWorkingDirectory(File workingDir) { this.service.setWorkingDirectory(workingDir); } + @Override public void setPort(int port) { this.port = port; } + @Override public int getPort() { return this.port; } diff --git a/ldap/src/main/java/org/springframework/security/ldap/server/EmbeddedLdapServerContainer.java b/ldap/src/main/java/org/springframework/security/ldap/server/EmbeddedLdapServerContainer.java new file mode 100644 index 00000000000..2ca55f44eda --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/server/EmbeddedLdapServerContainer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.server; + +/** + * Provides lifecycle services for an embedded LDAP server. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public interface EmbeddedLdapServerContainer { + + /** + * Returns the embedded LDAP server port. + * @return the embedded LDAP server port + */ + int getPort(); + + /** + * The embedded LDAP server port to connect to. Supplying 0 as the port indicates that + * a random available port should be selected. + * @param port the port to connect to + */ + void setPort(int port); + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java b/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java index 269b8adae1f..f8c1d0d84ac 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java +++ b/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,8 @@ /** * @author EddĂș MelĂ©ndez */ -public class UnboundIdContainer implements InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { +public class UnboundIdContainer + implements EmbeddedLdapServerContainer, InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { private InMemoryDirectoryServer directoryServer; @@ -57,10 +58,12 @@ public UnboundIdContainer(String defaultPartitionSuffix, String ldif) { this.ldif = ldif; } + @Override public int getPort() { return this.port; } + @Override public void setPort(int port) { this.port = port; }