Sean Casey
Sean Casey

Reputation: 86

Spring Boot Security PreAuthenticated Scenario with Anonymous access

I have a Spring Boot (1.5.6) application that is using the "pre-authenticated" authentication scenario (SiteMinder) from Spring Security.

I have a need to expose the actuator "health" endpoint anonymously meaning the requests to that endpoint will not go through SiteMinder and as a result, the SM_USER header will not be present in the HTTP Request Header.

The problem I'm facing is that no matter how I try to configure the "health" endpoint, the framework is throwing an org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException because the expected header ("SM_USER") is not present when the request does not go through SiteMinder.

This was my original security config:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
                .authorizeRequests().antMatchers("/cars/**", "/dealers/**")
                .hasAnyRole("CLIENT", "ADMIN")
            .and()
                .authorizeRequests().antMatchers("/health")
                .permitAll()
            .and()
                .authorizeRequests().anyRequest().denyAll()
            .and()
                .addFilter(requestHeaderAuthenticationFilter())
                .csrf().disable();
    }

    @Bean
    public Filter requestHeaderAuthenticationFilter() throws Exception {
        RequestHeaderAuthenticationFilter filter = new RequestHeaderAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManager());
        return filter;
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(preAuthProvider());
    }

    @Bean
    public AuthenticationProvider preAuthProvider() {
        PreAuthenticatedAuthenticationProvider authManager = new PreAuthenticatedAuthenticationProvider();
        authManager.setPreAuthenticatedUserDetailsService(preAuthUserDetailsService());
        return authManager;
    }

    @Bean
    public AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> preAuthUserDetailsService() {
        return new UserDetailsByNameServiceWrapper<>(inMemoryUserDetails());
    }

    @Bean
    public UserDetailsService inMemoryUserDetails() {
        return new InMemoryUserDetailsManager(getUserSource().getUsers());
    }

    @Bean
    public UserHolder getUserHolder() {
        return new UserHolderSpringSecurityImple();
    }

    @Bean
    @ConfigurationProperties
    public UserSource getUserSource() {
        return new UserSource();
    }

I've tried to exclude the /health endpoint a couple different ways to no avail.

Things I've tried:

Configure health endpoint for anonymous access rather than permitAll:

http
   .authorizeRequests().antMatchers("/health")
   .anonymous()

Configure WebSecurity to ignore the health endpoint:

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/health");
}

Turn off security for all actuator endpoints (not idea but I was grasping for straws):

management.security.enabled=false

Looking at the logs, the problem seems to be that the RequestHeaderAuthenticationFilter is getting registered as a top level filter rather than a filter in the existing securityFilterChain:

.s.DelegatingFilterProxyRegistrationBean : Mapping filter: 'springSecurityFilterChain' to: [/*]
o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'webRequestLoggingFilter' to: [/*]
o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestHeaderAuthenticationFilter' to: [/*]

Based on my understanding, because the RequestHeaderAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter, the framework knows where to insert the filter within the chain which is why I'm not tinkering with the addFilterBefore or addFilterAfter variants. Maybe I should be? Does anybody know the correct place to insert the filter explicitly? (I thought the need for explicitly specifying filter order was removed in prior versions of Spring Security)

I know I can configure the RequestHeaderAuthenticationFilter so that it doesn't throw an exception if the header is not present but I'd like to keep that on if at all possible.

I found this SO post that seems to be similar to my problem but unfortunately there's no answer there either. spring-boot-security-preauthentication-with-permitted-resources-still-authenti

Any help would be greatly appreciated! Thanks!

Upvotes: 2

Views: 7912

Answers (2)

PentaKon
PentaKon

Reputation: 4636

The provided answer is not complete most likely because Sean Casey made so many trial and error changes that lost track of which configuration actually fixed the problem. I am posting my findings which I believe have the correct configuration:

If you have incorrectly registered the RequestHeaderAuthenticationFilter as a @Bean, as the original answer says, then remove it. Just creating it as a normal instance and adding it as a filter registers it properly:

@Override
protected void configure(HttpSecurity http) throws Exception {
    RequestHeaderAuthenticationFilter filter = new RequestHeaderAuthenticationFilter();
    // configure your filter
    http
        .authorizeRequests().anyRequest().authenticated()
        .and()
        .addFilter(filter)
        .csrf().disable();
}

The magical configuration which Sean Casey tried but initially failed (due to the double registering of the auth filter) is the WebSecurity configuration:

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/health");
}

Notice that adding the HttpSecurity antMatchers does nothing in that case, it seems that WebSecurity is the one taking precedence and controlling what goes through.

EXTRA: Per Spring Security documentation, the WebSecurity.ignore method should be used for static resources, not for dynamic ones which should instead be mapped to allow all users. However in this case it seems the mapping gets overridden by the PreAuthentication filter which forces the use of the more aggressive ignore scenario.

Upvotes: 3

Sean Casey
Sean Casey

Reputation: 86

The problem was indeed the fact that the RequestHeaderAuthenticationFilter was being registered both as a top level filter (unwanted) and also within the Spring Security FilterChain (desired).

The reason for the "double registration" is because Spring Boot will register any Filter Beans with the Servlet Container automatically.

In order to prevent the "auto-registration" I just had to define a FilterRegistrationBean like so:

@Bean
public FilterRegistrationBean registration(RequestHeaderAuthenticationFilter filter) {
    FilterRegistrationBean registration = new FilterRegistrationBean(filter);
    registration.setEnabled(false);
    return registration;
}

Docs: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-disable-registration-of-a-servlet-or-filter

An alternate/simpler solution: Just don't mark the RequestHeaderAuthenticationFilter class as an @Bean which is fine because there's no kind of DI needed for that particular filter. By not marking the filter with @Bean, Boot won't try to auto register it which removes the need to define the FilterRegistrationBean.

Upvotes: 3

Related Questions