Summary

I'm using an OIDC Provider that supports OIDC Back-channel Logout Spec. However the current version of Spring Security doesn't implement this functionality.

Actual Behavior

There's no way to have single sign out.

Expected Behavior

Single sign out from all RPs in which the user has authenticated with SSO.

Configuration


Version

5.2

Comment From: ThomasVitale

@jgrandja is there any plan for this task? From January, I'll be available to help with it, if needed.

Comment From: jgrandja

@ThomasVitale I haven't planned this for 5.5 and since RC1 is Feb 3 it might be tight.

Either way, if you're still interested on working on this we can see how the timing goes and always fallback to 5.6. Thoughts?

Comment From: ThomasVitale

@jgrandja I also think that hitting RC1 for 5.5 might be tight. But I'm still interested and I can start working on it right away. Do you have any pointers or should I start drafting some design ideas first?

Comment From: jgrandja

@ThomasVitale Apologies for the delayed response.

Let's plan on getting this in for the release after 5.5, so there is no pressure on rushing for this.

Do you have any pointers or should I start drafting some design ideas first?

I haven't really given it much thought so I think starting to draft some design ideas makes a lot of sense.

Thanks!

Comment From: jgrandja

@ThomasVitale Here are the specs for managing OIDC sessions.

I would encourage you to read all 3 in order to gain a holistic view on OIDC session management.

Comment From: ThomasVitale

@jgrandja perfect, thanks, I'll do that.

Comment From: ThomasVitale

Update: I've studied the documentation and started drafting some design ideas. I'll share them asap.

Comment From: ThomasVitale

@jgrandja I identified the following three main tasks. I checked the points for which I have uploaded a draft solution so far, and added some questions to clarify the solution. What do you think?

1. Domain model, validation, decoding, serialization

Id Token - [x] Add optional "sid" field (OidcIdToken)

Logout Token

  • [x] Create data model (OidcLogoutToken)
  • [x] Define mixin for Jackson (de)serialization (OidcLogoutTokenMixin)
  • [x] Define validation logic (OidcLogoutTokenValidator)
  • [ ] Define decoder

Client Registration - [x] Add optional "backchannelLogoutUri" parameter to ClientRegistration.

Questions * How much should/can I refactor to reuse code between Id Token and Logout Token? For the validator and the decoder there's room for improvements, I think. * An optional validation rule for the Logout Token (bullet 7 here) involves storing all the incoming tokens to check if the same jti value is used in more than one token. Is it something we want to support? * For the Id Token, we use a DefaultOidcIdTokenValidatorFactory which is a function taking ClientRegistration as input argument. For the Logout Token, we need both a ClientRegistration and an optional Id Token. For the reactive version, I could use a Tuple, but I would prefer having a single implementation for consistency with the OidcIdToken validator. Is it ok if I encapsulate the two objects into a new LogoutTokenValidationInput or something like that? * I guess we want a default value for "backchannelLogoutUri". Should it be part of ClientRegistration? Or should it be up to the client of the class to check if it's defined (i.e. the filter mentioned in the last task)?

2. Terminate current authenticated sessions

I've been thinking a lot about how to retrieve the OidcIdToken given a "sid" value and how to invalidate current sessions either by "sid" (when a sid is specified) or all of them (when no sid is specified), also considering the integration with Spring Session. I don't have a valid proposal yet. Do you have any suggestion?

3. Define a filter to intercept backchannel logout requests

  • [ ] Define the new filter (OidcBackchannelLogoutRequestFilter) intercepting incoming requests from the OIDC Provider based on the "backchannelLogoutUri" defined for the given ClientRegistration.
  • [ ] Define a resolver for a backchannel logout request, which leverages the business logic implemented in the previous task (OidcBackchannelLogoutRequestResolver). Returns 200, 400, or 501 according to the specs, and uses the recommended Cache-Control and Pragma headers.

I'm currently drafting this part. Any comment?

Comment From: jgrandja

Thanks for the detailed comments @ThomasVitale !

