Spring MVC 4 Security - DAO Authentication Provider, Custom User Details, Authentication Success Handlers

elevysi · 23 February 2017 |

Just like many web applications, I have public and protected resources; the latter shall only be accessible to authenticated users in a good configuration. I have the notion of roles; each authenticated user have by default the role user while privileged users can have more roles on top of user such as admin. For my two roles, I implemented different success authentication handlers.

The two strategies in use:

  • When unauthenticated users request protected resources, they shall be redirected to the log in page and be re-redirected back to their requested resource if the authentication is successful
  • When unauthenticated users explicitly request to be logged in, they shall be redirected to their profile page if their role is user, or to their dashboard if they hold a privileged role such as admin

One strategy is the default one while the role based strategy is a more elaborated and needs custom implementation.

Spring security will provide you a basic authentication process in which once signed in, the security context will be able to fetch basic information about the authenticated user through the principal object such as their username or primary identifier. However, if you need to have more user information in the security context, you will need to specify which information to be held within the context through custom implemetations and extensions of the classes provided by Spring Security. This post will explain how to authenticate users with a local database, how to store user information in the Security Context's principal and how to handle a successful authentication.

To start off, these are the security dependencies declared within the pom.xml file:

<properties>
	<java-version>1.6</java-version>
	<org.springframework-version>4.2.1.RELEASE</org.springframework-version>
	<spring-security.version>4.0.2.RELEASE</spring-security.version>
</properties>

<!-- SpringSecurity dependencies -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-core</artifactId>
	<version>${spring-security.version}</version>
</dependency>

<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-web</artifactId>
	<version>${spring-security.version}</version>
</dependency>

<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-config</artifactId>
	<version>${spring-security.version}</version>
</dependency>

<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-taglibs</artifactId>
	<version>${spring-security.version}</version>
</dependency>

There are multiple authentication managers possible; the one that I use is the local database authentication which holds the local users; a user has a username and a password (stored as a hash in the DB through bcrypt). The local strategy is referred to as a DAO (Data Access Object) Authentication Provider which uses the dao-service pattern to load the user details into the security context from the data access layer. I created the class ActiveUser which extends the User class provided by Spring Security, for the sake of being able to hold additional properties, on top of those provided by Spring Security, such as the user profile.

package com.elevysi.site.security;

import java.util.Collection;
import java.util.List;

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

import com.elevysi.site.entity.Profile;

public class ActiveUser extends User{
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 8778094825710419801L;
	private List<Profile> profiles;
	
	private Profile userProfile;
	
	private String first_name;
	
	private Profile activeProfile;
	
	

	public Profile getActiveProfile() {
		return activeProfile;
	}

	public void setActiveProfile(Profile activeProfile) {
		this.activeProfile = activeProfile;
	}

	public String getFirst_name() {
		return first_name;
	}

	public void setFirst_name(String first_name) {
		this.first_name = first_name;
	}

	public Profile getUserProfile() {
		return userProfile;
	}

	public void setUserProfile(Profile userProfile) {
		this.userProfile = userProfile;
	}

	public ActiveUser(String username, String password, boolean enabled,
	         boolean accountNonExpired, boolean credentialsNonExpired,
	         boolean accountNonLocked,
	         Collection authorities,
	         String first_name,
	         Profile userProfile,
	         Profile activeProfile,
	         List<Profile> profiles
			) {

	             super(username, password, enabled, accountNonExpired,
	                credentialsNonExpired, accountNonLocked, authorities);
	             
	             this.userProfile = userProfile;
	             this.activeProfile = activeProfile;
	             this.profiles = profiles;
	             this.first_name = first_name;
     }
	
	
	public List<Profile> getProfiles() {
		return profiles;
	}

	public void setProfiles(List<Profile> profiles) {
		this.profiles = profiles;
	}

}

Dao Authentication will later on refer to the CustomUserDetailsService to load the ActiveUser into the security context; it is a service that will help us get the additional information we need to store in the Security Context obtained from the Data Access Layer, as well as formatting the user's data in the way required by Spring Security. It implements the UserDetailsService interface provided by Spring Security as shown below:

