Add LDAP factory beans

Issue gh-10138
This commit is contained in:
Eleftheria Stein 2021-10-04 15:21:34 +02:00
parent 2894ed318d
commit 6b56071c08
9 changed files with 1035 additions and 0 deletions

View File

@ -73,6 +73,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 'jakarta.persistence:jakarta.persistence-api'
testImplementation 'org.hibernate:hibernate-core-jakarta'
testImplementation 'org.hsqldb:hsqldb'

View File

@ -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();
}
}
}

View File

@ -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<GrantedAuthority> 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<? extends GrantedAuthority> 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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<T extends AbstractLdapAuthenticator> {
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();
}

View File

@ -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<DefaultSpringSecurityContextSource>, 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;
}
}
}

View File

@ -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<BindAuthenticator> {
public LdapBindAuthenticationManagerFactory(BaseLdapPathContextSource contextSource) {
super(contextSource);
}
@Override
protected BindAuthenticator createDefaultLdapAuthenticator() {
return new BindAuthenticator(getContextSource());
}
}

View File

@ -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<PasswordComparisonAuthenticator> {
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;
}
}

View File

@ -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