2

I'm building a Spring Boot authorization server which needs to generate Oauth2 tokens with two different auth methods. I want to have a different endpoint for each method, but by default Spring only creates /oauth/token, and while it can be changed, I don't think it is possible to have two different paths for it.

As an alternative, I'm trying to create two methods in a controller which do an internal forward to /oauth/token, adding a parameter to the request so I can know where it came from.

I have something like this:

@RequestMapping(value = "/foo/oauth/token", method = RequestMethod.POST)
public ModelAndView fooOauth(ModelMap model) {
    model.addAttribute("method", "foo");
    return new ModelAndView("forward:/oauth/token", model);
}

This performs the forward correctly, but the auth fails with:

There is no client authentication. Try adding an appropriate authentication filter.

The same request works correctly when sent to /oauth/token directly, so I'm guessing that the problem is that the BasicAuthenticationFilter is not running after the forward.

How can I make it work?

Anxo
  • 475
  • 4
  • 13

2 Answers2

2

I had exactly the same issue. After some research I found out that the problem was caused by Spring Boot 2, not by Spring Security configurations. According to the Spring Boot 2.0 migration guide:

Spring Security and Spring Session filters are configured for ASYNC, ERROR, and REQUEST dispatcher types.

and the Spring Boot's SecurityFilterAutoConfiguration source code:

@Bean
@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
        SecurityProperties securityProperties) {
    DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
            DEFAULT_FILTER_NAME);
    registration.setOrder(securityProperties.getFilter().getOrder());
    registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
    return registration;
}

private EnumSet<DispatcherType> getDispatcherTypes(
        SecurityProperties securityProperties) {
    if (securityProperties.getFilter().getDispatcherTypes() == null) {
        return null;
    }
    return securityProperties.getFilter().getDispatcherTypes().stream()
            .map((type) -> DispatcherType.valueOf(type.name())).collect(Collectors
                    .collectingAndThen(Collectors.toSet(), EnumSet::copyOf));
}

where the defaults for securityProperties.getFilter().getDispatcherTypes() are defined in SecurityProperties as:

private Set<DispatcherType> dispatcherTypes = new HashSet<>(
    Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST));

Thus by default, Spring Boot configures Spring Security so that its filters will not be applied to FORWARD requests (but only to ASYNC, ERROR and REQUEST), and therefore no security filter will be applied to authenticate the requests when forwarding them to /oauth/token.

The solution is simple. You can either add the following line to your application.properties in order to apply default filters to ALL forwarded requests

spring.security.filter.dispatcher-types=async,error,request,forward

or create your own custom filter chain with a path matcher and dispatcherType=FORWARD to only filter requests that are forwared to /oauth/token.

Eugen Labun
  • 2,561
  • 1
  • 22
  • 18
foly
  • 197
  • 7
-1

Looking carefully to the filter chains created for the Oauth endpoints, and for the forwarding controllers, it's easy to see that the latter are missing the BasicAuthenticationFilter, because they aren't authenticated, and auth isn't performed again after the forward.

To solve it, I created a new config like this:

@Configuration
public class ForwarderSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private List<AuthorizationServerConfigurer> configurers = Collections.emptyList();

    @Autowired
    private FooClientDetailsService fooClientDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
        for (AuthorizationServerConfigurer configurerBit : configurers) configurerBit.configure(configurer);
        http.apply(configurer);
        http
                .authorizeRequests()
                    .antMatchers("/foo/oauth/token").fullyAuthenticated()
                .and()
                    .requestMatchers()
                    .antMatchers("/foo/oauth/token");
        http.setSharedObject(ClientDetailsService.class, fooClientDetailsService);

    }

}

This code mimics what Spring Oauth does behind the scenes (here), running identical filter chains with the same authentication options on both endpoints.

When the /oauth/token endpoint finally runs, it finds the auth results that it expects, and everything works.

Finally, if you want to run a different ClientDetailsService on two forwarding endpoints, you just have to create two configuration classes like this one, and replace the ClientDetailsService on the setSharedObject call in each of them. Note that for this, you'll have to set different @Order values in each class.

Anxo
  • 475
  • 4
  • 13