package com.elevysi.site.service;

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 org.springframework.transaction.annotation.Transactional;

import com.elevysi.site.entity.Profile;
import com.elevysi.site.entity.ProfileType;
import com.elevysi.site.entity.Role;
import com.elevysi.site.security.ActiveUser;

import java.util.ArrayList;
import java.util.Iterator;
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;


@Service("customUserDetailsService")
@Transactional(readOnly = true)
public class CustomUserDetailsService implements UserDetailsService {
	
	
	@Autowired
	private UserService userService;
	
	@Autowired
	private ProfileService profileService;
	
	@Autowired
	private ProfileTypeService profileTypeService;
	
	/**
	 * Returns a populated {@link UserDetails} object. 
	 * The username is first retrieved from the database and then mapped to 
	 * a {@link UserDetails} object.
	 */
	
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
        	com.elevysi.site.entity.User domainUser = userService.loadUserByUsername(username);
            
            boolean enabled = true;
            boolean accountNonExpired = true;
            boolean credentialsNonExpired = true;
            boolean accountNonLocked = true;
            
            Set<Role> roles = domainUser.getRoles();
            Set<Profile> profiles = domainUser.getProfiles();
            List<String> springSecurityRoles = treatRoles(roles);
            
            List<GrantedAuthority> authList = getGrantedAuthorities(springSecurityRoles);
            
            Profile userProfile = profileService.getPrincipalProfile(domainUser);
            		
    		List<Profile> userProfiles = new ArrayList<Profile>();
    		
    		Iterator<Profile> it = profiles.iterator();
    		
    		while(it.hasNext()){
    			userProfiles.add(it.next());
    		}
            
            return new ActiveUser( 
            		domainUser.getUsername(), 
                    domainUser.getPassword(), 
                    enabled, 
                    accountNonExpired, 
                    credentialsNonExpired, 
                    accountNonLocked,
                    authList,
                    domainUser.getFirst_name(),
                    userProfile,
                    userProfile,
                    userProfiles);
            
		} catch (Exception e) {
			throw new RuntimeException(e);
		} 
        
    }
	
	public UserDetails cloneAndUpdateAuthenticatedUser(ActiveUser activeUser, Profile profile){
		activeUser.setActiveProfile(profile);
		this.addAndUpdateAuthenticatedUser(activeUser, profile);
		return activeUser;
	}

	public UserDetails addAndUpdateAuthenticatedUser(ActiveUser activeUser, Profile profile){
		
		boolean foundSameProfile = false;
		if(profile != null){
			List<Profile> userProfiles = activeUser.getProfiles();
			for(Profile storedProfile : userProfiles){
				if(storedProfile.getId() == profile.getId()){
					if(activeUser.getProfiles().remove(storedProfile)){
						activeUser.getProfiles().add(profile);
						foundSameProfile = true;
					}
					
				}
			}
			if(! foundSameProfile){
				activeUser.getProfiles().add(profile);
			}
		}
		
		return activeUser;
	}
    
    /**
	 * Converts a numerical role to an equivalent list of roles
	 * @param role the numerical role
	 * @return list of roles as as a list of {@link String}
	 */
    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;
    }
    
    /**
	 * Wraps {@link String} roles to {@link SimpleGrantedAuthority} objects
	 * @param roles {@link String} of roles
	 * @return list of granted authorities
	 */
    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;
    }
 
}

The main method of concern above is the loadUserByUsername method which introduces us to two different types of User instances; one is from the Spring Security API while the other, referred to as domain user, is a local instance of our User class, entity of my DB's users table. The method returns an instance of Spring Security's User class which has been locally inherited by the ActiveUser class to hold the user's properties we wish to keep on top of Spring required security user's properties.

To handle security in the Spring MVC 4 framework, one may either use JAVA configuration files or XML configurations. This post focusses on the JAVA configuration files. The @Configuration annotation on top of the class declaration makes the class available in the application context.

package com.elevysi.site.security;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
 

@Configuration
@EnableWebSecurity //Need to declare this application context for it to work so the other beans knoow
@EnableGlobalMethodSecurity(securedEnabled=true, jsr250Enabled=true, prePostEnabled=true)
public class SecurityConfig {
	
