0

Spring Security does not let me login using one of my 2 authentication providers.

My application should be able to authenticate two types of users - User and Organization by checking information stored on the database. For that, I created two separate WebSecurityConfiguration classes, with different SecurityFilterChain, defining different form logins and using the 2 different authentication providers - which use two different UserDetailsService implementations (pointing to different repositories).

My endpoints for Organization work - both register and login, and the session is successfully created, but for User, my POST method on the login endpoint (/auth/login/user) is blocked - returning Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' is not supported] on IntelliJ as a warning, but the backend returns status 200 on Postman. The application does not crash, but the POST method is not completed - on debug, it completes as 405 and redirects the request to /auth/login/org.

This is OrgWebSecurityConfiguration's SecurityFilterChain:

    @Bean
    @CrossOrigin
    public SecurityFilterChain orgSecurityFilterChain(HttpSecurity http) throws Exception {
        http.csrf()
                .disable()
                .securityMatchers((matcher) -> matcher
                        .requestMatchers("/auth/*/org").anyRequest())
                .authorizeHttpRequests()
                .requestMatchers("/auth/register/**")
                .permitAll()
                .requestMatchers("/auth/login/**")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginPage("/auth/login/org")
                .loginProcessingUrl("/auth/login/org")
                .defaultSuccessUrl("/auth/org-login-success", true)
                .permitAll()
                .and()
                .authenticationManager(orgAuthenticationManager(http))
                .logout()
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/auth/login?logout")
                .permitAll();
        return http.build();
    }

UserWebSecurityConfiguration's SecurityFilterChain:

    @Bean
    @CrossOrigin
    public SecurityFilterChain userSecurityFilterChain(HttpSecurity http) throws Exception {
        http.csrf()
                .disable()
                .securityMatchers((matcher) -> matcher
                        .requestMatchers("/auth/*/user").anyRequest())
                .authorizeHttpRequests()
                .requestMatchers("/auth/register/**")
                .permitAll()
                .requestMatchers("/auth/login/**")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginPage("/auth/login/user")
                .loginProcessingUrl("/auth/login/user")
                .defaultSuccessUrl("/auth/login-success", true)
                .permitAll()
                .and()
                .authenticationManager(userAuthenticationManager(http))
                .logout()
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/auth/login?logout")
                .permitAll();
        return http.build();
    }

userAuthProvider:

    @Bean
    public DaoAuthenticationProvider userAuthProvider() {
        DaoAuthenticationProvider userAuthenticationProvider = new DaoAuthenticationProvider();
        userAuthenticationProvider.setUserDetailsService(userDetailsService);
        userAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        return userAuthenticationProvider;
    }

orgAuthProvider:

    @Bean
    public DaoAuthenticationProvider orgAuthProvider() {
        DaoAuthenticationProvider orgAuthenticationProvider = new DaoAuthenticationProvider();
        orgAuthenticationProvider.setUserDetailsService(organizationDetailsService);
        orgAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        return orgAuthenticationProvider;
    }

Both orgAuthenticationManager and userAuthenticationManager were created by defining an authentication provider of type DaoAuthenticationProvider and setting the proper UserDetailsService for each, implementing the created auth providers on AuthenticationManagerBuilder. In this case, User's authentication manager is set to be the primary one, by using @Primary annotation.

I've tried using @Order(0) annotation on UserWebSecurityConfiguration as per this question, but the problem persisted. I also disabled csrf.

enid
  • 11
  • 1
  • 4
  • [You should be using `securityMatchers`](https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#_multiple_httpsecurity_instances) to specify the URLs that the `SecurityFilterChain` should be invoked. It is always important to share the whole configuration even if they look the same, that help us to help you. – Marcus Hert da Coregio Jul 12 '23 at 18:13
  • Thank you for your advice. I edited the question and included both `SecurityFilterChain`s, along with the authentication providers. – enid Jul 12 '23 at 18:40
  • may i ask why you havnt enabled spring DEBUG or TRACE logging and read your logs to see what the problem actually is, before asking here? – Toerktumlare Jul 12 '23 at 20:19
  • I have enabled debug, but I still don't get where the problem is. It says the same thing as the question title, and gives status 405. After that, the request is mapped to `org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)` - which I assume it would be `/login?error` - and gets redirected as a `GET` method to `/auth/login/org`. Note that my `POST` method was sent to `/auth/login/user`. – enid Jul 13 '23 at 13:25

1 Answers1

-1

I solved the problem by combining the two SecurityFilterChains into one, and adding both AuthenticationProviders to userAuthenticationManager, like so:

    @Bean
    @Primary
    public AuthenticationManager userAuthenticationManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .authenticationProvider(userAuthProvider())
                .authenticationProvider(orgAuthProvider())
                .build();
    }

And then normally adding userAuthenticationManager to securityFilterChain (previous userSecurityFilterChain):

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf()
                .disable()
                .securityMatchers((matcher) -> matcher
                        .requestMatchers("/auth/*").anyRequest())
                .authorizeHttpRequests()
                .requestMatchers("/auth/register/**")
                .permitAll()
                .requestMatchers("/auth/login")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginPage("/auth/login")
                .loginProcessingUrl("/auth/login")
                .defaultSuccessUrl("/auth/login-success", true)
                .permitAll()
                .and()
                .authenticationManager(userAuthenticationManager(http)) // AuthenticationManager with 2 auth providers is added here
                .logout()
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/auth/login?logout")
                .permitAll();
        return http.build();
    }

Now, I only use one form login to authenticate both User and Organization - /auth/login.

EXPLANATION:

By adding trace=true on application.properties, I was able to identify that a POST request to /auth/login/user returned status 405 and was redirected as a GET request to /auth/login/org. That meant that orgSecurityFilterChain was being considered the "default" filter on the application, even though userSecurityFilterChain was marked with Order(0). It gave permission to access /auth/*/user, but what was happening was that probably even though I accessed user endpoints - which should use userSecurityFilterChain - orgSecurityFilterChain was still used, since it was the default filter - which mapped /auth/login/org as its form login. That means that even though the request was supposed to be handled by userSecurityFilterChain - which mapped the login endpoint as /auth/login/user - it was being handled by orgSecurityFilterChain, which did not support POST requests to /auth/login/user.

enid
  • 11
  • 1
  • 4
  • Please note that your `userAuthenticationManager` method is incorrectly accessing the `AuthenticationManagerBuilder`. You should instead directly instantiate a `ProviderManager` with your list of `AuthenticationProvider`s. Additionally, I believe the `authenticationManager` DSL call is unnecessary because you are publishing `AuthenticationManager` as a `@Bean` which is auto-wired into Spring Security's filter chain(s). – Steve Riesenberg Jul 14 '23 at 15:47
  • @SteveRiesenberg Thanks for the advice. If I may ask, can you explain why `userAuthenticationManager` access to `AuthenticationManagerBuilder` is incorrect in this case? I didn't understand why. Thanks in advance! – enid Jul 14 '23 at 19:38
  • `http.getSharedObject(AuthenticationManagerBuilder.class)` is intended to access the "local" authentication manager builder internally when building a specific filter chain. It would be quite complicated to explain in a comment and involves a lot of internal framework complexity, but the short version is that publishing an `@Bean` should not rely on hacks that reach for internal framework implementation details, when it is much easier and correct to simply `return new ProviderManager(provider1, provider2, ...)`. – Steve Riesenberg Jul 14 '23 at 21:57