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.
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.ldifdn: 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.javapackage 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 ListfindAll() { 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.
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.Driverscript.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
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.
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.