1. Introduction

In this tutorial, we will see how we can configure Spring Security to work with two different login pages using two different Spring Security http elements in the configuration.

2. Configuring 2 Http Elements

One of the situations in which we may need two login pages is when we have one page for administrators of an application and a different page for normal users.

We will configure two http elements that will be differentiated by the URL pattern associated with each:

  • /user* for pages that will need a normal user authentication to be accessed
  • /admin* for pages that will be accessed by an administrator

Each http element will have a different login page and a different login processing URL.

In order to configure two different http elements, let’s create two static classes annotated with @Configuration.

Both will be placed inside a regular @Configuration class:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    ...
}

Let’s define the ConfigurerAdapter for the “ADMIN” users:

@Configuration
@Order(1)
public static class App1ConfigurationAdapter {

    @Bean
    public SecurityFilterChain filterChainApp1(HttpSecurity http) throws Exception {
        http.antMatcher("/admin*")
          .authorizeRequests()
          .anyRequest()
          .hasRole("ADMIN")
          
          .and()
          .formLogin()
          .loginPage("/loginAdmin")
          .loginProcessingUrl("/admin_login")
          .failureUrl("/loginAdmin?error=loginError")
          .defaultSuccessUrl("/adminPage")
          
          .and()
          .logout()
          .logoutUrl("/admin_logout")
          .logoutSuccessUrl("/protectedLinks")
          .deleteCookies("JSESSIONID")
          
          .and()
          .exceptionHandling()
          .accessDeniedPage("/403")
          
          .and()
          .csrf().disable();
       return http.build();
    }
}

And now, let’s define the ConfigurerAdapter for normal users:

@Configuration
@Order(2)
public static class App2ConfigurationAdapter {

    @Bean 
    public SecurityFilterChain filterChainApp2(HttpSecurity http) throws Exception {
        http.antMatcher("/user*")
          .authorizeRequests()
          .anyRequest()
          .hasRole("USER")
          
          .and()
          .formLogin()
          .loginPage("/loginUser")
          .loginProcessingUrl("/user_login")
          .failureUrl("/loginUser?error=loginError")
          .defaultSuccessUrl("/userPage")
          
          .and()
          .logout()
          .logoutUrl("/user_logout")
          .logoutSuccessUrl("/protectedLinks")
          .deleteCookies("JSESSIONID")
          
          .and()
          .exceptionHandling()
          .accessDeniedPage("/403")
          
          .and()
          .csrf().disable();
       return http.build();
    }
}

Note that by placing the @Order annotation on each static class, we are specifying the order in which the two classes will be considered based on the pattern matching when a URL is requested.

Two configuration classes cannot have the same order.

3. Custom Login Pages

We will create our own custom login pages for each type of user. For the administrator user, the login form will have a “user_login” action, as defined in the configuration:

<p>User login page</p>
<form name="f" action="user_login" method="POST">
    <table>
        <tr>
            <td>User:</td>
            <td><input type="text" name="username" value=""></td>
        </tr>
        <tr>
            <td>Password:</td>
            <td><input type="password" name="password" /></td>
        </tr>
        <tr>
            <td><input name="submit" type="submit" value="submit" /></td>
        </tr>
    </table>
</form>

The administrator login page is similar, except the form will have an action of “admin_login” as per the java configuration.

4. Authentication Configuration

Now we need to configure authentication for our application. Let’s look at two ways to accomplish this — one using a common source for user authentication, and the other using two separate sources.

4.1. Using a Common User Authentication Source

If both login pages share a common source for authenticating users, you can create a single bean of type UserDetailsService that will handle the authentication.

Let’s demonstrate this scenario using an InMemoryUserDetailsManager that defines two users — one with a role of “USER” and one with a role of “ADMIN”:

@Bean
public UserDetailsService userDetailsService() throws Exception {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User
      .withUsername("user")
      .password(encoder().encode("userPass"))
      .roles("USER")
      .build());
    
    manager.createUser(User
      .withUsername("admin")
      .password(encoder().encode("adminPass"))
      .roles("ADMIN")
      .build());
    
    return manager;
}

@Bean
public static PasswordEncoder encoder() {
    return new BCryptPasswordEncoder();
}

4.2. Using Two Different User Authentication Sources

If you have different sources for user authentication — one for administrators and one for normal users — you can configure an AuthenticationManagerBuilder inside each static @Configuration class. Let’s look at an example of an authentication manager for an “ADMIN” user:

@Configuration
@Order(1)
public static class App1ConfigurationAdapter {

    @Bean
    public UserDetailsService userDetailsServiceApp1() {
         UserDetails user = User.withUsername("admin")
             .password(encoder().encode("admin"))
             .roles("ADMIN")
             .build();
         return new InMemoryUserDetailsManager(user);
    }
}

In this case, the UserDetailsService bean from the previous section will no longer be used.

6. Conclusion

In this quick tutorial, we’ve shown how to implement two different login pages in the same Spring Security application.

The complete code for this article can be found in the GitHub project.

When you run the application, you can access the examples above on the /protectedLinks URI.