	@Autowired
    @Qualifier("customUserDetailsService")
    private UserDetailsService userDetailsService;
	
	@Autowired
    PersistentTokenRepository tokenRepository;
    
    
    @Autowired
    MySavedUrlAuthenticationSuccessHandler mySavedUrlAuthenticationSuccessHandler;
	
	@Autowired
    public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
        auth.authenticationProvider(authenticationProvider());
    }
	
	@Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        return authenticationProvider;
    }
 
    @Bean
    public PersistentTokenBasedRememberMeServices getPersistentTokenBasedRememberMeServices() {
        PersistentTokenBasedRememberMeServices tokenBasedservice = new PersistentTokenBasedRememberMeServices(
                "remember-me", userDetailsService, tokenRepository);
        return tokenBasedservice;
    }
 
    @Bean
    public AuthenticationTrustResolver getAuthenticationTrustResolver() {
        return new AuthenticationTrustResolverImpl();
    }
    
    @Configuration
	public static class GloablSecurity extends WebSecurityConfigurerAdapter {
    	
    	@Autowired
        PersistentTokenRepository tokenRepository;
    	
    	@Override
	    protected void configure(HttpSecurity http) throws Exception {
	        http
	        	.authorizeRequests()
	        	
					.antMatchers("/restricted/**").access("hasRole('USER')")
					.antMatchers("/").permitAll()
					.antMatchers("/public/**").permitAll()
					
					
					.antMatchers("/login/**").permitAll()
					.antMatchers("/logout/ajax/**").permitAll()
					
					
					.antMatchers("/dba/**").access("hasRole('ADMIN') or hasRole('DBA')")
					.antMatchers("/admin/**").access("hasRole('ADMIN') or hasRole('DBA')")
					
					.anyRequest().authenticated()
					.and()
				.formLogin()
					.loginPage("/login")
	                .failureUrl("/login/failure")
	                .defaultSuccessUrl("/auth/successlogin")
	                .usernameParameter("username")
	                .passwordParameter("password")
	                .and()
	            .rememberMe()
	            	.rememberMeParameter("remember-me")
	            	.tokenRepository(tokenRepository)
	                .tokenValiditySeconds(86400)
	                .and()
	            .csrf()
	            	.and()
	        	.exceptionHandling()
	        		.accessDeniedPage("/denied")
	                .and()
	            .logout()
	            	.logoutSuccessUrl("/logout/ajax/success")
	            	.logoutUrl("/logout")
	            	.invalidateHttpSession(true)
	            	.and()
	           .csrf();
    	        
    	        
    	    }
    	    
    	    @Override
    	    public void configure(WebSecurity webSecurity) throws Exception
    	    {
    	        webSecurity
    	            .ignoring()
    	            .antMatchers("/resources/**");
    	        
    	    }
	}
    
    @Configuration
    @Order(1)
    public static class RequestedAuthWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
    	
    	@Autowired
        RoleUrlAuthenticationSuccessHandler roleUrlAuthenticationSuccessHandler;
    	
        protected void configure(HttpSecurity http) throws Exception {

        	
        	http
        	.antMatcher("/auth/rqstd/**")
        	.authorizeRequests()
				.antMatchers("/auth/rqstd/failure").permitAll()
				.and()
			.formLogin()
				.loginPage("/auth/rqstd/login")
				.loginProcessingUrl("/auth/rqstd/doLogIn")
                .failureUrl("/auth/rqstd/failure")
                .successHandler(roleUrlAuthenticationSuccessHandler)
                .usernameParameter("username")
                .passwordParameter("password")
                .and()
            .csrf();
        }
    }
    
    @Configuration
    @Order(2)
    public static class ModalWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
    	
    	@Autowired
        ModalUrlAuthenticationSuccessHandler modalUrlAuthenticationSuccessHandler;
    	
    	 protected void configure(HttpSecurity http) throws Exception {
    		 http
         	.antMatcher("/modal/**")
         	.authorizeRequests()
 				.antMatchers("/modal/login").permitAll()
 				.antMatchers("/modal/**").authenticated()
 				.and()
 			.formLogin()
 				.loginPage("/modal/login")
 				.loginProcessingUrl("/modal/doLogIn")
                 .failureUrl("/auth/modal/failure")
                 .successHandler(modalUrlAuthenticationSuccessHandler)
                 .usernameParameter("username")
                 .passwordParameter("password")
                 .and()
             .csrf();
    	 }
    }
	
	
}

