Spring Security LDAP Authentication

author-image  By Dhiraj, 29 December, 2018   2K

In this tutorial, we will learn about securing our spring boot application with spring security LDAP authentication. We will have multiple users with role based(ADMIN, USER) entries in a ldif file and REST APIs exposed with the help of a controller class. Whenever a user tries to access the secured endpoint, the user will be redirected to a login page and after a successfull login the user will be allowed to access the secured APIs.

In this example, we will be using an in-memory open source LDAP server - unboundid to communicate with LDAP directory servers and the user info will be saved into MySQL DB. Also, with the release of spring boot 2.1.1, the LdapShaPasswordEncoder is depricated and hence we will be using BCryptPasswordEncoder to securely save our passwords. You can use this free online bcrypt tool to generate you custom passwords.The example that we will be creating here will a very simple authentication example with LDAP. You can visit my previous articles to add other authentication protocols in this example such as OAUTH2 role based authorization or JWT token based OAUTH2 Authorization.

Technologies Used

  • Maven
  • Java 8
  • Spring Boot 2
  • MYSQL
  • unboundid

Project Structure

We can generate our basic spring boot project from start.spring.io and then add then add other maven dependencies to it.

spring-boot-starter pom.xml
<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</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>
		<dependency>
			<groupId>com.unboundid</groupId>
			<artifactId>unboundid-ldapsdk</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
	</dependencies>

LDAP Brief Description

LDAP is a datastore that stores the data in a hierarchical format. It is used to store attribute centric data. For example a user can have multiple attributes related to it such as first name, last name, username, email, department, roles. So now if we want to save these information in a DB there can be issues with normalisation and we require multiple tables to store these details. For example, we will have a table called User_Profile to save basic user details. Similarly, we will have Roles table to save the roles associated with the user and similar setup for department too. Since, LDAP stores the data in a hierarchical format these issues are not common in LDAP. This hierarchical format is called DIT Directory Information Tree.

Different LDIF Attributes

First of all, let us discuss about the different attributes of LDAP or a LDIF file. ldif is the textual representation of LDAP.

dc dc(domain component) is the top level or root of the hierarchical data that we save into LDAP. Below is an example of dc definition.

dc=devglan,dc=com

ou - ou stands for organisational unit. For example, ou: finance is an entry inside dc.

cn - ou stands for common name. For example, cn: John Doe is an entry inside ou.

dn - dn is distinguished name and it uniquely idenntifies a particular entry in LDAP server. Following is a dn that uniqely identifies the entry of John Doe. To define a dn, start with the cn and navigate back to the top level to dc.

cn=John Doe,ou=finance,dc=devglan,dc=com
dn: dc=devglan,dc=com
objectclass: dcObject	
objectClass: organization
o: Devglan
dc: devglan

Here o and dc is the attribute where o is the organization.You can only use those attributes which are defined in the objectClass and hence if you want to use an attribute such as o then u need to define the object class of it.

In the following file, we have 2 users with username/password as john/john and mike/mike. User john has ADMIN role and mike has USER role.

users.ldif
dn: dc=devglan,dc=com
objectclass: top
objectclass: domain
objectclass: extensibleObject
dc: devglan

dn: ou=groups,dc=devglan,dc=com
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=people,dc=devglan,dc=com
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=john,ou=people,dc=devglan,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: John
sn: Doe
uid: john
userPassword: $2a$04$hgI8OjNQ8QwhphrfWmbvguTd4d4u7Iho972OOFEE7IefVonLLa3gi

dn: uid=mike,ou=people,dc=devglan,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Mike
sn: Hus
uid: mike
userPassword: $2a$04$Jz4L3PuP2zekBVH5ZN8kvuIVnYqQw09DpcW9kuPHb35G6SJ84M72O

dn: cn=admin,ou=groups,dc=devglan,dc=com
objectclass: top
objectclass: groupOfUniqueNames
cn: admin
uniqueMember: uid=john,ou=people,dc=devglan,dc=com

dn: cn=user,ou=groups,dc=devglan,dc=com
objectclass: top
objectclass: groupOfUniqueNames
cn: user
uniqueMember: uid=mike,ou=people,dc=devglan,dc=com

Controller Implementation

Now let us expose our REST endpoints with controller. Below is the implementation of our controller class that exposes endpoint to list user details.

UserController.java
package com.devglan.springsecurityldap.controller;

import com.devglan.springsecurityldap.dto.ApiResponse;
import com.devglan.springsecurityldap.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UserController {

    private static final Logger log = LoggerFactory.getLogger(UserController.class);

    public static final String SUCCESS = "success";
    public static final String ROLE_ADMIN = "ROLE_ADMIN";
    public static final String ROLE_USER = "ROLE_USER";

    @Autowired
    private UserService userService;

    @GetMapping
    public ApiResponse listUser(){
        log.info("received request to list user");
        return new ApiResponse(HttpStatus.OK, SUCCESS, userService.findAll());
    }

    @GetMapping(value = "/{id}")
    public ApiResponse getUser(@PathVariable long id){
        log.info("received request to update user %s");
        return new ApiResponse(HttpStatus.OK, SUCCESS, userService.findOne(id));
    }

}

