Skip to content

Commit 3ab3236

Browse files
committed
Do Not Wire Default OidcSessionStrategy without OidcLogoutConfigurer
Closes gh-14558
1 parent eea4279 commit 3ab3236

File tree

6 files changed

+172
-13
lines changed

6 files changed

+172
-13
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -582,6 +582,10 @@ private RequestMatcher getFormLoginNotEnabledRequestMatcher(B http) {
582582
}
583583

584584
private void configureOidcSessionRegistry(B http) {
585+
if (http.getConfigurer(OidcLogoutConfigurer.class) == null
586+
&& http.getSharedObject(OidcSessionRegistry.class) == null) {
587+
return;
588+
}
585589
OidcSessionRegistry sessionRegistry = OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http);
586590
SessionManagementConfigurer<B> sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class);
587591
if (sessionConfigurer != null) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -3974,8 +3974,10 @@ protected void configure(ServerHttpSecurity http) {
39743974

39753975
ReactiveAuthenticationManager manager = getAuthenticationManager();
39763976
ReactiveOidcSessionRegistry sessionRegistry = getOidcSessionRegistry();
3977-
AuthenticationWebFilter authenticationFilter = new OidcSessionRegistryAuthenticationWebFilter(manager,
3978-
authorizedClientRepository, sessionRegistry);
3977+
AuthenticationWebFilter authenticationFilter = (sessionRegistry != null)
3978+
? new OidcSessionRegistryAuthenticationWebFilter(manager, authorizedClientRepository,
3979+
sessionRegistry)
3980+
: new OAuth2LoginAuthenticationWebFilter(manager, authorizedClientRepository);
39793981
authenticationFilter.setRequiresAuthenticationMatcher(getAuthenticationMatcher());
39803982
authenticationFilter
39813983
.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository));
@@ -3984,8 +3986,10 @@ protected void configure(ServerHttpSecurity http) {
39843986
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
39853987

39863988
setDefaultEntryPoints(http);
3987-
http.addFilterAfter(new OidcSessionRegistryWebFilter(sessionRegistry),
3988-
SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
3989+
if (sessionRegistry != null) {
3990+
http.addFilterAfter(new OidcSessionRegistryWebFilter(sessionRegistry),
3991+
SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
3992+
}
39893993
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
39903994
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
39913995
}
@@ -4031,6 +4035,9 @@ MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTM
40314035
}
40324036

40334037
private ReactiveOidcSessionRegistry getOidcSessionRegistry() {
4038+
if (ServerHttpSecurity.this.oidcLogout == null && this.oidcSessionRegistry == null) {
4039+
return null;
4040+
}
40344041
if (this.oidcSessionRegistry == null) {
40354042
this.oidcSessionRegistry = getBeanOrNull(ReactiveOidcSessionRegistry.class);
40364043
}
@@ -4269,8 +4276,7 @@ public Duration getMaxIdleTime() {
42694276

42704277
}
42714278

4272-
private static final class OidcSessionRegistryAuthenticationWebFilter
4273-
extends OAuth2LoginAuthenticationWebFilter {
4279+
static final class OidcSessionRegistryAuthenticationWebFilter extends OAuth2LoginAuthenticationWebFilter {
42744280

42754281
private final Log logger = LogFactory.getLog(getClass());
42764282

config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@
2222
import java.util.HashMap;
2323
import java.util.List;
2424
import java.util.Map;
25+
import java.util.stream.Collectors;
2526

2627
import org.apache.http.HttpHeaders;
2728
import org.junit.jupiter.api.AfterEach;
@@ -36,6 +37,7 @@
3637
import org.springframework.context.ConfigurableApplicationContext;
3738
import org.springframework.context.annotation.Bean;
3839
import org.springframework.context.annotation.Configuration;
40+
import org.springframework.context.event.SmartApplicationListener;
3941
import org.springframework.http.HttpStatus;
4042
import org.springframework.http.MediaType;
4143
import org.springframework.mock.web.MockFilterChain;
@@ -48,16 +50,19 @@
4850
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
4951
import org.springframework.security.config.test.SpringTestContext;
5052
import org.springframework.security.config.test.SpringTestContextExtension;
53+
import org.springframework.security.context.DelegatingApplicationListener;
5154
import org.springframework.security.core.Authentication;
5255
import org.springframework.security.core.GrantedAuthority;
5356
import org.springframework.security.core.authority.AuthorityUtils;
5457
import org.springframework.security.core.authority.SimpleGrantedAuthority;
5558
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
5659
import org.springframework.security.core.context.SecurityContextChangedListener;
5760
import org.springframework.security.core.context.SecurityContextHolderStrategy;
61+
import org.springframework.security.core.session.SessionDestroyedEvent;
5862
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
5963
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
6064
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
65+
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
6166
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
6267
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
6368
import org.springframework.security.oauth2.client.registration.ClientRegistration;
@@ -95,7 +100,9 @@
95100
import org.springframework.security.web.context.HttpRequestResponseHolder;
96101
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
97102
import org.springframework.security.web.context.SecurityContextRepository;
103+
import org.springframework.security.web.session.HttpSessionDestroyedEvent;
98104
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
105+
import org.springframework.test.util.ReflectionTestUtils;
99106
import org.springframework.test.web.servlet.MockMvc;
100107
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
101108

@@ -150,10 +157,10 @@ public class OAuth2LoginConfigurerTests {
150157
@Autowired
151158
private FilterChainProxy springSecurityFilterChain;
152159

153-
@Autowired
160+
@Autowired(required = false)
154161
private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;
155162

156-
@Autowired
163+
@Autowired(required = false)
157164
SecurityContextRepository securityContextRepository;
158165

159166
public final SpringTestContext spring = new SpringTestContext(this);
@@ -642,6 +649,26 @@ public void logoutWhenUsingOidcLogoutHandlerThenRedirects() throws Exception {
642649
.andExpect(redirectedUrl("https://logout?id_token_hint=id-token"));
643650
}
644651

652+
@Test
653+
public void configureWhenOidcSessionStrategyThenUses() {
654+
this.spring.register(OAuth2LoginWithOidcSessionRegistry.class).autowire();
655+
OidcSessionRegistry registry = this.spring.getContext().getBean(OidcSessionRegistry.class);
656+
this.spring.getContext().publishEvent(new HttpSessionDestroyedEvent(this.request.getSession()));
657+
verify(registry).removeSessionInformation(this.request.getSession().getId());
658+
}
659+
660+
// gh-14558
661+
@Test
662+
public void oauth2LoginWhenDefaultsThenNoOidcSessionRegistry() {
663+
this.spring.register(OAuth2LoginConfig.class).autowire();
664+
DelegatingApplicationListener listener = this.spring.getContext().getBean(DelegatingApplicationListener.class);
665+
List<SmartApplicationListener> listeners = (List<SmartApplicationListener>) ReflectionTestUtils
666+
.getField(listener, "listeners");
667+
assertThat(listeners.stream()
668+
.filter((l) -> l.supportsEventType(SessionDestroyedEvent.class))
669+
.collect(Collectors.toList())).isEmpty();
670+
}
671+
645672
private void loadConfig(Class<?>... configs) {
646673
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
647674
applicationContext.register(configs);
@@ -1117,6 +1144,32 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
11171144

11181145
}
11191146

1147+
@Configuration
1148+
@EnableWebSecurity
1149+
static class OAuth2LoginWithOidcSessionRegistry {
1150+
1151+
private final OidcSessionRegistry registry = mock(OidcSessionRegistry.class);
1152+
1153+
@Bean
1154+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
1155+
// @formatter:off
1156+
http
1157+
.oauth2Login((oauth2) -> oauth2
1158+
.clientRegistrationRepository(
1159+
new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION))
1160+
.oidcSessionRegistry(this.registry)
1161+
);
1162+
// @formatter:on
1163+
return http.build();
1164+
}
1165+
1166+
@Bean
1167+
OidcSessionRegistry oidcSessionRegistry() {
1168+
return this.registry;
1169+
}
1170+
1171+
}
1172+
11201173
@Configuration
11211174
@EnableWebSecurity
11221175
static class OAuth2LoginWithXHREntryPointConfig extends CommonSecurityFilterChainConfig {

config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -34,11 +34,13 @@
3434
import org.springframework.security.authentication.ReactiveAuthenticationManager;
3535
import org.springframework.security.authentication.TestingAuthenticationToken;
3636
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
37+
import org.springframework.security.config.Customizer;
3738
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
3839
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
3940
import org.springframework.security.config.test.SpringTestContext;
4041
import org.springframework.security.config.test.SpringTestContextExtension;
4142
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
43+
import org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2LoginSpec.OidcSessionRegistryAuthenticationWebFilter;
4244
import org.springframework.security.core.Authentication;
4345
import org.springframework.security.core.AuthenticationException;
4446
import org.springframework.security.core.authority.AuthorityUtils;
@@ -54,10 +56,12 @@
5456
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
5557
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
5658
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager;
59+
import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
5760
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
5861
import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler;
5962
import org.springframework.security.oauth2.client.registration.ClientRegistration;
6063
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
64+
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
6165
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
6266
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
6367
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
@@ -576,6 +580,27 @@ public void oauth2LoginWhenAuthenticationConverterFailsThenDefaultRedirectToLogi
576580
// @formatter:on
577581
}
578582

583+
@Test
584+
public void oauth2LoginWhenOidcSessionRegistryThenUses() {
585+
this.spring.register(OAuth2LoginWithOidcSessionRegistry.class).autowire();
586+
SecurityWebFilterChain chain = this.spring.getContext().getBean(SecurityWebFilterChain.class);
587+
assertThat(chain.getWebFilters()
588+
.filter((filter) -> filter instanceof OidcSessionRegistryAuthenticationWebFilter)
589+
.collectList()
590+
.block()).isNotEmpty();
591+
}
592+
593+
// gh-14558
594+
@Test
595+
public void oauth2LoginWhenDefaultsThenNoOidcSessionRegistry() {
596+
this.spring.register(OAuth2LoginWithSingleClientRegistrations.class, OAuth2LoginConfig.class).autowire();
597+
SecurityWebFilterChain chain = this.spring.getContext().getBean(SecurityWebFilterChain.class);
598+
assertThat(chain.getWebFilters()
599+
.filter((filter) -> filter instanceof OidcSessionRegistryAuthenticationWebFilter)
600+
.collectList()
601+
.block()).isEmpty();
602+
}
603+
579604
Mono<SecurityContext> authentication(Authentication authentication) {
580605
SecurityContext context = new SecurityContextImpl();
581606
context.setAuthentication(authentication);
@@ -624,6 +649,21 @@ InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
624649

625650
}
626651

652+
@EnableWebFlux
653+
static class OAuth2LoginConfig {
654+
655+
@Bean
656+
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
657+
// @formatter:off
658+
http
659+
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
660+
.oauth2Login(Customizer.withDefaults());
661+
// @formatter:on
662+
return http.build();
663+
}
664+
665+
}
666+
627667
@EnableWebFlux
628668
static class OAuth2AuthorizeWithMockObjectsConfig {
629669

@@ -892,6 +932,35 @@ ClientRegistration clientRegistration() {
892932

893933
}
894934

935+
@Configuration
936+
@EnableWebFluxSecurity
937+
static class OAuth2LoginWithOidcSessionRegistry {
938+
939+
private final ReactiveOidcSessionRegistry registry = mock(ReactiveOidcSessionRegistry.class);
940+
941+
private final ReactiveClientRegistrationRepository clients = new InMemoryReactiveClientRegistrationRepository(
942+
TestClientRegistrations.clientRegistration().build());
943+
944+
@Bean
945+
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
946+
// @formatter:off
947+
http
948+
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
949+
.oauth2Login((oauth2) -> oauth2
950+
.clientRegistrationRepository(this.clients)
951+
.oidcSessionRegistry(this.registry)
952+
);
953+
// @formatter:on
954+
return http.build();
955+
}
956+
957+
@Bean
958+
ReactiveOidcSessionRegistry oidcSessionRegistry() {
959+
return this.registry;
960+
}
961+
962+
}
963+
895964
static class GitHubWebFilter implements WebFilter {
896965

897966
@Override

docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,33 @@ open fun filterChain(http: HttpSecurity): SecurityFilterChain {
170170
----
171171
======
172172

173+
Then, you need a way listen to events published by Spring Security to remove old `OidcSessionInformation` entries, like so:
174+
175+
[tabs]
176+
======
177+
Java::
178+
+
179+
[source=java,role="primary"]
180+
----
181+
@Bean
182+
public HttpSessionEventListener sessionEventListener() {
183+
return new HttpSessionEventListener();
184+
}
185+
----
186+
187+
Kotlin::
188+
+
189+
[source=kotlin,role="secondary"]
190+
----
191+
@Bean
192+
open fun sessionEventListener(): HttpSessionEventListener {
193+
return HttpSessionEventListener()
194+
}
195+
----
196+
======
197+
198+
This will make so that if `HttpSession#invalidate` is called, then the session is also removed from memory.
199+
173200
And that's it!
174201

175202
This will stand up the endpoint `+/logout/connect/back-channel/{registrationId}+` which the OIDC Provider can request to invalidate a given session of an end user in your application.

0 commit comments

Comments
 (0)