Search code examples
spring-bootspring-securityoauth-2.0spring-security-6

Using two or more SecurityFilterChains with Spring Security 6 does not work properly, only one chain is invoked


I have trouble to get my two SecurityFilterhains work in conjunction with each other using Spring Security 6. For one of my endpoint paths (/v1/transactions/**) I want the user to authorize with Oauth2 and for the other endpoint path (/v1/info) Basic Auth is required. Only one of the configurations works as expected depending on which @Order() they have.

With the below two SecurityFilterChain configurations I am able to make requests to /v1/info using Basic Auth but not making requests to /v1/transaction/** using Oauth2 which just gives me 401 Access Denied.

If I change the order so basicAuthSecurityFilterChain gets @Order(2) and oauth2SecurityFilterChain gets @Order(1) then I can make calls to /v1/transaction/** using OAauth2 but not make calls to /v1/info using Basic Auth which then gives me 401 Access Denied.

I am not sure why I encounter this behaviour since the docs tells me that the invocation of a SecurityFilterChain is decided based on the path and the paths for the basic auth and oauth2 resources are different (/v1/transaction/** vs /v1/info).

@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
@EnableWebSecurity
@Configuration
public class BasicAuthSecurity {
    
    public AuthenticationManager authProviderManager() { //omitted code) }

    @Bean
    @Order(1)
    public SecurityFilterChain basicAuthSecurityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authenticationManager(authProviderManager())
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers(POST, "/v1/info", "/v1/info/{user}").hasRole("user")
                        .anyRequest().authenticated()
                )
                .httpBasic(withDefaults())
                .build();
    }
}

And for the OAuth2 SecurityFilterChain I am using this:

@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
@EnableWebSecurity
@Configuration
public class Oauth2Security {

    @Bean
    @Order(2)
    public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers(POST, "/v1/transaction/**")
                        .hasAnyRole("poweruser", "admin")
                        .anyRequest().authenticated()
                )
                .oauth2ResourceServer()
                .jwt()
                .and().and().build();
    }
}

Solution

  • you are missing securityMatcher in the first filter-chain in @Order. That simple :/

    // Applies only to the specified security-matchers:
    // requests with a Basic Authorization header 
    @Bean
    @Order(1)
    public SecurityFilterChain basicAuthSecurityFilterChain(HttpSecurity http) throws Exception {
      http.securityMatcher((HttpServletRequest request) -> {
        return Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION)).map(h -> {
          return h.toLowerCase().startsWith("basic ");
        }).orElse(false);
      });
    
      return http
          .authenticationManager(authProviderManager())
          .authorizeHttpRequests(authorize -> authorize
              .requestMatchers(POST, "/v1/info", "/v1/info/{user}").hasRole("user")
              .anyRequest().authenticated()
          )
          .httpBasic(withDefaults())
          .build();
    }
    
    // this one has lowest precedence (higher order) and no security matcher 
    // => behaves as default when higher precedence (lower order) ones security matchers did not match 
    @Bean
    @Order(2)
    public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
      return http
          .authorizeHttpRequests(authorize -> authorize
              .requestMatchers(POST, "/v1/transaction/**")
              .hasAnyRole("poweruser", "admin")
              .anyRequest().authenticated()
          )
          .oauth2ResourceServer()
          .jwt()
          .and().and().build();
    }
    

    Note that you can write security-matchers using about anything from the requests. It is more frequent to see some matching path patterns:

    http.securityMatcher("/v1/info/**");