Skip to content

Commit b75b3df

Browse files
committed
Add OIDC Back-Channel Logout Support
1 parent a757144 commit b75b3df

File tree

25 files changed

+2440
-2
lines changed

25 files changed

+2440
-2
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> oauth2Logout() throws Exception {
2840+
return getOrApply(new OidcLogoutConfigurer<>());
2841+
}
2842+
2843+
public HttpSecurity oauth2Logout(Customizer<OidcLogoutConfigurer<HttpSecurity>> oauth2LogoutCustomizer)
2844+
throws Exception {
2845+
oauth2LogoutCustomizer.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,281 @@
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.Consumer;
20+
21+
import jakarta.servlet.http.HttpServletRequest;
22+
import jakarta.servlet.http.HttpServletResponse;
23+
import jakarta.servlet.http.HttpSession;
24+
import org.apache.commons.logging.Log;
25+
import org.apache.commons.logging.LogFactory;
26+
27+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
28+
import org.springframework.context.ApplicationContext;
29+
import org.springframework.context.ApplicationListener;
30+
import org.springframework.context.event.GenericApplicationListenerAdapter;
31+
import org.springframework.context.event.SmartApplicationListener;
32+
import org.springframework.security.authentication.AuthenticationManager;
33+
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
34+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
35+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
36+
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
37+
import org.springframework.security.context.DelegatingApplicationListener;
38+
import org.springframework.security.core.Authentication;
39+
import org.springframework.security.core.session.AbstractSessionEvent;
40+
import org.springframework.security.core.session.SessionDestroyedEvent;
41+
import org.springframework.security.core.session.SessionIdChangedEvent;
42+
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcBackChannelLogoutAuthenticationManager;
43+
import org.springframework.security.oauth2.client.oidc.authentication.session.InMemoryOidcProviderSessionRegistry;
44+
import org.springframework.security.oauth2.client.oidc.authentication.session.OidcProviderSessionRegistration;
45+
import org.springframework.security.oauth2.client.oidc.authentication.session.OidcProviderSessionRegistry;
46+
import org.springframework.security.oauth2.client.oidc.web.authentication.logout.OidcBackChannelLogoutFilter;
47+
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
48+
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
49+
import org.springframework.security.web.authentication.logout.BackchannelLogoutHandler;
50+
import org.springframework.security.web.authentication.logout.LogoutHandler;
51+
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
52+
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
53+
import org.springframework.security.web.csrf.CsrfFilter;
54+
import org.springframework.security.web.csrf.CsrfToken;
55+
import org.springframework.util.Assert;
56+
57+
/**
58+
* An {@link AbstractHttpConfigurer} for OAuth 2.0 Logout flows
59+
*
60+
* <p>
61+
* OAuth 2.0 Logout provides an application with the capability to have users log out by
62+
* using their existing account at an OAuth 2.0 or OpenID Connect 1.0 Provider.
63+
*
64+
*
65+
* <h2>Security Filters</h2>
66+
*
67+
* The following {@code Filter} is populated:
68+
*
69+
* <ul>
70+
* <li>{@link OidcBackChannelLogoutFilter}</li>
71+
* </ul>
72+
*
73+
* <h2>Shared Objects Used</h2>
74+
*
75+
* The following shared objects are used:
76+
*
77+
* <ul>
78+
* <li>{@link ClientRegistrationRepository}</li>
79+
* </ul>
80+
*
81+
* @author Josh Cummings
82+
* @since 6.1
83+
* @see HttpSecurity#oauth2Logout()
84+
* @see OidcBackChannelLogoutFilter
85+
* @see ClientRegistrationRepository
86+
*/
87+
public final class OidcLogoutConfigurer<B extends HttpSecurityBuilder<B>>
88+
extends AbstractHttpConfigurer<OidcLogoutConfigurer<B>, B> {
89+
90+
private BackChannelLogoutConfigurer backChannel;
91+
92+
/**
93+
* Sets the repository of client registrations.
94+
* @param clientRegistrationRepository the repository of client registrations
95+
* @return the {@link OidcLogoutConfigurer} for further configuration
96+
*/
97+
public OidcLogoutConfigurer<B> backChannel(Consumer<BackChannelLogoutConfigurer> backChannelLogoutConfigurer) {
98+
if (this.backChannel == null) {
99+
this.backChannel = new BackChannelLogoutConfigurer();
100+
}
101+
backChannelLogoutConfigurer.accept(this.backChannel);
102+
return this;
103+
}
104+
105+
public B and() {
106+
return getBuilder();
107+
}
108+
109+
@Override
110+
public void configure(B builder) throws Exception {
111+
if (this.backChannel != null) {
112+
this.backChannel.configure(builder);
113+
}
114+
}
115+
116+
private void registerDelegateApplicationListener(ApplicationListener<?> delegate) {
117+
DelegatingApplicationListener delegating = getBeanOrNull(DelegatingApplicationListener.class);
118+
if (delegating == null) {
119+
return;
120+
}
121+
SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate);
122+
delegating.addListener(smartListener);
123+
}
124+
125+
private <T> T getBeanOrNull(Class<T> type) {
126+
ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
127+
if (context == null) {
128+
return null;
129+
}
130+
try {
131+
return context.getBean(type);
132+
}
133+
catch (NoSuchBeanDefinitionException ex) {
134+
return null;
135+
}
136+
}
137+
138+
public final class BackChannelLogoutConfigurer {
139+
140+
private LogoutHandler logoutHandler = new BackchannelLogoutHandler();
141+
142+
private AuthenticationManager authenticationManager = new OidcBackChannelLogoutAuthenticationManager();
143+
144+
private OidcProviderSessionRegistry providerSessionRegistry = new InMemoryOidcProviderSessionRegistry();
145+
146+
public BackChannelLogoutConfigurer clientLogoutHandler(LogoutHandler logoutHandler) {
147+
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
148+
this.logoutHandler = logoutHandler;
149+
return this;
150+
}
151+
152+
public BackChannelLogoutConfigurer authenticationManager(AuthenticationManager authenticationManager) {
153+
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
154+
this.authenticationManager = authenticationManager;
155+
return this;
156+
}
157+
158+
public BackChannelLogoutConfigurer oidcProviderSessionRegistry(
159+
OidcProviderSessionRegistry providerSessionRegistry) {
160+
Assert.notNull(providerSessionRegistry, "providerSessionRegistry cannot be null");
161+
this.providerSessionRegistry = providerSessionRegistry;
162+
return this;
163+
}
164+
165+
private AuthenticationManager authenticationManager() {
166+
return this.authenticationManager;
167+
}
168+
169+
private OidcProviderSessionRegistry oidcProviderSessionRegistry() {
170+
return this.providerSessionRegistry;
171+
}
172+
173+
private LogoutHandler logoutHandler() {
174+
return this.logoutHandler;
175+
}
176+
177+
private SessionAuthenticationStrategy sessionAuthenticationStrategy() {
178+
OidcProviderSessionAuthenticationStrategy strategy = new OidcProviderSessionAuthenticationStrategy();
179+
strategy.setProviderSessionRegistry(oidcProviderSessionRegistry());
180+
return strategy;
181+
}
182+
183+
void configure(B http) {
184+
ClientRegistrationRepository clientRegistrationRepository = OAuth2ClientConfigurerUtils
185+
.getClientRegistrationRepository(http);
186+
OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(clientRegistrationRepository,
187+
authenticationManager());
188+
filter.setProviderSessionRegistry(oidcProviderSessionRegistry());
189+
LogoutHandler expiredStrategy = logoutHandler();
190+
filter.setLogoutHandler(expiredStrategy);
191+
http.addFilterBefore(filter, CsrfFilter.class);
192+
SessionManagementConfigurer<B> sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class);
193+
if (sessionConfigurer != null) {
194+
sessionConfigurer.addSessionAuthenticationStrategy(sessionAuthenticationStrategy());
195+
}
196+
OidcClientSessionEventListener listener = new OidcClientSessionEventListener();
197+
listener.setProviderSessionRegistry(this.providerSessionRegistry);
198+
registerDelegateApplicationListener(listener);
199+
}
200+
201+
static final class OidcClientSessionEventListener implements ApplicationListener<AbstractSessionEvent> {
202+
203+
private final Log logger = LogFactory.getLog(OidcClientSessionEventListener.class);
204+
205+
private OidcProviderSessionRegistry providerSessionRegistry = new InMemoryOidcProviderSessionRegistry();
206+
207+
/**
208+
* {@inheritDoc}
209+
*/
210+
@Override
211+
public void onApplicationEvent(AbstractSessionEvent event) {
212+
if (event instanceof SessionDestroyedEvent destroyed) {
213+
this.logger.debug("Received SessionDestroyedEvent");
214+
this.providerSessionRegistry.deregister(destroyed.getId());
215+
return;
216+
}
217+
if (event instanceof SessionIdChangedEvent changed) {
218+
this.logger.debug("Received SessionIdChangedEvent");
219+
this.providerSessionRegistry.reregister(changed.getOldSessionId(), changed.getNewSessionId());
220+
}
221+
}
222+
223+
/**
224+
* The registry where OIDC Provider sessions are linked to the Client session.
225+
* Defaults to in-memory storage.
226+
* @param providerSessionRegistry the {@link OidcProviderSessionRegistry} to
227+
* use
228+
*/
229+
void setProviderSessionRegistry(OidcProviderSessionRegistry providerSessionRegistry) {
230+
Assert.notNull(providerSessionRegistry, "providerSessionRegistry cannot be null");
231+
this.providerSessionRegistry = providerSessionRegistry;
232+
}
233+
234+
}
235+
236+
static final class OidcProviderSessionAuthenticationStrategy implements SessionAuthenticationStrategy {
237+
238+
private final Log logger = LogFactory.getLog(getClass());
239+
240+
private OidcProviderSessionRegistry providerSessionRegistry = new InMemoryOidcProviderSessionRegistry();
241+
242+
/**
243+
* {@inheritDoc}
244+
*/
245+
@Override
246+
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) throws SessionAuthenticationException {
247+
HttpSession session = request.getSession(false);
248+
if (session == null) {
249+
return;
250+
}
251+
if (authentication == null) {
252+
return;
253+
}
254+
if (!(authentication.getPrincipal() instanceof OidcUser user)) {
255+
return;
256+
}
257+
String sessionId = session.getId();
258+
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
259+
OidcProviderSessionRegistration registration = new OidcProviderSessionRegistration(sessionId, csrfToken, user);
260+
if (this.logger.isTraceEnabled()) {
261+
this.logger.trace(String.format("Linking a provider [%s] session to this client's session", user.getIssuer()));
262+
}
263+
this.providerSessionRegistry.register(registration);
264+
}
265+
266+
/**
267+
* The registration for linking OIDC Provider Session information to the
268+
* Client's session. Defaults to in-memory.
269+
* @param providerSessionRegistry the {@link OidcProviderSessionRegistry} to
270+
* use
271+
*/
272+
void setProviderSessionRegistry(OidcProviderSessionRegistry providerSessionRegistry) {
273+
Assert.notNull(providerSessionRegistry, "providerSessionRegistry cannot be null");
274+
this.providerSessionRegistry = providerSessionRegistry;
275+
}
276+
277+
}
278+
279+
}
280+
281+
}

0 commit comments

Comments
 (0)