The class has references to the notions mentioned above; the authentication manager is set to be a DAO one calling the custom user service we created earlier to load our user with extended details in the security context.
A particular focus shall be given to the configureGlobalSecurity method; it is the one that encodes our application to use a DAO authentication provider that has been declared as a bean within the same configuration file. The DAO authentication provider accepts and sets a UserDetailsService for setting the user details. Please note the use of the @Qualifier annotation on top of the UserServiceDetails dependecy declaration to map it to CustomUserDetailsService which happens to be implementing the UserDetailsService interface. The DAO provider bean also sets our password encoding scheme.

Please also note the use of the @EnableWebSecurity (to induce the use of security in the app) and @EnableGlobalMethodSecurity (to induce conditionnal execution of methods) annotations. The configure method:

  • matches path patterns to be restricted or authorized
  • states the login form username and password parameters
  • specifies the login and logout actions
  • enables/de-activates cross site request forgery protection (carefull when CRSF protection is enabled to provide a token in the application's forms posts)
  • login failure handlers, exception handlers and many other parameters

The authentication handlers will use path matching to determine which strategy to handle a successful login; the path will tell the app whether the authentication is explicity requested or whether it is induced by a protected resource request.
Please note that one can declare as many success handlers as they wish and specify their order of execution through the @Order annotation; lower order-annotated handlers will be executed first until a matching strategy is mapped onto the requesting path.
The snippet above refers to several other handlers that one can create to fit their needs (ommitted in this post for the sake of being brief). The role-based authentication handler looks as follows:

package com.elevysi.site.security;

import java.io.IOException;
import java.util.Collection;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

@Component
public class RoleUrlAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{
	protected Log logger = LogFactory.getLog(this.getClass());
	private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();


	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
		
		handle(request, response, authentication);
		clearAuthenticationAttributes(request);
		
		
	}

	protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
		String targetUrl = determineTargetUrl(authentication);

		if (response.isCommitted()) {
			return;
		}

		redirectStrategy.sendRedirect(request, response, targetUrl);
	}

	/** Builds the target URL according to the logic defined in the main class Javadoc. */
	protected String determineTargetUrl(Authentication authentication) {
		
		boolean isUser = false;
		boolean isAdmin = false;
		Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
		for (GrantedAuthority grantedAuthority : authorities) {
			if (grantedAuthority.getAuthority().equals("ROLE_USER")) {
				isUser = true;
				break;
			} else if (grantedAuthority.getAuthority().equals("ROLE_ADMIN")) {
				isAdmin = true;
				break;
			}
		}

		if (isUser) {
			
			ActiveUser loggedUser = (ActiveUser)authentication.getPrincipal();
			
			return "/profile";
		} else if (isAdmin) {
			
			return "/admin/dashboard";
		} else {
			throw new IllegalStateException();
		}
	}

	public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
		this.redirectStrategy = redirectStrategy;
	}
	protected RedirectStrategy getRedirectStrategy() {
		return redirectStrategy;
	}

}

Our focus goes to the determineTargetUrl method which evaluates the role of the authenticated user and sends them to their profile page or to their admin dashboard page. 

To get the user details throught the application, one can use the snippet below; the method returns an instance of ActiveUser class as contained within the  security context' principal's object; use any of the ActiveUser getters methods to get the object's properties thereafter.

public ActiveUser getActiveUser(){
    	
    	Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    	boolean isAuthenticated =  false;
    	
    	if (!(auth instanceof AnonymousAuthenticationToken)) {
    		isAuthenticated =  true;
		}
    	
    	if(auth != null && isAuthenticated){
			return  (ActiveUser)auth.getPrincipal();
		}
    	
    	return null;
    }

This is how you nail basic security configurations down in a Spring MVC 4 application; you may cope the code on github.