Skip to content

Commit cb33fd7

Browse files
committed
Add OIDC Back-Channel Logout Support
Closes gh-12570
1 parent 1461c0f commit cb33fd7

File tree

51 files changed

+5397
-114
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+5397
-114
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import org.springframework.security.config.annotation.web.configurers.X509Configurer;
7171
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer;
7272
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
73+
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer;
7374
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
7475
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer;
7576
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer;
@@ -2835,6 +2836,16 @@ public HttpSecurity oauth2Login(Customizer<OAuth2LoginConfigurer<HttpSecurity>>
28352836
return HttpSecurity.this;
28362837
}
28372838

2839+
public OidcLogoutConfigurer<HttpSecurity> oidcLogout() throws Exception {
2840+
return getOrApply(new OidcLogoutConfigurer<>());
2841+
}
2842+
2843+
public HttpSecurity oidcLogout(Customizer<OidcLogoutConfigurer<HttpSecurity>> oidcLogoutCustomizer)
2844+
throws Exception {
2845+
oidcLogoutCustomizer.customize(getOrApply(new OidcLogoutConfigurer<>()));
2846+
return HttpSecurity.this;
2847+
}
2848+
28382849
/**
28392850
* Configures OAuth 2.0 Client support.
28402851
* @return the {@link OAuth2ClientConfigurer} for further customizations

config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ public SessionManagementConfigurer<H> sessionAuthenticationStrategy(
296296
* @param sessionAuthenticationStrategy
297297
* @return the {@link SessionManagementConfigurer} for further customizations
298298
*/
299-
SessionManagementConfigurer<H> addSessionAuthenticationStrategy(
299+
public SessionManagementConfigurer<H> addSessionAuthenticationStrategy(
300300
SessionAuthenticationStrategy sessionAuthenticationStrategy) {
301301
this.sessionAuthenticationStrategies.add(sessionAuthenticationStrategy);
302302
return this;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
18+
19+
import java.util.function.Function;
20+
21+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
22+
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
23+
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
24+
import org.springframework.security.oauth2.jwt.Jwt;
25+
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
26+
27+
final class DefaultOidcLogoutTokenValidatorFactory implements Function<ClientRegistration, OAuth2TokenValidator<Jwt>> {
28+
29+
@Override
30+
public OAuth2TokenValidator<Jwt> apply(ClientRegistration clientRegistration) {
31+
return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
32+
new OidcBackChannelLogoutTokenValidator(clientRegistration));
33+
}
34+
35+
}

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
2626
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
2727
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
28+
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
29+
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
2830
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
2931
import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
3032
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
@@ -112,4 +114,13 @@ private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizedClientService
112114
return (!authorizedClientServiceMap.isEmpty() ? authorizedClientServiceMap.values().iterator().next() : null);
113115
}
114116

117+
static <B extends HttpSecurityBuilder<B>> OidcSessionRegistry getOidcSessionRegistry(B builder) {
118+
OidcSessionRegistry sessionRegistry = builder.getSharedObject(OidcSessionRegistry.class);
119+
if (sessionRegistry == null) {
120+
sessionRegistry = new InMemoryOidcSessionRegistry();
121+
builder.setSharedObject(OidcSessionRegistry.class, sessionRegistry);
122+
}
123+
return sessionRegistry;
124+
}
125+
115126
}

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,43 @@
2222
import java.util.LinkedHashMap;
2323
import java.util.Map;
2424

