Spring SAML Extension and Spring Security CSRF Protection Conflict

Paul Croarkin picture Paul Croarkin · Oct 22, 2014 · Viewed 7.1k times · Source

We have a Spring MVC (4.0.5) application with Spring Security (3.2.4) which includes CSRF protection which works fine. We are now adding the SAML security extension (spring-security-saml2-core 1.0.0) which causes an issue with CSRF protection.

The metadata has been configured on SSOCircle and trying to access http://localhost:8080/myapp directs to the login page on SSOCircle. After authentication, the browser redirects to http://localhost:8080/myapp/saml/SSO and generates an error:

HTTP Status 403 - Expected CSRF token not found. Has your session expired?

If we turn off CSRF protection, everything works. How can we maintain CSRF protection and still use the SAML extension?

Before setting up the SAML extension, we used a login form and CSRF protection worked and we did not receive an error on the login JSP and it did not have the token.

Code before SAML:

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity.authorizeRequests()
            .antMatchers("/login", "/login.request", "/logout").permitAll()
            .anyRequest()
                .hasAnyAuthority("MyRole")
                    .and().formLogin()
            .loginPage("/login.request").loginProcessingUrl("/login")
            .failureUrl("/login.request?error").permitAll().and().logout()
            .logoutUrl("/logout").permitAll()
            .logoutSuccessUrl("/login.request");
}

Code with SAML:

@Override
protected void configure(HttpSecurity http) throws Exception {
    //http.csrf().disable();

    http.httpBasic().authenticationEntryPoint(samlEntryPoint());

    http.addFilterBefore(metadataGeneratorFilter(),
            ChannelProcessingFilter.class).addFilterAfter(samlFilter(),
            BasicAuthenticationFilter.class);

    http
        .authorizeRequests()
            .antMatchers("/error").permitAll()
            .antMatchers("/saml/**").permitAll()
            .anyRequest()
                .hasAnyAuthority("MyRole")
            .anyRequest().authenticated();

    http.logout().logoutSuccessUrl("/");
}

UPDATE

After re-enabling CSRF protection and setting logging to DEBUG, here are the logs that occur right after the successful authentication:

22.10.2014 16:54:17.374 [http-bio-8080-exec-8] DEBUG o.s.w.m.support.MultipartFilter -
                Using MultipartResolver 'filterMultipartResolver' for MultipartFilter

22.10.2014 16:54:17.377 [http-bio-8080-exec-8] DEBUG o.s.b.f.s.DefaultListableBeanFactory -
                Returning cached instance of singleton bean 'filterMultipartResolver'

22.10.2014 16:54:17.788 [http-bio-8080-exec-8] DEBUG o.s.w.m.support.MultipartFilter -
                Request [/epass/saml/SSO] is not a multipart request

22.10.2014 16:54:17.790 [http-bio-8080-exec-8] DEBUG o.s.s.w.u.m.AntPathRequestMatcher -
                Checking match of request : '/saml/sso'; against '/resources/**'

22.10.2014 16:54:17.791 [http-bio-8080-exec-8] DEBUG o.s.security.web.FilterChainProxy -
                /saml/SSO at position 1 of 14 in additional filter chain; firing Filter: 'MetadataGeneratorFilter'

22.10.2014 16:54:17.793 [http-bio-8080-exec-8] DEBUG o.s.security.web.FilterChainProxy -
                /saml/SSO at position 2 of 14 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'

22.10.2014 16:54:17.795 [http-bio-8080-exec-8] DEBUG o.s.security.web.FilterChainProxy -
                /saml/SSO at position 3 of 14 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'

22.10.2014 16:54:17.797 [http-bio-8080-exec-8] DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository -
                HttpSession returned null object for SPRING_SECURITY_CONTEXT

22.10.2014 16:54:17.798 [http-bio-8080-exec-8] DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository -
                No SecurityContext was available from the HttpSession: org.apache.catalina.session.StandardSessionFacade@b08c9c9. A new one will be created.

22.10.2014 16:54:17.800 [http-bio-8080-exec-8] DEBUG o.s.security.web.FilterChainProxy -
                /saml/SSO at position 4 of 14 in additional filter chain; firing Filter: 'HeaderWriterFilter'

22.10.2014 16:54:17.801 [http-bio-8080-exec-8] DEBUG o.s.s.w.h.writers.HstsHeaderWriter -
                Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@244a79ef

22.10.2014 16:54:17.802 [http-bio-8080-exec-8] DEBUG o.s.security.web.FilterChainProxy -
                /saml/SSO at position 5 of 14 in additional filter chain; firing Filter: 'CsrfFilter'
22.10.2014 16:54:17.805 [http-bio-8080-exec-8] DEBUG o.s.security.web.csrf.CsrfFilter -
                Invalid CSRF token found for `http://localhost:8080/myapp/saml/SSO`

22.10.2014 16:54:17.807 [http-bio-8080-exec-8] DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository -
                SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.

22.10.2014 16:54:17.808 [http-bio-8080-exec-8] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter -
                SecurityContextHolder now cleared, as request processing completed

Answer

Vladimír Schäfer picture Vladimír Schäfer · Oct 25, 2014

You have at least two options.

One is to implement a custom RequestMatcher (org.springframework.security.web.util.RequestMatcher) which will not match on Spring SAML URLs and provide this to the csrf configuration with:

http.csrf().requireCsrfProtectionMatcher(matcher);

The other, easier is to define Spring SAML endpoints in a separate http configuration which will not have the csrf protection enabled.

The XML configuration to do this can be similar to:

<!-- SAML processing endpoints -->
<security:http pattern="/saml/**" entry-point-ref="samlEntryPoint">
    <security:custom-filter before="FIRST" ref="metadataGeneratorFilter"/>
    <security:custom-filter after="BASIC_AUTH_FILTER" ref="samlFilter"/>
</security:http>

<!-- Secured pages with SAML as entry point -->
<security:http entry-point-ref="samlEntryPoint">
    <security:csrf />
    <security:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY"/>
    <security:custom-filter before="FIRST" ref="metadataGeneratorFilter"/>
</security:http>

For Java configuration something like this should work:

@Configuration
@EnableWebSecurity
public class MutlipleHttpConfigurationConfig {

    @Configuration
    @Order(1)
    public static class SAMLWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/saml/**");
            http.csrf().disable();
            http.httpBasic().authenticationEntryPoint(samlEntryPoint());
            http.addFilterBefore(metadataGeneratorFilter(),
                    ChannelProcessingFilter.class).addFilterAfter(samlFilter(),
                    BasicAuthenticationFilter.class);
        }
    }

    @Configuration
    public static class BasicWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
        protected void configure(HttpSecurity http) throws Exception {
            http.httpBasic().authenticationEntryPoint(samlEntryPoint());
            http.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class);
            http
                    .authorizeRequests()
                    .antMatchers("/error").permitAll()
                    .anyRequest()
                    .hasAnyAuthority("MyRole")
                    .anyRequest().authenticated();

            http.logout().logoutSuccessUrl("/");
        }
    }
}

Details on defining multiple http configuration with Java configuration can be found in the Spring Security manual.