UserDto.java
package com.devglan.springsecurityldap.dto;

import java.util.List;

public class UserDto {

    private long id;
    private String firstName;
    private String lastName;
    private String username;
    private String email;
  
}

ApiResponse.java
package com.devglan.springsecurityldap.dto;

import org.springframework.http.HttpStatus;

public class ApiResponse {
	
	private int status;
	privat String message;
	private Object result;

	public ApiResponse(HttpStatus status, String message, Object result){
	    this.status = status.value();
	    this.message = message;
	    this.result = result;
    }

}

Service Implementation

Below is our simple service class imlementation.

UserServiceImpl.java

@Transactional
@Service(value = "userService")
public class UserServiceImpl implements UserService {

    private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);

    @Autowired
    private UserDao userDao;

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    public List findAll() {
        List users = new ArrayList<>();
        userDao.findAll().iterator().forEachRemaining(user -> users.add(user.toUserDto()));
        return users;
    }

    @Override
    public User findOne(long id) {
        return userDao.findById(id).get();
    }

}

Spring Security LDAP Configuration

Now let us define our LDAP security configuration. Here, we are securing our /users endpoint with access role of ADMIN. Since, LdapShaPasswordEncoder has been deprecated, we have defined our custom PasswordEncoder which internally uses BCryptPasswordEncoder provided by spring security. The ldapAuthentication() method configures things where the username at the login form is plugged into {0} such that it searches uid={0},ou=people,dc=devglan,dc=com in the LDAP server.

WebSecurityConfig.java
package com.devglan.springsecurityldap.config;

importorg.springframework.context.annotation.Bean;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.authorizeRequests()
				.antMatchers("/users").hasRole("ADMIN")
				.and()
			.formLogin();
	}

	@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth
			.ldapAuthentication()
				.userDnPatterns("uid={0},ou=people")
				.groupSearchBase("ou=groups")
				.contextSource()
					.url("ldap://localhost:8389/dc=devglan,dc=com")
					.and()
				.passwordCompare()
					.passwordEncoder(passwordEncoder())
					.passwordAttribute("userPassword");
	}

	private PasswordEncoder passwordEncoder() {
		final BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();
		return new PasswordEncoder() {
			@Override
			public String encode(CharSequence rawPassword) {
				return bcrypt.encode(rawPassword.toString());
			}
			@Override
			public boolean matches(CharSequence rawPassword, String encodedPassword) {
				return bcrypt.matches(rawPassword, encodedPassword);
			}
		};
	}

	@Bean
	public BCryptPasswordEncoder bcryptEncoder() {
		return new BCryptPasswordEncoder();
	}

}

Below are the different properties that we have defined our application.properties

spring.ldap.embedded.ldif=classpath:users.ldif
spring.ldap.embedded.base-dn=dc=devglan,dc=com
spring.ldap.embedded.port=8389
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.user.datasource.driver-class-name=com.mysql.jdbc.Driver
script.sql
create table users (id bigint not null auto_increment, email varchar(255), first_name varchar(255), last_name varchar(255), username varchar(255), primary key (id)) engine=MyISAM
INSERT INTO users (email, first_name, last_name, username) values ('dhiraj@devglan.com', 'Dhiraj', 'Ray', 'only2dhir');
INSERT INTO users (email, first_name, last_name, username) values ('mike@gmail.com', 'Mike', 'Huss', 'mikehuss');

Testing the Application

Whenever a user try to access http://localhost:8080/users, the user will be redirected to http://localhost:8080/login

spring-ldap-login

If a user provides username/password as mike/mike, then user will be redirected to whitelabel error page as the user has a role of USER. To fetch the user list, you can enter the username/password as john/john as this user has ADMIn role and our /users endpoint can be accessed by user having ADMIN role.

spring-ldap-post-login

Conclusion

In this tutorial we discussed about securing spring boot app with spring security LDAP authentication. We used ldif file for the textual representation of LDAP and used in-memory LDAP server UnboundId for this tutorial. We also defined our custom password encoder and used Bcrypt with it. You can download the source from github here.

About The Author

author-image

Further Reading on Spring Security

1. Spring Oauth2 Role Based Authorization

2. Spring Boot Oauth2 Angular

3. Jwt Role Based Authorization

4. Angular Jwt Authentication

5. Spring Boot Jwt Auth

6. Spring Jms Activemq Integration Example

7. Spring Boot Security Oauth2 Example