25+
import jakarta.servlet.http.HttpServletRequest;
26+
import jakarta.servlet.http.HttpServletResponse;
27+
import jakarta.servlet.http.HttpSession;
28+
import org.apache.commons.logging.Log;
29+
import org.apache.commons.logging.LogFactory;
30+
2531
import org.springframework.beans.factory.BeanFactoryUtils;
2632
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
2733
import org.springframework.context.ApplicationContext;
34+
import org.springframework.context.ApplicationListener;
35+
import org.springframework.context.event.GenericApplicationListenerAdapter;
36+
import org.springframework.context.event.SmartApplicationListener;
2837
import org.springframework.core.ResolvableType;
2938
import org.springframework.security.authentication.AuthenticationProvider;
3039
import org.springframework.security.config.Customizer;
3140
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
3241
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3342
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
3443
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
44+
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
45+
import org.springframework.security.context.DelegatingApplicationListener;
3546
import org.springframework.security.core.Authentication;
3647
import org.springframework.security.core.AuthenticationException;
3748
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
49+
import org.springframework.security.core.session.AbstractSessionEvent;
50+
import org.springframework.security.core.session.SessionDestroyedEvent;
51+
import org.springframework.security.core.session.SessionIdChangedEvent;
3852
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
3953
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider;
4054
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
4155
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
4256
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
4357
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
4458
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider;
59+
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
60+
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
61+
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
4562
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
4663
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
4764
import org.springframework.security.oauth2.client.registration.ClientRegistration;
@@ -67,7 +84,10 @@
6784
import org.springframework.security.web.RedirectStrategy;
6885
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
6986
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
87+
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
88+
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
7089
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
90+
import org.springframework.security.web.csrf.CsrfToken;
7191
import org.springframework.security.web.savedrequest.RequestCache;
7292
import org.springframework.security.web.util.matcher.AndRequestMatcher;
7393
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@@ -124,6 +144,7 @@
124144
* <li>{@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not
125145
* configured and {@code DefaultLoginPageGeneratingFilter} is available, then a default
126146
* login page will be made available</li>
147+
* <li>{@link OidcSessionRegistry}</li>
127148
* </ul>
128149
*
129150
* @author Joe Grandja
@@ -202,6 +223,18 @@ public OAuth2LoginConfigurer<B> loginProcessingUrl(String loginProcessingUrl) {
202223
return this;
203224
}
204225

226+
/**
227+
* Sets the registry for managing the OIDC client-provider session link
228+
* @param oidcSessionRegistry the {@link OidcSessionRegistry} to use
229+
* @return the {@link OAuth2LoginConfigurer} for further configuration
230+
* @since 6.2
231+
*/
232+
public OAuth2LoginConfigurer<B> oidcSessionRegistry(OidcSessionRegistry oidcSessionRegistry) {
233+
Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null");
234+
getBuilder().setSharedObject(OidcSessionRegistry.class, oidcSessionRegistry);
235+
return this;
236+
}
237+
205238
/**
206239
* Returns the {@link AuthorizationEndpointConfig} for configuring the Authorization
207240
* Server's Authorization Endpoint.
@@ -397,6 +430,7 @@ public void configure(B http) throws Exception {
397430
authenticationFilter
398431
.setAuthorizationRequestRepository(this.authorizationEndpointConfig.authorizationRequestRepository);
399432
}
433+
configureOidcSessionRegistry(http);
400434
super.configure(http);
401435
}
402436

@@ -546,6 +580,29 @@ private RequestMatcher getFormLoginNotEnabledRequestMatcher(B http) {
546580
return AnyRequestMatcher.INSTANCE;
547581
}
548582

583+
private void configureOidcSessionRegistry(B http) {
584+
OidcSessionRegistry sessionRegistry = OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http);
585+
SessionManagementConfigurer<B> sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class);
586+
if (sessionConfigurer != null) {
587+
OidcSessionRegistryAuthenticationStrategy sessionAuthenticationStrategy = new OidcSessionRegistryAuthenticationStrategy();
588+
sessionAuthenticationStrategy.setSessionRegistry(sessionRegistry);
589+
sessionConfigurer.addSessionAuthenticationStrategy(sessionAuthenticationStrategy);
590+
}
591+
OidcClientSessionEventListener listener = new OidcClientSessionEventListener();
592+
listener.setSessionRegistry(sessionRegistry);
593+
registerDelegateApplicationListener(listener);
594+
}
595+
596+
private void registerDelegateApplicationListener(ApplicationListener<?> delegate) {
597+
DelegatingApplicationListener delegating = getBeanOrNull(
598+
ResolvableType.forType(DelegatingApplicationListener.class));
599+
if (delegating == null) {
600+
return;
601+
}
602+
SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate);
603+
delegating.addListener(smartListener);
604+
}
605+
549606
/**
550607
* Configuration options for the Authorization Server's Authorization Endpoint.
551608
*/
@@ -793,4 +850,83 @@ public boolean supports(Class<?> authentication) {
793850

794851
}
795852

853+
private static final class OidcClientSessionEventListener implements ApplicationListener<AbstractSessionEvent> {
854+
855+
private final Log logger = LogFactory.getLog(OidcClientSessionEventListener.class);
856+
857+
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
858+
859+
/**
860+
* {@inheritDoc}
861+
*/
862+
@Override
863+
public void onApplicationEvent(AbstractSessionEvent event) {
864+
if (event instanceof SessionDestroyedEvent destroyed) {
865+
this.logger.debug("Received SessionDestroyedEvent");
866+
this.sessionRegistry.removeSessionInformation(destroyed.getId());
867+
return;
868+
}
869+
if (event instanceof SessionIdChangedEvent changed) {
870+
this.logger.debug("Received SessionIdChangedEvent");
871+
OidcSessionInformation information = this.sessionRegistry.removeSessionInformation(changed.getOldSessionId());
872+
if (information == null) {
873+
this.logger.debug("Failed to register new session id since old session id was not found in registry");
874+
return;
875+
}
876+
this.sessionRegistry.saveSessionInformation(information.withSessionId(changed.getNewSessionId()));
877+
}
878+
}
879+
880+
/**
881+
* The registry where OIDC Provider sessions are linked to the Client session.
882+
* Defaults to in-memory storage.
883+
* @param sessionRegistry the {@link OidcSessionRegistry} to use
884+
*/
885+
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
886+
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
887+
this.sessionRegistry = sessionRegistry;
888+
}
889+
890+
}
891+
892+
private static final class OidcSessionRegistryAuthenticationStrategy implements SessionAuthenticationStrategy {
893+
894+
private final Log logger = LogFactory.getLog(getClass());
895+
896+
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
897+
898+
/**
899+
* {@inheritDoc}
900+
*/
901+
@Override
902+
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) throws SessionAuthenticationException {
903+
HttpSession session = request.getSession(false);
904+
if (session == null) {
905+
return;
906+
}
907+
if (!(authentication.getPrincipal() instanceof OidcUser user)) {
908+
return;
909+
}
910+
String sessionId = session.getId();
911+
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
912+
Map<String, String> headers = (csrfToken != null) ? Map.of(csrfToken.getHeaderName(), csrfToken.getToken()) : Collections.emptyMap();
913+
OidcSessionInformation registration = new OidcSessionInformation(sessionId, headers, user);
914+
if (this.logger.isTraceEnabled()) {
915+
this.logger.trace(String.format("Linking a provider [%s] session to this client's session", user.getIssuer()));
916+
}
917+
this.sessionRegistry.saveSessionInformation(registration);
918+
}
919+
920+
/**
921+
* The registration for linking OIDC Provider Session information to the Client's
922+
* session. Defaults to in-memory storage.
923+
* @param sessionRegistry the {@link OidcSessionRegistry} to use
924+
*/
925+
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
926+
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
927+
this.sessionRegistry = sessionRegistry;
928+
}
929+
930+
}
931+
796932
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
18+
19+
import java.util.Collections;
20+
21+
import org.springframework.security.authentication.AbstractAuthenticationToken;
22+
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
23+
24+
/**
25+
* An {@link org.springframework.security.core.Authentication} implementation that
26+
* represents the result of authenticating an OIDC Logout token for the purposes of
27+
* performing Back-Channel Logout.
28+
*
29+
* @author Josh Cummings
30+
* @since 6.2
31+
* @see OidcLogoutAuthenticationToken
32+
* @see <a target="_blank" href=
33+
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel
34+
* Logout</a>
35+
*/
36+
class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
37+
38+
private final OidcLogoutToken logoutToken;
39+
40+
/**
41+
* Construct an {@link OidcBackChannelLogoutAuthentication}
42+
* @param logoutToken a deserialized, verified OIDC Logout Token
43+
*/
44+
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) {
45+
super(Collections.emptyList());
46+
this.logoutToken = logoutToken;
47+
setAuthenticated(true);
48+
}
49+
50+
/**
51+
* {@inheritDoc}
52+
*/
53+
@Override
54+
public OidcLogoutToken getPrincipal() {
55+
return this.logoutToken;
56+
}
57+
58+
/**
59+
* {@inheritDoc}
60+
*/
61+
@Override
62+
public OidcLogoutToken getCredentials() {
63+
return this.logoutToken;
64+
}
65+
66+
}

0 commit comments

Comments
 (0)