Skip to content

MissingCsrfTokenException with OIDC backchannel logout #16630

Open
@aelillie

Description

@aelillie

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;
    }
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions