logo

Spring Security Redirect Based on User Roles

Published on
Authors

So far, we've built a basic spring boot application, enabled spring security and created a basic login form. In the last lesson, we expanded on the first lesson by adding different user roles and showing and hiding front-end content based on these roles (User Roles and Thymeleaf Extras).

Today we'll be looking at redirecting users with different roles to different pages after they log in.


Admin.html

Following on from our previous example, we've now created a new HTML file called admin.html. We will redirect our admin users to this new page when they log in.

  <!DOCTYPE html>
  <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">

  <head>
      <title>codenerve.com - Welcome!</title>
      <meta charset="UTF-8">
      <title>Admin</title>
      <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700" rel="stylesheet">
      <link rel="stylesheet" href="css/style.css">
  </head>
      <body>
          <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

          <div>
               Custom administrator page.
          </div>


          <br/>
          <form th:action="@{/logout}" method="post">
              <input type="submit" value="Sign Out"/>
          </form>
      </body>
  </html>

MvcConfig

Now, to serve the new admin.html page, we must add this page to our MvcConfig class.

As with the previous examples, this is accomplished by creating a class, extending WebMvcConfigurerAdapter and overriding the addViewControllers method. This time adding all the earlier pages of our app and the new admin page:

package dev.mwhyte.spring.sec.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
        registry.addViewController("/index").setViewName("index");
        registry.addViewController("/admin").setViewName("admin");
        registry.addViewController("/login").setViewName("login");
    }
}

WebSecurityConfig

The Constructor

To decide what to do when different user roles log in. We've created a new field of type AuthenticationSuccessHandler. We're setting this new configuration bean via constructor injection.

configure method

This method is in charge of overriding and configuring HttpSecurity explicitly. From the last example, we've added two lines.

First, we've added a new antMatcher under the authorizeRequests section, and we've told spring security only to allow a user with the "ADMIN” role access to all endpoints starting with “/admin”:

.antMatchers("/admin").hasRole("ADMIN")

Secondly, we've added our CustomAuthenticationSuccessHandler under the formLogin section to tell spring security to ask this CustomAuthenticationSuccessHandler what to do when a successful login occurs:

.successHandler(authenticationSuccessHandler)

configureGlobal method

The configureGlobal method is our in-memory registry of users. We've added two users. One with the primary USER role and the other with the ADMIN role.

Full example:

package dev.mwhyte.spring.sec.config;

import org.springframework.beans.factory.annotation.Autowired;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    public WebSecurityConfig(AuthenticationSuccessHandler authenticationSuccessHandler) {
        this.authenticationSuccessHandler = authenticationSuccessHandler;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers( "/css/**", "/images/**", "/favicon.ico").permitAll()
                .antMatchers("/admin").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .successHandler(authenticationSuccessHandler)
                .permitAll()
                .and()
                .logout()
                .permitAll()
                .and().csrf().disable(); // we'll enable this in a later blog post
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("user").password("{noop}pass").roles("USER")
                .and()
                .withUser("admin").password("{noop}pass").roles("ADMIN");
    }
}

CustomAuthenticationSuccessHandler

As you can see from our sample code below, this class implements springs AuthenticationSuccessHandler class and overrides the onAuthenticationSuccess method.

Once a user successfully logs in, the onAuthenticationSuccess method is called, and the user's role is checked. If the user's role is admin, we redirect to the /admin HTTP endpoint; otherwise, we redirect them to the /index endpoint.

At this point, our MvcConfig takes over and serves the correct HTML page based on the viewController we created previously.

package dev.mwhyte.spring.sec.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Set;


@Configuration
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {


    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {

        Set<String> roles = AuthorityUtils.authorityListToSet(authentication.getAuthorities());

        if (roles.contains("ROLE_ADMIN")) {
            httpServletResponse.sendRedirect("/admin");
        } else {
            httpServletResponse.sendRedirect("/index");
        }
    }
}

