To test a secured Spring application with Spring Security and LDAP authentication, we need to take the following steps:
- Activate and configure your embedded LDAP server for your tests.
- Define users and roles of your choice for your embedded LDAP server.
- Write Spring Tests.
Activate and configure your embedded LDAP server
There are at least the two following possibilities to start an embedded LDAP server with Spring:
- With Spring Boot
- With Spring Security
With Spring Boot
Spring Boot starts for each application context an embedded LDAP server automatically if Spring Boot finds an LDAP server implementation in the classpath and you declare some configuration properties in the application.properties. For the LDAP server implementation called unboundid, you need to declare at least the following properties:
spring.ldap.embedded.ldif=classpath:bootstrap.ldif
spring.ldap.embedded.base-dn=dc=springframework,dc=org
# Further properties are:
spring.ldap.embedded.port
spring.ldap.embedded.validation.enabled
spring.ldap.embedded.credential.username
spring.ldap.embedded.credential.password
spring.ldap.embedded.ur
As highlighted above, Spring Boot starts an embedded LDAP server for each application context. Logically, that means, it starts an embedded LDAP server for each test class. Practically, this is not always true since Spring Boot caches and reuses application contexts. However, you should always expect that there is more than one LDAP server running while executing your tests. For this reason, you may not declare a port for your LDAP server. In this way, it will automatically uses a free port. Otherwise, your tests will fail with “Address already in use”.
With Spring Security
If you want to use – for your tests – an embedded LDAP server as your central authentication management system with Spring Security, then you need to configure Spring Security as follows. Tip: Use two Spring Security configuration classes: one in your src/main/java and one in your src/test/java. The one in src/main/java could simply connect to an existing, non-embedded LDAP server. The one in src/test/java starts one embedded LDAP server for each new test context.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityTestConfiguration extends WebSecurityConfigurerAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(SecurityTestConfiguration.class);
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
// @formatter:off
auth.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.groupSearchBase("ou=groups")
.contextSource() // starts an LDAP server if url is not provided
.ldif("classpath:bootstrap-spring-security.ldif")
.root("dc=springframework,dc=org")
// automatically adds the entry indicated by .root()
.and()
.passwordCompare()
.passwordEncoder(passwordEncoder)
.passwordAttribute("userPassword")
.and();
// @formatter:on
LOGGER.info("Security configuration loaded.");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and();
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic();
// @formatter:on
}
}
If you do not declare a URL, but an LDIF file path instead, then Spring Security starts an embedded LDAP server automatically – again – for each application context.
One very important difference to Spring Boot is that the root entry is automatically created by Spring Security! So, your LDIF file must not contain the root entry. Otherwise, your application or your tests will fail with “An entry with DN ‘<dn of your root entry>’ already exists in the server.” We highlight this critical root entry in the next section.
Define users and roles of your choice for your embedded LDAP server
Below, you find an example LDIF file. The first entry is the root entry whose dn has to be used as value for the property spring.ldap.embedded.base-dn or, respectively, as argument for the method root(). Note that Spring Security automatically adds the root entry to the LDAP server. Hence, your LDIF file may not contain the root entry.
dn: dc=springframework,dc=org
objectclass: top
objectclass: domain
objectclass: extensibleObject
dc: springframework
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups
dn: ou=subgroups,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: subgroups
dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people
dn: ou=space cadets,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: space cadets
dn: ou=\"quoted people\",dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: "quoted people"
dn: ou=otherpeople,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: otherpeople
dn: uid=ben,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Ben Alex
sn: Alex
uid: ben
userPassword: {SHA}nFCebWjxfaLbHHG1Qk5UU4trbvQ=
dn: uid=bob,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Bob Hamilton
sn: Hamilton
uid: bob
userPassword: {noop}bobspassword
dn: uid=joe,ou=otherpeople,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Joe Smeth
sn: Smeth
uid: joe
userPassword: joespassword
dn: cn=mouse\, jerry,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Mouse, Jerry
sn: Mouse
uid: jerry
userPassword: jerryspassword
dn: cn=slash/guy,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: slash/guy
sn: Slash
uid: slashguy
userPassword: slashguyspassword
dn: cn=quote\"guy,ou=\"quoted people\",dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: quote\"guy
sn: Quote
uid: quoteguy
userPassword: quoteguyspassword
dn: uid=space cadet,ou=space cadets,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Space Cadet
sn: Cadet
uid: space cadet
userPassword: spacecadetspassword
dn: cn=developers,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: developers
ou: developer
uniqueMember: uid=ben,ou=people,dc=springframework,dc=org
uniqueMember: uid=bob,ou=people,dc=springframework,dc=org
dn: cn=managers,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: managers
ou: manager
uniqueMember: uid=ben,ou=people,dc=springframework,dc=org
uniqueMember: cn=mouse\, jerry,ou=people,dc=springframework,dc=org
dn: cn=submanagers,ou=subgroups,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: submanagers
ou: submanager
uniqueMember: uid=ben,ou=people,dc=springframework,dc=org
Write Spring Tests
And here are two example tests which tests the same thing but each with a different application context (see the different class annotations). As you will notice in the console output, two LDAP servers with different ports will be started and the tests will pass.
import static org.hamcrest.Matchers.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class HelloController1Test {
@Autowired
private MockMvc mvc;
@Test
public void getHelloWithValidLogin() throws Exception {
// @formatter:off
mvc.perform(get("/").with(httpBasic("bob", "bobspassword")).with(csrf()))
.andExpect(status().isOk())
.andExpect(content().string(equalTo("Greetings from Spring Boot!")));
// @formatter:on
}
@Test
public void getHelloWithInvalidPassword() throws Exception {
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/")
.accept(MediaType.APPLICATION_JSON);
mvc.perform(requestBuilder.with(httpBasic("bob", "wrong password"))).andExpect(status().isUnauthorized());
}
}
import static org.hamcrest.Matchers.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.ldap.AutoConfigureDataLdap;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import chw.tutorial.springboot.SecurityTestConfiguration;
@RunWith(SpringRunner.class)
@WebMvcTest(value = HelloController.class)
@Import(SecurityTestConfiguration.class)
@AutoConfigureDataLdap
public class HelloController3Test {
@Autowired
private MockMvc mvc;
@Test
public void getHelloWithValidLogin() throws Exception {
// @formatter:off
mvc.perform(get("/").with(httpBasic("bob", "bobspassword")).with(csrf()))
.andExpect(status().isOk())
.andExpect(content().string(equalTo("Greetings from Spring Boot!")));
// @formatter:on
}
@Test
public void getHelloWithInvalidPassword() throws Exception {
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/")
.accept(MediaType.APPLICATION_JSON);
mvc.perform(requestBuilder.with(httpBasic("bob", "wrong password"))).andExpect(status().isUnauthorized());
}
}
Pitfalls
As described above, you may get one of the following error messages if you have not correctly configured your embedded LDAP server. In this blog post, we explained why these errors occur and gave appropriate solutions.
- “Address already in use: NET_bind”
- “com.unboundid.ldap.sdk.LDAPException: An entry with DN ‘dc=springframework,dc=org’ already exists in the server.”