I would really appreciate if you can wait on my feedback.

I'm currently backlogged as we're targeting Spring Security 5.5.0-RC1 April 12 and Spring Authorization Server 0.1.1 April 15.

Comment From: ThomasVitale

@jgrandja absolutely no problem, I'll wait.

Comment From: a-i-ks

Good to read that this feature is on the agenda. Is there already a rough estimate of when it will be included in a release? @ThomasVitale , do you know of any work-around to achieve the behaviour without official support from Spring Security? Thanks for your work!

Comment From: jgrandja

@ThomasVitale Thank you for your patience!

Add optional "sid" field (OidcIdToken)

Yes. The attribute name for OidcIdToken should be sessionId

Create data model (OidcLogoutToken)

I don't think we need this at the moment. The OidcClientLogoutFilter will pass the logout token as a String to OidcClientLogoutAuthenticationProvider, which will validate it via configured JwtDecoder. If valid, it will invalidate the client session.

Define validation logic (OidcLogoutTokenValidator)

Yes. And this OAuth2TokenValidator would be associated with the NimbusJwtDecoder, which is associated with the OidcClientLogoutAuthenticationProvider.

Add optional "backchannelLogoutUri" parameter to ClientRegistration.

Instead of adding backchannelLogoutUri let's use ClientRegistration.configurationMetadata. Please see comment

How much should/can I refactor to reuse code between Id Token and Logout Token?

For the initial draft PR, let's avoid touching existing code and see where there is duplication. Then we can consider re-factoring for reuse if necessary.

An optional validation rule for the Logout Token (bullet 7 here) involves storing all the incoming tokens to check if the same jti value

Let's keep this out for now as it will add more complexity in storing the jti. But let's revisit this after we have most of the implementation in place and then we can reevaluate it to see if we should add it.

For the Id Token, we use a DefaultOidcIdTokenValidatorFactory which is a function taking ClientRegistration as input argument. For the Logout Token, we need both a ClientRegistration and an optional Id Token.

If you can't reuse DefaultOidcIdTokenValidatorFactory, then encapsulate all the decoding logic within the OidcClientLogoutAuthenticationProvider and then we'll see how we can reuse.

Terminate current authenticated sessions....I don't have a valid proposal yet. Do you have any suggestion?

I haven't thought about this yet but I think what might need to happen is that the session is invalidated when the next request comes into the client application. We would need to determine if the session has a "pending" back-channel logout event and if yes then invalidate at that point.

Define the new filter (OidcBackchannelLogoutRequestFilter)

I proposed OidcClientLogoutFilter but I think your proposal might be better. Consider both and we'll go from there in the review.

Define OidcBackchannelLogoutRequestResolver

I'm not sure if this is needed. All logic could be encapsulated in the Filter.

Comment From: ThomasVitale

@jgrandja thanks for your feedback. I'll keep working on the task accordingly.

Comment From: ThomasVitale

Sorry for the long waiting time. I plan to submit the new draft solution later this month.

Comment From: jgrandja

No worries @ThomasVitale. Whenever you have time.

Comment From: heruan

Hello! Just found this as I'm looking on how to get OIDC back-channel to work for my Spring app. Any update?

Comment From: jgrandja

@heruan There are no updates at this time. But we will try our best to get this feature in after 6.0 is released.

Comment From: dalbani

Exciting to see it be moved from Planning to Prioritized 👍

Comment From: jzheaux

@ThomasVitale, I've just started working on this. If you have a branch that you'd like to share, I'd be happy to see what I can incorporate. If not, that's no problem.

Comment From: ThomasVitale

@jzheaux thanks a lot for picking this up, I'm sorry I didn't manage to complete it. This is the initial draft I worked on: https://github.com/ThomasVitale/spring-security/commit/b58a6fa092df5ddbf53983da82ba160136bbfa6b

Comment From: ch4mpy

@jzheaux is there an estimation of when this Back-Chanel Logout feature will be available for OAuth2 clients?

