spring-projects / spring-projects/spring-security

MissingCsrfTokenException with OIDC backchannel logout

Open
#16,630 3 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage type: bug
Dominant language
Java
Stars
9.6k
Forks
6.3k
Avg merge
2d 11h
Merged PRs (30d)
52

Description

Describe the bug
Upgrading from Spring Boot 3.3.0 to 3.4.2 I'm suddenly getting issues with CSRF when the OidcBackChannelLogoutHandler performs a POST request for logging out. It seems to be the CsrfFilter that creates it and passes it to the AccessDeniedHandler, which results in the logout to fail. I'm using the setup as described in https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa. Disabling csrf protection the OIDC backchannel logout works fine.

To Reproduce

  1. Prepare an application which uses Spring Session stored in JDBC + OIDC backchannel logout configured and has CSRF protection
  2. Log in to the application using OIDC integration
  3. Trigger OIDC back channel logout

Expected behavior
The user is successfully logged out and has its session invalidated.

Actual behavior
OidcBackChannelLogoutHandler fails in its request to logout, as the MissingCsrfTokenException is caught by the AccessDeniedHandler.

Sample

@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class SecurityConfig {
    protected static final String LOCAL_LOGOUT_URL = "http://localhost:8080/logout";
    protected static final String SESSION_COOKIE_NAME = "JSESSIONID";

    @Bean
    public void configure(HttpSecurity http, AccessDeniedHandler accessDeniedHandler,
                             AuthenticationEntryPoint authenticationEntryPoint,
                             GrantedAuthoritiesMapper grantedAuthoritiesMapper,
                             StilLoginOidcUserHandler stilLoginOidcUserHandler,
                             AuthenticationSuccessHandler authenticationSuccessHandler,
                             LogoutSuccessHandler logoutSuccessHandler,
                             ClientRegistrationRepository clientRegistrationRepository,
                             MvcRequestMatcher.Builder mvc,
                             OidcSessionRegistry oidcSessionRegistry,
                             OidcSessionLogoutHandler oidcSessionLogoutHandler) throws Exception {
        http
                .csrf(csrf -> csrf
                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                        .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
                        .ignoringRequestMatchers(LOCAL_LOGOUT_URL)
                )
                .authorizeHttpRequests(authorize -> authorize    
                        .requestMatchers(mvc.pattern("/api/**")).authenticated()                 
                        // Permissions to load Angular SPA
                        .requestMatchers(
                                mvc.pattern("/"),
                                mvc.pattern("/index.html"),
                                mvc.pattern("*.css"),
                                mvc.pattern("*.js"),
                                mvc.pattern("*.woff"),
                                mvc.pattern("*.woff2")
                        ).permitAll()
                        .requestMatchers(mvc.pattern("/logout")).authenticated()
                        .anyRequest().permitAll() // Let Angular handle the routing for unknown paths
                )
                .oauth2Login(oauth2 -> oauth2
                        .authorizationEndpoint(authorizationEndpointConfig -> {
                            var resolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, OAUTH2_REQUEST_BASE_URI);
                            resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
                            authorizationEndpointConfig.authorizationRequestResolver(resolver);
                        })
                        .userInfoEndpoint(userInfo -> userInfo
                                .oidcUserService(stilLoginOidcUserHandler)
                                .userAuthoritiesMapper(grantedAuthoritiesMapper))
                        .successHandler(authenticationSuccessHandler)
                        .oidcSessionRegistry(oidcSessionRegistry)
                )
                .logout(logout -> logout
                        .clearAuthentication(true)
                        .invalidateHttpSession(true)
                        .deleteCookies(SESSION_COOKIE_NAME)
                        .addLogoutHandler(oidcSessionLogoutHandler)
                        .logoutSuccessHandler(logoutSuccessHandler)
                )
                .oidcLogout(oidcLogout -> oidcLogout
                        .backChannel(backChannel -> backChannel.logoutUri(LOCAL_LOGOUT_URL))
                        .clientRegistrationRepository(clientRegistrationRepository)
                        .oidcSessionRegistry(oidcSessionRegistry)
                )
                .exceptionHandling(exceptions -> exceptions
                        .authenticationEntryPoint(authenticationEntryPoint)
                        .accessDeniedHandler(accessDeniedHandler)
                );
    }

    @Bean
    public MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
        return new MvcRequestMatcher.Builder(introspector);
    }

    @Bean
    public LogoutSuccessHandler oidcLogoutSuccessHandler(ClientRegistrationRepository clientRegistrationRepository) {
        var oidcLogoutSuccessHandler = new OIDCLogoutSuccessHandler(clientRegistrationRepository);
        oidcLogoutSuccessHandler.setPostLogoutRedirectUri("https://{baseHost}{basePort}" + ENDPOINTS_LOGOUT);
        return oidcLogoutSuccessHandler;
    }

    @Bean
    public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
        return new OidcUserService();
    }

    @Bean
    public CookieSerializer cookieSerializer() {
        var serializer = new DefaultCookieSerializer();
        serializer.setCookieName(SESSION_COOKIE_NAME);
        serializer.setUseBase64Encoding(false);
        return serializer;
    }
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the failure with the provided SecurityConfig, Spring Session JDBC, OIDC backchannel logout, and CSRF enabled. Trace the request through OidcBackChannelLogoutHandler, CsrfFilter, and AccessDeniedHandler; done means the backchannel logout succeeds and the user's session is invalidated without disabling CSRF protection.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring
Domain
authentication, backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.