III. Setting up a Spring Boot service for a local database authentication

elevysi · 05 April 2019 |

As a service of its own, it is going to follow the same set-up as our basic Spring Boot service, except for the security part as it will later on be handling the authorization and authentication of the other services.
Let’s start the service domain with 4 tables: users, roles, groups, user_groups, groups_roles (a join table for the many to many relationship between the groups and the roles). Each user will be given a group while a group will include one or more roles, that the user will inherit from their membership to a group. The mysql database creation script is as follows:

CREATE DATABASE `authdb`;
USE authdb;
CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `email` varchar(255) DEFAULT NULL,
  `first_name` varchar(255) DEFAULT NULL,
  `last_name` varchar(255) DEFAULT NULL,
  `active` int(1) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `username_UNIQUE` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

CREATE TABLE `groups` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

CREATE TABLE `user_groups` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `group_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

CREATE TABLE `group_roles` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `group_id` int(11) NOT NULL,
  `role_id` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

Let's have a look at the dependencies needed for the application

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.2.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.tutorial.auth</groupId>
	<artifactId>authservice</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>authservice</name>
	<description>Auth Service</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>
	
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>Greenwich.RELEASE</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<dependencies>
		
<!-- 		Persistence -->
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		
		
<!-- 		Security -->
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

        <!--         Cloud -->
             
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
 
 
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-security</artifactId>
        </dependency>

		<!-- 		Web and Views		 -->
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	


	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

The business logic of this service is mainly concerned with the registration of the users, their authentication and their maintenance. For instance, while registering a user, they need to be affected to the right group so that the right role is affected to them. For this, we implement the registration method (through addUser() and doAddUser()) in the UsersController as follows:

package com.tutorial.auth.controller;

import java.util.List;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.tutorial.auth.model.User;
import com.tutorial.auth.service.ApiService;
import com.tutorial.auth.service.UserService;
import com.tutorial.common.dto.PostDTO;

@Controller
@RequestMapping("/ui/users")
public class UserController {
	
	private UserService userService;
	private ApiService apiService;
	
	@Autowired
	public UserController(UserService userService, ApiService apiService){
		this.userService = userService;
		this.apiService = apiService;
	}
	
	@GetMapping("/add")
	public String addUser(Model model){
		User user = new User();
		model.addAttribute("user", user);
		return "addUser";
	}
	
	@PostMapping("/add")
	public String doAddUser(Model model, @Valid User user, BindingResult result){
		if(result.hasErrors()){
			return "addUser";
		}
		
		userService.registerUser(user);
		return "redirect:/ui/users/";
	}
	
	@GetMapping("/")
	public String users(Model model){
		model.addAttribute("users", userService.findAll());
		return "usersIndex";
	}
	
	
	@GetMapping("/edit/{id}")
	public String editUser(Model model, @PathVariable("id") Long id){
		User user = userService.findByID(id);
		model.addAttribute("user", user);
		return "editUser";
	}
	
	@PostMapping("/edit/{id}")
	public String doEditUser(Model model, @Valid User user, BindingResult result, @PathVariable("id") Long id){
		if(result.hasErrors()){
			return "editUser";
		}
		
		userService.save(user);
		return "redirect:/ui/users/";
	}
	
	@GetMapping("/posts")
	public String posts(Model model){
		
		//Get the latest Posts from the basic service
		List<PostDTO> posts = apiService.getLatestPosts();
		model.addAttribute("posts", posts);
		
		return "posts";
	}
	

}

Please note the reference to ApiService whose code can be found on github and which we will be covering a little bit later.
The corresponding UserService assigns the user the right group at registration time as shown below:

package com.tutorial.auth.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import com.tutorial.auth.dao.UserDAO;
import com.tutorial.auth.model.Group;
import com.tutorial.auth.model.User;

@Service
public class UserService extends AbstractServiceImplement<User, Long>{
	
	private static final Long USER_GROUP_ID = (long) 1;
	
	private GroupService groupService;
	private UserDAO userDAO;
	private BCryptPasswordEncoder bCryptPasswordEncoder;
	
	
	@Autowired
	public UserService(
			GroupService groupService, 
			UserDAO userDAO,
			BCryptPasswordEncoder bCryptPasswordEncoder
	){
		this.groupService = groupService;
		this.bCryptPasswordEncoder = bCryptPasswordEncoder;
		this.userDAO = userDAO;
	}
	
	public User registerUser(User user){
		user.setActive(true);
		//handle password
		user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
		//Add the user to the group of users
		Group userGroup = groupService.findByID(USER_GROUP_ID);
		if(userGroup != null){
			user.getGroups().add(userGroup);
		}
		
		return userDAO.save(user);
	}
	
	
	public List<User> getActiveUsers(){
		return userDAO.getActiveUsers();
	}
	
}

Please note the use of the password encoder to store the password in the database as hashes instead of the plain format; BCryptPasswordEncoder will be declared as a bean in the Security configuration class.

package com.tutorial.auth.config;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

//Hybrid Security Configuration

@Configuration
public class SecurityConfig {
	
	
	@Configuration
	public static class FormSecurityConfig extends WebSecurityConfigurerAdapter{
		

		
		@Autowired
	    private DataSource dataSource;
		
		
		@Bean
	    public BCryptPasswordEncoder passwordEncoder() {
	        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
	        return bCryptPasswordEncoder;
	    }
		
		@Override
		protected void configure(HttpSecurity http) throws Exception{
			
			http.requestMatchers()
				.antMatchers("/" , "/ui/**", "/login")
				.and()
				.authorizeRequests()
				.antMatchers("/").permitAll()
				.antMatchers("/ui/public/**").permitAll()
				.anyRequest().authenticated()
				.and()
				.formLogin().permitAll()
				;
			
		}
		
		public void configure(WebSecurity webSecurity) throws Exception{
			webSecurity
				.ignoring()
				.antMatchers("/js/**")
				.antMatchers("/css/**")
				;
		}
	
	    
	    
	    @Override
	    protected void configure(AuthenticationManagerBuilder auth)
	            throws Exception {
	        auth.
	                jdbcAuthentication()
	                .usersByUsernameQuery("select username, password, active from users where username=?")
	                .authoritiesByUsernameQuery("select u.email, r.name from users u "
	                		+ "inner join user_groups ug on(u.id=ug.user_id) "
	                		+ "inner join group_roles gr on(ug.group_id=gr.id)"
	                		+ "inner join roles r on(gr.role_id=r.id) where u.username=?")
	                .dataSource(dataSource)
	                .passwordEncoder(passwordEncoder())
	                ;
	    }
		
	}

}

The AuthenticationManagerBuilder is built upon on the local datasource and declares the sql statement used to create the Principal in the security context. We can improve on the use of sql statements by customizing our Principal object I explained in the Extending Security with Custom Principal post.
For this, we are going to customize Spring Security's Principal by extending the UserDetails class so that we can eventually hold additional attributes beyond the username offered by the default Spring Security Context.  The class UserPrincipal extends UserDetails and is implemented as follows:

package com.tutorial.auth.config;

import java.util.Collection;

import org.springframework.security.core.userdetails.User;


public class UserPrincipal extends User{
	
	private String email;
	
	public UserPrincipal(String username,
			String password,
			boolean enabled,
			boolean accountNonExpired,
			boolean credentialsNonExpired,
			boolean accountNonLocked,
			Collection authorities,
			String email)
	{
		super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
		this.email = email;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}
		
}

Let's also implement the necessary code to retrieve the details of the security context (here implemented within the UserService class):

public UserPrincipal getCurrentUser(){
	Authentication auth = SecurityContextHolder.getContext().getAuthentication();
	boolean isAuthenticated =  false;
	
	if (!(auth instanceof AnonymousAuthenticationToken)) {
		isAuthenticated =  true;
	}
	 
	if(auth != null && isAuthenticated){
		logger.info("Principal is "+auth.getPrincipal());
		/**
		 * In case a client credentials flow is used, the Principal will be a String corresponding to the Oauth2 client_id of the calling service
		 * In case local authentication or the authorization code flow is used, the Principal will be an object of type UserPrincipal
		 */
		if(! (auth.getPrincipal() instanceof String)) return  (UserPrincipal)auth.getPrincipal();
	}
	
	return null;
}

Once we have defined the UserDetails object template, we need to implement a custom UserDetailsService, i.e class CustomUserDetailsService responsible for loading the right UserDetails object into the security context through the loadUserByUsername method.

package com.tutorial.auth.config;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.tutorial.auth.dao.UserDAO;
import com.tutorial.auth.model.Group;
import com.tutorial.auth.model.Role;

@Service
public class CustomUserDetailsService implements UserDetailsService{
	
	private UserDAO userDAO;
	
	@Autowired
	public CustomUserDetailsService(UserDAO userDAO){
		this.userDAO = userDAO;
	}
	
	
	@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                try{
	                com.tutorial.auth.model.User domainUser = userDAO.loadByUsername(username);
	                return this.create(domainUser);
                }catch(UsernameNotFoundException e){
                    throw new RuntimeException(e);
                }
    }
	
	public UserDetails create(com.tutorial.auth.model.User user){
		
		
		boolean enabled = user.isActive();
        boolean accountNonExpired = user.isActive();
        boolean credentialsNonExpired = user.isActive();
        boolean accountNonLocked = user.isActive();

        Set<Role> roles = new HashSet<Role>();
        Set<Group> groups = user.getGroups();

        //Add the roles inherited from the groups to the array of user's roles
        for(Group group : groups){
            roles.addAll(group.getRoles());
        }

        List<String> springSecurityRoles = treatRoles(roles);
        List<GrantedAuthority> authList = getGrantedAuthorities(springSecurityRoles);

        return new UserPrincipal(
            user.getUsername(),
            user.getPassword(),
            enabled,
            accountNonExpired,
            credentialsNonExpired,
            accountNonLocked,
            authList,
            user.getEmail()
        );

        
	}
	
	
	public List<String> treatRoles(Set<Role> roles) {
        List<String> security_roles = new ArrayList<String>();

        for (Role userRole : roles) {
        	security_roles.add(userRole.getName());
        }

        return security_roles;
    }
	
	
	 public static List<GrantedAuthority> getGrantedAuthorities(List<String> roles) {
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();

        for (String role : roles) {
            authorities.add(new SimpleGrantedAuthority(role));
        }
        return authorities;
    }


}

Afterwards, let’s refactor the SecurityConfig.java class to accommodate the customizations we have put in place. We will also implement within the same class the logic related to the resource server security for the routes prefixed with /api/**.
This will be done through inner classes on which an order will be defined to indicate which one holds precedence to the other when addressing the service requests. Similarly to the basic service set-up, annotation @EnableResourceServer is declared to indicate that this is a resource server, as shown in the snippet below:

package com.tutorial.auth.config;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

//Hybrid Security Configuration

@Configuration
public class SecurityConfig {
	
	@Configuration
	@Order(10)
	@EnableResourceServer
	public static class ResourceServerConfig extends ResourceServerConfigurerAdapter{
		
		private static final String  RESOURCE_ID = "authorizationResourceApi";
		
		@Override
        public void configure(ResourceServerSecurityConfigurer resources) {
                resources.resourceId(RESOURCE_ID).stateless(false);
        }

        @Override
		public void configure(HttpSecurity http) throws Exception{
		    http
		    .requestMatchers().antMatchers("/api/**")
		    .and()
		    .authorizeRequests()
		      .anyRequest().authenticated();
		}
		
	}
	
	
	@Configuration
	@Order(20)
	public static class FormSecurityConfig extends WebSecurityConfigurerAdapter{

		@Autowired
		private CustomUserDetailsService customUserDetailsService;
		
		
		
		@Bean
	    public BCryptPasswordEncoder passwordEncoder() {
	        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
	        return bCryptPasswordEncoder;
	    }
		
		@Override
		protected void configure(HttpSecurity http) throws Exception{
			
			http.requestMatchers()
				.antMatchers("/" , "/ui/**", "/login")
				.and()
				.authorizeRequests()
				.antMatchers("/").permitAll()
				.antMatchers("/ui/public/**").permitAll()
				.anyRequest().authenticated()
				.and()
				.formLogin().permitAll()
				;
			
		}
		
		public void configure(WebSecurity webSecurity) throws Exception{
			webSecurity
				.ignoring()
				.antMatchers("/js/**")
				.antMatchers("/css/**")
				;
		}
		
		@Override
	    @Bean
	    public AuthenticationManager authenticationManagerBean() throws Exception {
	        return super.authenticationManagerBean();
	    }
	    
	    	
	    @Bean
	    public DaoAuthenticationProvider authenticationProvider(){
	    	DaoAuthenticationProvider authenticattionProvider = new DaoAuthenticationProvider();
	    	authenticattionProvider.setUserDetailsService(customUserDetailsService);
	    	authenticattionProvider.setPasswordEncoder(passwordEncoder());
	    	
	    	return authenticattionProvider;
	    }
	    
	    @Override
	    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	    	auth.parentAuthenticationManager(authenticationManagerBean()).userDetailsService(customUserDetailsService);
	        auth.authenticationProvider(authenticationProvider());
	    }
		
	}

}

As you might see, a DaoAuthenticationProvider bean has been created which sets the implemented CustomUserDetailsService as its UserDetailsService. Once authenticated, the security context is built up with the custom UserPrincipal object we have defined earlier.