Quite a few users of the deprecated Keycloak adapters for spring-security 5 are using this feature, labeled as Single Sign On (which implied Single Sign Out), and are blocked in their clients migration to spring-security 6.

As I am trying to replace rich browser application security, currently implemented as OAuth2 public clients, with session cookies and spring-cloud-gateway as BFF (configured as OAuth2 confidential client), I was thrilled when I saw this issue move to "In progress": this Single Sign Out feature would be more than just "nice to have" on a Spring Gateway configured as OAuth2 client

Comment From: ch4mpy

I have implementations for both servlet and reactive applications.

Both are based on the same mechanisms: override the authorized client repository to keep an index of authorized clients keys (OP issuer and user subject on that OP) for all sessions, so that, when the direct request from the OP is processed (without any user session), it is still possible to retrieve the authorized client to remove and the session to (potentially) invalidate: it should be invalidated only if the removed authorized client was the last one for the processed user: when a client is logged in with let's say Google and Github at the same time, the user session should not be invalidated if he logs out from only one of the two. Removing the right authorized client is enough.

The tough parts were: - working around the single tenant nature of the authorized client repository: it works pretty bad out of the box when a user authenticated with authorization-code on two different issuers on which he'll surely have different subjects - hooking in the session lifecycle for reactive applications which have no HttpSessionListener equivalent to do the cleanup on authorized client repository indexes when a session is removed.

Here is how I achieved the second point:

@Bean
WebSessionManager webSessionManager(WebSessionStore webSessionStore) {
    DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
    webSessionManager.setSessionStore(webSessionStore);
    return webSessionManager;
}

@Bean
WebSessionStore webSessionStore(ServerProperties serverProperties) {
    return new SpringAddonsWebSessionStore(serverProperties.getReactive().getSession().getTimeout());
}

public static interface WebSessionListener {

    default void sessionCreated(WebSession session) {
    }

    default void sessionRemoved(String sessionId) {
    }
}

static class SpringAddonsWebSessionStore implements WebSessionStore {
    private final InMemoryWebSessionStore delegate = new InMemoryWebSessionStore();
    private final ConcurrentLinkedQueue<WebSessionListener> webSessionListeners = new ConcurrentLinkedQueue<WebSessionListener>();

    private final Duration timeout;

    public SpringAddonsWebSessionStore(Duration timeout) {
        this.timeout = timeout;
    }

    public void addWebSessionListener(WebSessionListener listener) {
        webSessionListeners.add(listener);
    }

    @Override
    public Mono<WebSession> createWebSession() {
        return delegate.createWebSession().doOnSuccess(this::setMaxIdleTime)
                .doOnSuccess(session -> webSessionListeners.forEach(l -> l.sessionCreated(session)));
    }

    @Override
    public Mono<WebSession> retrieveSession(String sessionId) {
        return delegate.retrieveSession(sessionId);
    }

    @Override
    public Mono<Void> removeSession(String sessionId) {
        webSessionListeners.forEach(l -> l.sessionRemoved(sessionId));
        return delegate.removeSession(sessionId);
    }

    @Override
    public Mono<WebSession> updateLastAccessTime(WebSession webSession) {
        return delegate.updateLastAccessTime(webSession);
    }

    private void setMaxIdleTime(WebSession session) {
        session.setMaxIdleTime(this.timeout);
    }
}

For the rest (Back-Channel Logout controller and security filter-chain), I wrote spring-boot starters available from here for servlets and there for reactive applications.

That works with custom authorized client repositories for servlets and Webflux which have the double responsibility of bringing multi-tenancy support and keeping the indexes for authorized clients across sessions.

Comment From: maradanasai

Hello, any update on when this can be released?

Comment From: jgrandja

@maradanasai We're targeting 6.2. You can see the progress in gh-12570.

Comment From: jzheaux

Closed in https://github.com/spring-projects/spring-security/pull/12570/commits/b75b3dfe7bb81275b376faac85e02a46f99c410d