Different AuthenticationManager per path/route in spring security in MvC

Viewed 553

Preamble

Since there are a lot of questions on StackOverflow about this already, I first want to ensure that this is not a duplicate and differentiate.

This is about

  • Having 2(or more) different AuthenticationProviders in 2 different AuthenticationManagers to be used on different routes.
  • Using the methods in Spring Security 5.5 not 3.x
  • Using a non XML configuration based approach

So the question is not about:

  • How to include several AuthenticationProvideres in on AuthenticationManager to offer "alternative authentications" (which most questions tend to be)

Case

Assume one has 2 custom AuthenticationProviders: CATApiTokenProvider and DOGApiTokenProvider. It is by design that we not talk about AOuth/JWT/Basic/Form providers, since they offer shortcuts.

Now we have 2 REST API endpoints /dog/endpoint and /cat/endpoint.

Question

How would one properly implement this today, with Spring Security 5.5:

  • We want the authentication provider CATApiTokenProvider to only be able to authenticate requests on /cat/endpoint
  • We want the authentication provider DOGApiTokenProvider to only be able to authenticate requests on /dog/endpoint

So one cannot authenticate with a cat token on /dog/endpoint and neither with a dog token on /cat/endpoint.

My Ideas/Approaches

a) I understand that since I have custom Cat/Dog filters, one can use the AuthenticationManagerResolver and pass one instance into the filter when creating the bean. This resolver might look like

  public AuthenticationManagerResolver<HttpServletRequest> resolver()
  {
    return request -> {
      if (request.getPathInfo().startsWith("/dog/")) {
        try {
          return ???;
        } catch (Exception exception) {
          log.error(exception);
        }
      }
      if (request.getPathInfo().startsWith("/cat/")) {
        try {
          return ???;
        } catch (Exception exception) {
          log.error(exception);
        }
      }
    };
  }

Two questions with that would be:

  • how to return different authentication managers here? How to instantiate 2 different AM with each one CatAP and DogAP? Currently I use public void configure(AuthenticationManagerBuilder auth) but as far as I understand, I would only configure 'the one' AuthenticationManager and I could add DogAP and CatAP there, but this would let as having 1 AM with 2 APs, so when using this AM i could auth with the dog token on the cat endpoint
  • is this really the right way to implement this? I would have expected to be able to provide the AM on the SecurityConfiguration level

b) Somehow instantiate 2 different AuthenticationManagers and then use the SecurityConfiguration to assign them to different matchers.

Two questions:

  • what is the right way to spawn 2 different AMs with different providers?
  • I cannot understand how I would add an AM for a spec
http.authorizeRequests()
      .antMatchers("/dog/**")
      .?
1 Answers

You can either publish multiple filter chains or wire your own AuthenticationFilter with an AuthenticationManagerResolver

You may use AuthenticationManagerResolver to return different AuthenticationManagers. Since Spring Security 5.4.0, we don't need to extend the WebSecurityConfigurerAdapter to configure our SecurityFilterChain anymore, you can instead define a bean of SecurityFilterChain type.

I'll go into detail on wiring your own AuthenticationFilter.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated());
        http.addFilterBefore(apiAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    private AuthenticationFilter apiAuthenticationFilter() {
        AuthenticationFilter authenticationFilter = new AuthenticationFilter(new ApiAuthenticationManagerResolver(), new BasicAuthenticationConverter());
        authenticationFilter.setSuccessHandler((request, response, authentication) -> {});
        return authenticationFilter;
    }

    public static class ApiAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {

        private final Map<RequestMatcher, AuthenticationManager> managers = Map.of(
                new AntPathRequestMatcher("/dog/**"), new DogAuthenticationProvider()::authenticate,
                new AntPathRequestMatcher("/cat/**"), new CatAuthenticationProvider()::authenticate
        );

        @Override
        public AuthenticationManager resolve(HttpServletRequest request) {
            for (Map.Entry<RequestMatcher, AuthenticationManager> entry : managers.entrySet()) {
                if (entry.getKey().matches(request)) {
                    return entry.getValue();
                }
            }
            throw new IllegalArgumentException("Unable to resolve AuthenticationManager");
        }
    }

    public static class DogAuthenticationProvider implements AuthenticationProvider {

        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            if (authentication.getName().endsWith("_dog")) {
                return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(),
                        AuthorityUtils.createAuthorityList("ROLE_DOG"));
            }
            throw new BadCredentialsException("Username should end with _dog");
        }

        @Override
        public boolean supports(Class<?> authentication) {
            return true;
        }

    }

    public static class CatAuthenticationProvider implements AuthenticationProvider {

        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            if (authentication.getName().endsWith("_cat")) {
                return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(),
                        AuthorityUtils.createAuthorityList("ROLE_CAT"));
            }
            throw new BadCredentialsException("Username should end with _cat");
        }

        @Override
        public boolean supports(Class<?> authentication) {
            return true;
        }

    }
}

In the example above, we have two AuthenticationProviders, one for cat and other for dog. They are resolved upon an AntPathRequestMatcher matching for both /dog/** and /cat/** endpoints, inside the ApiAuthenticationManagerResolver. There is no need to defined an AuthenticationManager for each dog and cat, since AuthenticationProvider/Manager have the same interface.

The ApiAuthenticationManagerResolver is then wired inside an AuthenticationFilter in your filter chain.

You can also define two different filter chains for each endpoint, like so:

@Bean
public SecurityFilterChain dogApiSecurity(HttpSecurity http) throws Exception {
    http.requestMatchers((matchers) -> matchers
                .antMatchers("/dog/**"));
    http.authorizeRequests((authz) -> authz
            .anyRequest().authenticated());
    http.httpBasic();
    http.authenticationProvider(new DogAuthenticationProvider());
    return http.build();
}

@Bean
public SecurityFilterChain catApiSecurity(HttpSecurity http) throws Exception {
    http.requestMatchers((matchers) -> matchers
                .antMatchers("/cat/**"));
    http.authorizeRequests((authz) -> authz
            .anyRequest().authenticated());
    http.httpBasic();
    http.authenticationProvider(new CatAuthenticationProvider());
    return http.build();
}

Please, when defining multiple filter chains, the ordering is important, make use of the @Order annotation in those scenarios.

When you do http.requestMatcher(new AntPathRequestMatcher("/endpoint/**")); you are telling Spring Security to only call the filter chain when the request matches that path.

There is also a ticket within Spring Security's repository to provide a AuthenticationManagerResolver implementation which accepts Map<RequestMatcher, AuthenticationManager>, it would be nice if you think it makes sense, give a thumbs up there.

Related