How to register multiple UserDetailsService on a authenticationManagerBuilder

2k Views Asked by At

I have two different repository for normal user and admin and separate url endpoints to authenticate. I want the authentication manager to use separate UserDetailsService for the endpoints since both normal and admin users can have same username and password but different repositories.

Example: if the endpoint hit is user_login then UserDetailsService1 and if the endpoint hit is admin_login then UserDetailsService2 How can I achieve this?

2

There are 2 best solutions below

1
On

The HttpSecurity.formLogin DSL only supports a single log in URL because that is what is most common. However, you can do this by explicitly registering a second UsernamePasswordAuthenticationFilter. The documentation has some nice diagrams of how form based log in works.

I created a sample (make sure to use the linked branch). Below is a summary and description of what is happening:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                // allow anyone to access the admin log in page
                .mvcMatchers("/admin_login").permitAll()
                // require admin access to any admin URLs
                .mvcMatchers("/admin/**").hasRole("ADMIN")
                // any other URL just requires to be authenticated
                .anyRequest().authenticated()
                .and()
            // configure the user based authentication
            .formLogin()
                // this means you should serve a log in page for users at GET /user_login
                // Spring Security will process POST /user_login as a user log in
                .loginPage("/user_login")
                // allow anyone to access the /user_login since they aren't authenticated when they see a log in page
                .permitAll()
                .and()
             // formLogin above only supports a single repository because that is what is most common
             // fortunately formLogin is just a shortcut for the code below
             // here we add the admin login form explicitly
            .addFilter(adminAuthenticationFilter());
    }

    // formLogin for users will pick up a UserDetailsService exposed as a Bean    
    @Bean
    static InMemoryUserDetailsManager userDetailsManager() {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
                .password("user")
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }

    // create an admin version of UsernamePasswordAuthenticationFilter     
    private static UsernamePasswordAuthenticationFilter adminAuthenticationFilter() {
        // inject the adminAuthenticationProvider so only admins are authenticated with this Filter
        UsernamePasswordAuthenticationFilter result = new UsernamePasswordAuthenticationFilter(adminAuthenticationProvider());
        // only process POST /admin_login
        result.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin_login", "POST"));
        // errors should go to /admin_login?error
        result.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/admin_login?error"));
        return result;
    }

    // create an AuthenticationManager that is only used by Admin users
    private static AuthenticationManager adminAuthenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(adminUsers());
        return new ProviderManager(authenticationProvider);
    }

    // we use the same username as user based to demon that it will work properly
   // the difference is that the password is admin and the user will have admin role so it can access URLs in /admin/
    static InMemoryUserDetailsManager adminUsers() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("user")
                .password("admin")
                .roles("USER", "ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user);
    }
}

As mentioned above you are responsible for creating the log in pages and ensuring they post to the correct URLs. The first step is to create a controller that maps the URLs to the views you want to display. Here we use a single Controller for convenience, but you can split this up:

@Controller
public class LoginController {

    @GetMapping("/admin_login")
    String adminLogin() {
        return "admin_login";
    }

    @GetMapping("/user_login")
    String login() {
        return "user_login";
    }
}

Then you need to have two views. The first view is admin_login.html. In a Boot + Thymeleaf application something like this would be located in src/main/resources/templates/user_login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <title>User Log In</title>
</head>
<body>
<div class="container">
    <h1>User Log In</h1>
    <form method="post"
          th:action="@{/user_login}">
        <div th:if="${param.error}">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}">
            You have been logged out.
        </div>
        <div>
            <label for="username">Username</label>
            <input type="text"
                   id="username"
                   name="username"
                   placeholder="Username"
                   required autofocus>
        </div>
        <div class="form-group">
            <label for="password">Password</label>
            <input type="password"
                   id="password"
                   name="password"
                   placeholder="Password"
                   required>
        </div>
        <button type="submit">Sign in</button>
    </form>
</div>
</body>
</html>

This is all detailed in the link I provided above. The key is that it submits a POST to /user_login with HTTP parameters username and password.

You need a similar view for the admin login that does a POST to /admin_login with HTTP parameters username and password.

0
On

You can have something like this

@Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                .userDetailsService(service1)
                .passwordEncoder(passwordEncoder());
        authenticationManagerBuilder
                .userDetailsService(service2)
                .passwordEncoder(passwordEncoder());
    }