spring-projects / spring-projects/spring-security
MissingCsrfTokenException with OIDC backchannel logout
Nobody has claimed this yet.
- 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
- Prepare an application which uses Spring Session stored in JDBC + OIDC backchannel logout configured and has CSRF protection
- Log in to the application using OIDC integration
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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