Summary
I have been using Keycloak's Spring Boot Adapter so far. The problem is the continuous necessity to keep the dependency up-to-date in every platform update. That's why, with Spring Security 5.2.x+ generic support, I had considered delegating this integration to Spring's.
There's one specific feature from Keycloak that isn't currently supported: single logout through the backchannel.
This Keycloak's issue details how it could be achieved in a generic way: if there were a way to propagate a client_session_state param during the token exchange invocation, backchannel support would work, enabling Single Logout accross the realm.
Actual Behavior
After logging out, keycloak tries to invoke the backchannel but can't locate the associated sessions:
19:49:02,605 DEBUG [org.keycloak.services.managers.AuthenticationManager] (default task-1) backchannel logout to: resource-server-2
19:49:02,605 DEBUG [org.keycloak.services.managers.ResourceAdminManager] (default task-1) Cant logout {0}: no logged adapter sessions
This makes the application that logged out, actually need to reauthenticate, but leaves all the others with an active session.
Expected Behavior
Logging out from one application should allow automatic logout from all the others.
Configuration
Sample project that shows this behaviour: https://github.com/codependent/spring-boot-2-oidc-sample
I've tested with Keycloal 8.0.1. Just created a realm insight with two confidential clients: resourcer-server-1 and resource-server-2, configuring each Admin URL to their context roots.
Version
5.2.x
Sample
https://github.com/codependent/spring-boot-2-oidc-sample
Comment From: codependent
This is how Keycloak's adapter sends that information (along with the hostname):
public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri, String sessionId) throws IOException, HttpFailure {
List<NameValuePair> formparams = new ArrayList<>();
redirectUri = stripOauthParametersFromRedirect(redirectUri);
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "authorization_code"));
formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
formparams.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri));
if (sessionId != null) {
formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, sessionId));
formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, HostUtils.getHostName()));
}
The same could be achieved if there were some way to hook those additional parameters into the OAuth2AuthorizationCodeGrantRequestEntityConverter, e.g.:
private MultiValueMap<String, String> buildFormParameters(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
...
if (Boolean.TRUE.equals(clientRegistration.isInformClientId())) {
formParameters.add("client_session_state", XXX); // XXX = http session id
}
if (Boolean.TRUE.equals(clientRegistration.isInformClientSessionHost())) {
formParameters.add("client_session_host", XXX); // XXX = hostname
}
...
return formParameters;
}
Comment From: jgrandja
@codependent
There's one specific feature from Keycloak that isn't currently supported: single logout through the backchannel.
It seems that you are referring to OpenID Connect Back-Channel Logout 1.0?
But when I read...
This Keycloak's issue details how it could be achieved in a generic way: if there were a way to propagate a client_session_state param during the token exchange invocation, backchannel support would work, enabling Single Logout accross the realm.
This does not sound like OIDC back-channel logout.
Can you provide a link to the Keycloak reference docs for this feature so I can better understand?
Comment From: codependent
@jgrandja
It seems that you are referring to OpenID Connect Back-Channel Logout 1.0?
Actually their backchannel logout documentation is kind of obscure, and I don't think they follow that spec. This is how clients should be configured to support b-c logout:
Admin URL
For Keycloak specific client adapters, this is the callback endpoint for the client. The Keycloak server will use this URI to make callbacks like pushing revocation policies, performing backchannel logout, and other administrative operations. For Keycloak servlet adapters, this can be the root URL of the servlet application. For more information see Securing Applications and Services Guide.
That is to say we have to set it up to the root of our application: http://xxx/context-root
Their adapters work this way, as shown above:
-
When exchanging the code for token they also inform two additional parameters that allow them to associate the session with each user login in per application.
-
When logging out from an application they invoke the Admin URL for each client in which the user was logged in, passing some parameters. Their adapter identifies those parameters to destroy de session.
I understand this behaviour is non standard (spec-wise) but on the other hand it's the only thing that prevents us from using a library, spring security, that decouples us from Keycloak's adapters, but also allows us to use all their features (backchannel logout).
It would be great if Spring Security provided a kind of extension to support this specific Keycloak. capability.
Comment From: jgrandja
Actually their backchannel logout documentation is kind of obscure
Yes agreed I couldn't find anything.
It would be great if Spring Security provided a kind of extension to support this specific Keycloak. capability.
We do not implement provider specific implementations. Our goal is to be spec-compliant but providing the right amount of extension hooks to allow the user to customize if necessary.
We could consider adding support for OpenID Connect Back-Channel Logout 1.0, however, we cannot add any proprietary logic specific to a provider.
Comment From: codependent
I understand, there's no point in adding custom behaviour in a generalistic framework.
I guess I could get by if I extend OAuth2AuthorizationCodeGrantRequestEntityConverter to provide that information, and use my extended class instead. Not sure this can be done:
-
Is there a way to access the HttpSession id and the host name in
private MultiValueMap<String, String> buildFormParameters? Maybe withRequestContextHolder? And in a webflux application? -
How can I replace OAuth2AuthorizationCodeGrantRequestEntityConverter with my own extension in the security context configuration?
Comment From: codependent
Answering myself, I managed to solve the previous questions and hook in my own implementation:
class KeycloakOAuth2AuthorizationCodeGrantRequestEntityConverter : OAuth2AuthorizationCodeGrantRequestEntityConverter() {
override fun convert(authorizationCodeGrantRequest: OAuth2AuthorizationCodeGrantRequest): RequestEntity<*>? {
val sessionId = RequestContextHolder.getRequestAttributes()?.sessionId
val host = InetAddress.getLocalHost().hostName
val converted = super.convert(authorizationCodeGrantRequest)
(converted?.body as MultiValueMap<String, String>)["client_session_state"] = sessionId
(converted.body as MultiValueMap<String, String>)["client_session_host"] = host
return converted
}
}
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
val authorizationCodeTokenResponseClient = DefaultAuthorizationCodeTokenResponseClient()
authorizationCodeTokenResponseClient.setRequestEntityConverter(KeycloakOAuth2AuthorizationCodeGrantRequestEntityConverter())
http.authorizeRequests { authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
}.oauth2Login { oauth2login: OAuth2LoginConfigurer<HttpSecurity> ->
oauth2login.tokenEndpoint { tokenEndpoint ->
tokenEndpoint.accessTokenResponseClient(authorizationCodeTokenResponseClient)
}
}.logout { logout ->
logout.logoutSuccessHandler(oidcLogoutSuccessHandler())
}
...
Now Keycloak has access to the registered clients and invokes them successfully:
18:35:51,683 DEBUG [org.keycloak.services.managers.AuthenticationManager] (default task-1) backchannel logout to: resource-server-1
18:35:51,698 DEBUG [org.keycloak.services.managers.ResourceAdminManager] (default task-1) logout resource resource-server-1 url: http://localhost:8181/resource-server-1 sessionIds: [B456ED635C39465032300ABC48BFDE57]
Comment From: codependent
@jgrandja Could you give me some hint how to replicate this with a Webflux reactive application?
Comment From: jgrandja
@codependent
For the reactive side, you can supply a custom WebClient to WebClientReactiveAuthorizationCodeTokenResponseClient.setWebClient() configured with an ExchangeFilterFunction that could modify the request by adding the additional parameter(s). See this comment for an example. NOTE: The example performs response post-processing so in your case you would use ExchangeFilterFunction.ofRequestProcessor() for request pre-processing.
Comment From: codependent
Just one more question: since RequestContextHolder is not available in Webflux, how could I access the session id in my ExchangeFilterFunction?
val tokenResponseFilter = ExchangeFilterFunction.ofRequestProcessor { request: ClientRequest ->
val builder = ClientRequest.from(request)
//TODO Retrieve session id and add it as a request parameter
Mono.just(builder.build())
}
Comment From: jgrandja
You could access ServerWebExchange from Reactor's context. See this example.
Just a heads up that we use GitHub for bug reports/tracking and new feature requests only. Questions should be asked on StackOverflow.
Closing this issue in favour of #7845
Comment From: mcejp
For anyone stumbling onto this issue months (years) later: OpenID Connect Back-Channel Logout was shipped in Keycloak 12: - https://www.keycloak.org/2020/12/keycloak-1200-released.html - https://web.archive.org/web/20200922183709/https://issues.redhat.com/browse/KEYCLOAK-2940 - https://github.com/keycloak/keycloak/pull/7272