Describe the bug
In Spring Security 6.2.2 the OidcBackChannelLogoutHandler.java logout handler automatically replaces the logout URL endpoint hostname with localhost. However, in a Tomcat context, we also need to specify the port number, typically 8080. This is not possible in the current implementation.
To Reproduce Initiate a back channel logout.
Expected behavior
Local session is invalidated through a POST request to http://localhost:<PORT>/logout, where in my case it should be http://localhost:8080/logout.
Actual behavior
A logout POST request is send to http://localhost/logout, with no effect.
String computeLogoutEndpoint(HttpServletRequest request) {
String url = request.getRequestURL().toString();
return UriComponentsBuilder.fromHttpUrl(url)
.host("localhost")
.replacePath(this.logoutEndpointName)
.build()
.toUriString();
}
Sample This is my security config setup:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, AccessDeniedHandler accessDeniedHandler,
AuthenticationEntryPoint authenticationEntryPoint,
GrantedAuthoritiesMapper grantedAuthoritiesMapper,
AuthenticationSuccessHandler authenticationSuccessHandler,
LogoutSuccessHandler logoutSuccessHandler,
ClientRegistrationRepository clientRegistrationRepository,
MvcRequestMatcher.Builder mvc,
OidcSessionRegistry oidcSessionRegistry) throws Exception {
return http
.csrf(csrf -> csrf.disable()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
)
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests(authorize -> authorize
...
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorizationEndpointConfig -> {
var resolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, OAUTH2_REQUEST_BASE_URI);
resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
authorizationEndpointConfig.authorizationRequestResolver(resolver);
})
.userInfoEndpoint(userInfo -> userInfo
.userAuthoritiesMapper(grantedAuthoritiesMapper))
.successHandler(authenticationSuccessHandler)
.oidcSessionRegistry(oidcSessionRegistry)
)
.logout(logout -> logout
.logoutSuccessHandler(logoutSuccessHandler)
)
.oidcLogout(oidcLogout -> oidcLogout
.backChannel(withDefaults())
.clientRegistrationRepository(clientRegistrationRepository)
.oidcSessionRegistry(oidcSessionRegistry)
)
...
.build();
}
Comment From: jzheaux
Hi, @aelillie, thanks for the report. I'm not sure I understand completely just yet, though. The code you outlined does not remove any port from the resulting URI (see also) so it seems like I might be missing some details to be able to apply the appropriate fix.
Can you provide a minimum reproducer that demonstrates the issue you are having?
Comment From: aelillie
Hi @jzheaux , thank you for your quick respond. That is true, but the issue is that I do not have the possibility to add one.
If the request URL is e.g. http://server-one.com/logout/connect/back-channel/kc, the resulting logout URL will be http://localhost/logout.
Comment From: jzheaux
Thanks for the feedback, @aelillie. I'll close this in favor of #14609, the fix should go out in the next maintenance release.
Comment From: dalbani
In order to build a backchannel logout URI that points to localhost, I'm curious by the way how I can determine the port where the application is running.
I've tried the following but they basically all return null:
- @Value("${server.port}")
- Environment::getProperty("server.port")
- ServerProperties::getPort()
As in:
public SecurityFilterChain securityFilterChain(HttpSecurity http, ServerProperties serverProperties) throws Exception {
return http
...
.oidcLogout(oidcLogout -> oidcLogout
.backChannel(backChannel -> backChannel
.logoutUri(String.format("http://localhost:%d/logout", serverProperties.getPort()))))
.build();
}
In the end, I added server.port=8080 to application.properties to be able to look up the server.port property, but is there a more idiomatic / reliable way?
I even tried using ServletWebServerApplicationContext::getWebServer()::getPort() but the application couldn't start up anymore 🤔
Comment From: lmorocz
Hi @dalbani! You do not need to determinate the port in your configuration for this. You can use the prepared URI variables in the logoutUri from OidcBackChannelLogoutHandler#computeLogoutEndpoint since Spring Security 6.2.4, e.g. {basePort}.
This is a complex configuration with scheme, port and context path:
oidcLogout.backChannel(backChannel -> backChannel.logoutUri("{baseScheme}://localhost{basePort}{basePath}/logout"))
Please note that the default URI template from Spring Security 6.2.4 should be just fine for your use-case:
final class OidcBackChannelLogoutHandler implements LogoutHandler {
[...]
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
Comment From: dalbani
Hi @lmorocz, thanks for your comment.
But I should have said where I'm coming from w.r.t. to this issue.
Basically, as it was raised in comments in https://github.com/spring-projects/spring-security/issues/14553: if the requests to the backchannel logout endpoints is made to e.g. https://example.com/logout/connect/back-channel/my-registration then Spring Security uses https://localhost/logout for the subsequent call.
Which is not correct in my case, as it should rather be http://localhost:8080/logout.
That's why I cannot make use of {baseScheme} or {basePort}.
Comment From: lmorocz
Then use {baseHost} instead of localhost in the logoutUri (along with {baseScheme} and {basePort} as well). This way you will end up a request to https://example.com/logout (which is a revproxy to http://localhost:8080/logout I assume).
Another option is to, set up your IDP (Keycloak?) client BackChannel logout URI to the internal URI of your app (http://localhost:8080/logout/connect/back-channel/my-registration) - provided they are on the same host. Or set up the internal IP address or host name of the host of your app if they are on the same subnet (http://myapphost:8080/logout/connect/back-channel/my-registration). Then use the proper URI variables in the logoutUri() configuration at your discretion.
Comment From: dalbani
Then use
{baseHost}instead oflocalhostin the logoutUri (along with{baseScheme}and{basePort}as well). This way you will end up a request tohttps://example.com/logout(which is a revproxy tohttp://localhost:8080/logoutI assume).
Indeed, that's an option I didn't consider. Although it involves making the request go through the reverse proxy... to eventually reach the application itself. Not a dealbreaker in the grand scheme of things, but still a little over complicated.
Another option is to, set up your IDP (Keycloak?) client BackChannel logout URI to the internal URI of your app
This is unfortunately not a option for me, as the IdP (Keycloak indeed) cannot access the app directly. It has to go through the reverse proxy.
Comment From: lmorocz
Yes, it is complicated, but please note that before 6.2.2 this was the only way as 6.2.1 only replaced the path of the original request to /logout and nothing else. We have more configuration options now which is always nice.
Comment From: dalbani
I'm glad that 6.2.2 introduced an improvement indeed; kudos to the Spring Security developers 👍