Spring Security With LDAP Authentication
In this article, an author shares their experience with Spring Security on a demo purpose application of which the source code is available on Github.
Join the DZone community and get the full member experience.
Join For FreeIn real-world applications on production systems; security is a top concern. This security concern is separated into two: Authentication and Authorization. Authentication mainly focuses on who can access to the application and authorization focuses on who can access which resources based on their authorities.
In this article I will share my experience with Spring Security on a demo purpose application of which source code is available on Github. In this project, I used LDAP Authentication method for authentication. For the token generation mechanism, I used a private/public RSA key method. Private key is used for token generation and public key is used for token validation.
The aim of the project is to return a jwt token to the login user and the user will use this token to access resources in subsequent requests. The demo application offers two rest endpoints: one is for the authentication mechanism to take place (/auth-server) and the other is a representative rest resource (/resource). Anyone can access the "/auth-server" endpoint, but the access is restricted to the "/resource" endpoint. When the user logs into the system; there will be a rest call to the "/auth-server" endpoint for LDAP authentication. For successful authentication, there will be a jwt token in the response. The second rest call will be to the actual resource endpoint "/resource" with the token in the header as an Authentication object. We will see how this is done in the codebase.
It is very simple to enable Spring Security in your projects. You can do that by only adding a few dependencies. Let’s start to examine our demo application to see how things are done.
First; the two dependencies below will be added to the pom.xml to enable Spring Security. You can do this also when generating the project structure in spring initializer by adding Spring Security Component.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
Apart from these, we need to add additional dependencies for LDAP. Spring gives us the opportunity to use an embedded LDAP server.
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
Security configuration is done based on a configuration file:
@Configuration
@EnableWebSecurity
public class DemoSecurityConfiguration extends WebSecurityConfigurerAdapter
{
private Environment env;
public DemoSecurityConfiguration(Environment env){
this.env = env;
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.authenticationProvider(new LdapAuthenticationProvider(env)).eraseCredentials(false);
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
httpSecurity
.authorizeRequests()
.antMatchers("/auth-server").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.disable()
.httpBasic();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public JwtTokenProvider provider(){
return new JwtTokenProvider();
}
}
The @EnableWebSecurity annotation is used for enabling spring web security. Configuration class must extend from Spring’s WebSecurityConfigurerAdapter class. First, the configure method is needed for defining the authentication method. The second configure method is to define security properties. For authentication, I created a custom LdapAuthenticationProvider class. Inside the second configure method, we are authorizing any request to the endpoint “/auth-server,” but the other endpoints should be authenticated first. We disable CSRF and we use an HTTP basic authentication.
For an embedded LDAP server, in the application.properties file we should define some properties:
spring.ldap.embedded.port=8389
spring.ldap.embedded.ldif=classpath:ldap.ldif
spring.ldap.embedded.base-dn=dc=springframework,dc=org
We will read the LDAP structure from a LDIF file(ldap.ldif); LDAP server’s port will be 8389; the root directory will be dc=springframework,dc=org.
Below is the custom LDAP authentication provider class:
@Component
public class LdapAuthenticationProvider implements AuthenticationProvider
{
private Environment environment;
public LdapAuthenticationProvider(Environment environment) {
this.environment = environment;
}
private LdapContextSource contextSource;
private LdapTemplate ldapTemplate;
private void initContext()
{
contextSource = new LdapContextSource();
contextSource.setUrl(environment.getProperty("ldap.server.url"));
contextSource.setAnonymousReadOnly(true);
contextSource.setUserDn("uid={0},ou=people");
contextSource.afterPropertiesSet();
ldapTemplate = new LdapTemplate(contextSource);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
initContext();
Filter filter = new EqualsFilter("uid", authentication.getName());
Boolean authenticate = ldapTemplate.authenticate(LdapUtils.emptyLdapName(), filter.encode(), authentication.getCredentials().toString());
if (authenticate)
{
UserDetails userDetails = new User(authentication.getName(), authentication.getCredentials().toString()
, new ArrayList<>());
Authentication auth = new UsernamePasswordAuthenticationToken(userDetails,
authentication.getCredentials().toString(), new ArrayList<>());
return auth;
}
else
{
return null;
}
}
@Override
public boolean supports(Class<?> authentication)
{
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
The first LdapContext is prepared after an EqualsFilter operation is done based on the uid(username). If a match happens with the correct password, then an Authentication object is prepared and returned. Otherwise, it returns null.
After successful authentication, the JWTTokenProvider class prepares a token with below method: generateToken:
public class JwtTokenProvider
{
public static String generateToken(String userName) throws Exception
{
Instant now = Instant.now();
String jwtToken = Jwts.builder()
.claim("name", userName)
.setSubject("jwt_token")
.setId(UUID.randomUUID().toString())
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(now.plus(5l, ChronoUnit.MINUTES)))
.signWith(SignatureAlgorithm.RS512, getPrivateKey())
.compact();
return jwtToken;
}
private static Key getPrivateKey() throws Exception
{
ClassLoader classLoader = JwtTokenProvider.class.getClassLoader();
File file = new File(classLoader.getResource("private.key").getFile());
byte[] rsaPrivateKeyArr = FileUtils.readFileToByteArray(file);
String rsaPrivateKey = new String(rsaPrivateKeyArr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getMimeDecoder().decode(rsaPrivateKey));
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(keySpec);
}
}
I used private/public RSA key method to generate the token. I generated a private key with the OpenSSL genrsa command and put this private key to a resource file as "private.key" in/resources folder. The jwt token is prepared based on this private key in generateToken method. Also, an expiration time can be set to the token with the setExpiration method.
In postman, we can call the rest endpoints as follows:
Thanks for reading!
You can find the source code at Github.
Opinions expressed by DZone contributors are their own.
Comments