Skip to content

Support Keycloak's OIDC backchannel logout #7770

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

Closed
codependent opened this issue Dec 25, 2019 · 11 comments
Closed

Support Keycloak's OIDC backchannel logout #7770

codependent opened this issue Dec 25, 2019 · 11 comments
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: duplicate A duplicate of another issue

Comments

@codependent
Copy link

codependent commented Dec 25, 2019

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

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Dec 25, 2019
@codependent
Copy link
Author

codependent commented Dec 26, 2019

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

@jgrandja
Copy link
Contributor

@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?

@jgrandja jgrandja added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-triage An issue we've not yet triaged labels Jan 14, 2020
@codependent
Copy link
Author

@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:

  1. 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.

  2. 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.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jan 14, 2020
@jgrandja
Copy link
Contributor

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.

@jgrandja jgrandja added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Jan 15, 2020
@codependent
Copy link
Author

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:

  1. Is there a way to access the HttpSession id and the host name in private MultiValueMap<String, String> buildFormParameters? Maybe with RequestContextHolder? And in a webflux application?

  2. How can I replace OAuth2AuthorizationCodeGrantRequestEntityConverter with my own extension in the security context configuration?

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jan 16, 2020
@codependent
Copy link
Author

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]

@codependent
Copy link
Author

codependent commented Jan 16, 2020

@jgrandja Could you give me some hint how to replicate this with a Webflux reactive application?

@codependent codependent reopened this Jan 16, 2020
@jgrandja
Copy link
Contributor

@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.

@jgrandja jgrandja added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Jan 17, 2020
@jgrandja jgrandja self-assigned this Jan 17, 2020
@codependent
Copy link
Author

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())
        }

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jan 17, 2020
@jgrandja
Copy link
Contributor

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

@jgrandja jgrandja added status: duplicate A duplicate of another issue and removed status: feedback-provided Feedback has been provided labels Jan 20, 2020
@mcejp
Copy link

mcejp commented Feb 17, 2021

For anyone stumbling onto this issue months (years) later: OpenID Connect Back-Channel Logout was shipped in Keycloak 12:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: duplicate A duplicate of another issue
Projects
None yet
Development

No branches or pull requests

4 participants