Skip to content
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

Support OpenID Connect Back-Channel Logout #7845

Closed
codependent opened this issue Jan 18, 2020 · 25 comments
Closed

Support OpenID Connect Back-Channel Logout #7845

codependent opened this issue Jan 18, 2020 · 25 comments
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: enhancement A general enhancement
Milestone

Comments

@codependent
Copy link

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

@codependent codependent changed the title Feature requets: OIDC Back-channel Logout Spec Feature request: OIDC Back-channel Logout Spec Jan 18, 2020
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jan 18, 2020
@jgrandja jgrandja changed the title Feature request: OIDC Back-channel Logout Spec Support OpenID Connect Back-Channel Logout Jan 20, 2020
@jgrandja jgrandja added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Jan 20, 2020
@jgrandja jgrandja modified the milestone: 5.4.x Mar 11, 2020
@ThomasVitale
Copy link
Contributor

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

@jgrandja
Copy link
Contributor

jgrandja commented Jan 5, 2021

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

@ThomasVitale
Copy link
Contributor

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

@jgrandja
Copy link
Contributor

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

@jgrandja
Copy link
Contributor

jgrandja commented Jan 19, 2021

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

@ThomasVitale
Copy link
Contributor

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

@ThomasVitale
Copy link
Contributor

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

@ThomasVitale
Copy link
Contributor

ThomasVitale commented Mar 30, 2021

@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

  • Add optional "sid" field (OidcIdToken)

Logout Token

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

Client Registration

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

@jgrandja
Copy link
Contributor

jgrandja commented Apr 5, 2021

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.

@ThomasVitale
Copy link
Contributor

@jgrandja absolutely no problem, I'll wait.

@a-i-ks
Copy link

a-i-ks commented Apr 30, 2021

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!

@jgrandja
Copy link
Contributor

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

@jgrandja jgrandja added this to the 5.6.x milestone May 17, 2021
@ThomasVitale
Copy link
Contributor

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

@ThomasVitale
Copy link
Contributor

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

@jgrandja
Copy link
Contributor

jgrandja commented Jul 9, 2021

No worries @ThomasVitale. Whenever you have time.

@jgrandja jgrandja modified the milestones: 5.6.x, 5.7.x Oct 20, 2021
@jgrandja jgrandja removed this from the 5.7.x milestone Mar 11, 2022
@jgrandja jgrandja added this to the 6.0.x milestone Mar 11, 2022
@jgrandja jgrandja removed this from the 6.0.x milestone Jun 13, 2022
@heruan
Copy link
Contributor

heruan commented Aug 19, 2022

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

@jgrandja
Copy link
Contributor

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

@jgrandja jgrandja self-assigned this Jan 12, 2023
@dalbani
Copy link

dalbani commented Jan 12, 2023

Exciting to see it be moved from Planning to Prioritized 👍

@jzheaux jzheaux assigned jzheaux and unassigned ThomasVitale and jgrandja Jan 13, 2023
@jzheaux
Copy link
Contributor

jzheaux commented Jan 13, 2023

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

@ThomasVitale
Copy link
Contributor

@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

@ch4mpy
Copy link
Contributor

ch4mpy commented Feb 9, 2023

@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

@ch4mpy
Copy link
Contributor

ch4mpy commented Mar 16, 2023

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.

@maradanasai
Copy link

Hello, any update on when this can be released?

@jgrandja
Copy link
Contributor

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

@jgrandja jgrandja added this to the 6.2.0-M3 milestone Oct 13, 2023
@jzheaux
Copy link
Contributor

jzheaux commented Oct 17, 2023

Closed in b75b3df

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) type: enhancement A general enhancement
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

10 participants