Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MissingCsrfTokenException with OIDC backchannel logout #16630

Open
aelillie opened this issue Feb 21, 2025 · 1 comment
Open

MissingCsrfTokenException with OIDC backchannel logout #16630

aelillie opened this issue Feb 21, 2025 · 1 comment
Labels
status: waiting-for-triage An issue we've not yet triaged type: bug A general bug

Comments

@aelillie
Copy link

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;
    }
}
@aelillie aelillie added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Feb 21, 2025
@aelillie
Copy link
Author

Related to #15227 and #15540.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: waiting-for-triage An issue we've not yet triaged type: bug A general bug
Projects
None yet
Development

No branches or pull requests

1 participant