Demo

Check out the source code, open the Application class, and right-click to run the demo.

To start the example, port 8080 will need to be available on your machine. If it is not, you can change this default in the application.properties file using:

server.port=8081

Set this to whatever value you wish.

Alternatively, you can watch this short video:


Unit Testing

As always, we've amended and added some additional tests to cover the new functionality:

package dev.mwhyte.spring.sec;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.FormLoginRequestBuilder;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.core.StringContains.containsString;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
public class ApplicationTests {
	@Autowired
	private MockMvc mockMvc;

	@Test
	public void loginWithValidUserThenAuthenticated() throws Exception {
		FormLoginRequestBuilder login = formLogin()
				.user("user")
				.password("pass");

		mockMvc.perform(login)
				.andExpect(authenticated().withUsername("user"));
	}

	@Test
	public void loginWithInvalidUserThenUnauthenticated() throws Exception {
		FormLoginRequestBuilder login = formLogin()
				.user("invalid")
				.password("invalidpassword");

		mockMvc.perform(login)
				.andExpect(unauthenticated());
	}

	@Test
	public void accessUnsecuredResourceThenOk() throws Exception {
		mockMvc.perform(get("/css/style.css"))
				.andExpect(status().isOk());
	}

	@Test
	public void accessSecuredResourceUnauthenticatedThenRedirectsToLogin() throws Exception {
		mockMvc.perform(get("/hello"))
				.andExpect(status().is3xxRedirection())
				.andExpect(redirectedUrlPattern("**/login"));
	}

	@Test
	@WithMockUser
	public void accessSecuredResourceAuthenticatedThenOk() throws Exception {
		mockMvc.perform(get("/index"))
				.andExpect(status().isOk());
	}

	@Test
	@WithMockUser(roles = "USER")
	public void loginWithRoleUserThenExpectAdminPageForbidden() throws Exception {
		mockMvc.perform(get("/admin"))
				.andExpect(status().isForbidden());
	}

	@Test
	@WithMockUser(roles = "ADMIN")
	public void loginWithRoleAdminThenExpectAdminContent() throws Exception {
		mockMvc.perform(get("/admin"))
				.andExpect(status().isOk())
				.andExpect(content().string(containsString("Custom administrator page.")));
	}

	@Test
	public void loginWithRoleUserThenExpectIndexPageRedirect() throws Exception {
		FormLoginRequestBuilder login = formLogin()
				.user("user")
				.password("pass");

		mockMvc.perform(login)
				.andExpect(authenticated().withUsername("user"))
				.andExpect(redirectedUrl("/index"));
	}

	@Test
	public void loginWithRoleAdminThenExpectAdminPageRedirect() throws Exception {
		FormLoginRequestBuilder login = formLogin()
				.user("admin")
				.password("pass");

		mockMvc.perform(login)
				.andExpect(authenticated().withUsername("admin"))
				.andExpect(redirectedUrl("/admin"));
	}
}

Here is a quick recap of the Spring test support we're using:

  • @ExtendWith(SpringExtension.class) — Tells JUnit5 to run unit tests with Spring's testing support

  • @SpringBootTest — Run as spring boot app. i.e. load application.properties and spring beans

  • @AutoConfigureMockMvc — Creates a Test helper class called MockMvc. We can imitate a front-end client making requests to the server from this.

  • @WithMockUser — Provides the ability to mock certain users. An authenticated user in our case.

  • FormLoginRequestBuilder — A utility class that allows us to create a form-based login request.

Next up, we will be covering spring security's Cross-Site Request Forgery (CSRF) protection.


Resources

GitHub - mwhyte-dev/spring-security at 3.redirect-based-on-role

Build a Basic Login Form With Spring Security, Thymeleaf, and Java

Spring Security — User Roles and ThymeLeaf Extras

Spring Security

Thymeleaf + Spring Security integration basics