diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index 8f479814a9..a6b5f7c52b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -582,6 +582,10 @@ public final class OAuth2LoginConfigurer> } private void configureOidcSessionRegistry(B http) { + if (http.getConfigurer(OidcLogoutConfigurer.class) == null + && http.getSharedObject(OidcSessionRegistry.class) == null) { + return; + } OidcSessionRegistry sessionRegistry = OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http); SessionManagementConfigurer sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class); if (sessionConfigurer != null) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java index b22e7e5a25..993ba84155 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 3df5775afc..76be5b2555 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -3974,8 +3974,10 @@ public class ServerHttpSecurity { ReactiveAuthenticationManager manager = getAuthenticationManager(); ReactiveOidcSessionRegistry sessionRegistry = getOidcSessionRegistry(); - AuthenticationWebFilter authenticationFilter = new OidcSessionRegistryAuthenticationWebFilter(manager, - authorizedClientRepository, sessionRegistry); + AuthenticationWebFilter authenticationFilter = (sessionRegistry != null) + ? new OidcSessionRegistryAuthenticationWebFilter(manager, authorizedClientRepository, + sessionRegistry) + : new OAuth2LoginAuthenticationWebFilter(manager, authorizedClientRepository); authenticationFilter.setRequiresAuthenticationMatcher(getAuthenticationMatcher()); authenticationFilter .setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository)); @@ -3984,8 +3986,10 @@ public class ServerHttpSecurity { authenticationFilter.setSecurityContextRepository(this.securityContextRepository); setDefaultEntryPoints(http); - http.addFilterAfter(new OidcSessionRegistryWebFilter(sessionRegistry), - SecurityWebFiltersOrder.HTTP_HEADERS_WRITER); + if (sessionRegistry != null) { + http.addFilterAfter(new OidcSessionRegistryWebFilter(sessionRegistry), + SecurityWebFiltersOrder.HTTP_HEADERS_WRITER); + } http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC); http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION); } @@ -4031,6 +4035,9 @@ public class ServerHttpSecurity { } private ReactiveOidcSessionRegistry getOidcSessionRegistry() { + if (ServerHttpSecurity.this.oidcLogout == null && this.oidcSessionRegistry == null) { + return null; + } if (this.oidcSessionRegistry == null) { this.oidcSessionRegistry = getBeanOrNull(ReactiveOidcSessionRegistry.class); } @@ -4269,8 +4276,7 @@ public class ServerHttpSecurity { } - private static final class OidcSessionRegistryAuthenticationWebFilter - extends OAuth2LoginAuthenticationWebFilter { + static final class OidcSessionRegistryAuthenticationWebFilter extends OAuth2LoginAuthenticationWebFilter { private final Log logger = LogFactory.getLog(getClass()); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java index dab28dd0f3..d85637961d 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.apache.http.HttpHeaders; import org.junit.jupiter.api.AfterEach; @@ -36,6 +37,7 @@ import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.SmartApplicationListener; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.mock.web.MockFilterChain; @@ -48,6 +50,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.context.DelegatingApplicationListener; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; @@ -55,9 +58,11 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.context.SecurityContextChangedListener; import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.core.session.SessionDestroyedEvent; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -95,7 +100,9 @@ import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.context.HttpRequestResponseHolder; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.session.HttpSessionDestroyedEvent; import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -150,10 +157,10 @@ public class OAuth2LoginConfigurerTests { @Autowired private FilterChainProxy springSecurityFilterChain; - @Autowired + @Autowired(required = false) private AuthorizationRequestRepository authorizationRequestRepository; - @Autowired + @Autowired(required = false) SecurityContextRepository securityContextRepository; public final SpringTestContext spring = new SpringTestContext(this); @@ -642,6 +649,26 @@ public class OAuth2LoginConfigurerTests { .andExpect(redirectedUrl("https://logout?id_token_hint=id-token")); } + @Test + public void configureWhenOidcSessionStrategyThenUses() { + this.spring.register(OAuth2LoginWithOidcSessionRegistry.class).autowire(); + OidcSessionRegistry registry = this.spring.getContext().getBean(OidcSessionRegistry.class); + this.spring.getContext().publishEvent(new HttpSessionDestroyedEvent(this.request.getSession())); + verify(registry).removeSessionInformation(this.request.getSession().getId()); + } + + // gh-14558 + @Test + public void oauth2LoginWhenDefaultsThenNoOidcSessionRegistry() { + this.spring.register(OAuth2LoginConfig.class).autowire(); + DelegatingApplicationListener listener = this.spring.getContext().getBean(DelegatingApplicationListener.class); + List listeners = (List) ReflectionTestUtils + .getField(listener, "listeners"); + assertThat(listeners.stream() + .filter((l) -> l.supportsEventType(SessionDestroyedEvent.class)) + .collect(Collectors.toList())).isEmpty(); + } + private void loadConfig(Class... configs) { AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext(); applicationContext.register(configs); @@ -1117,6 +1144,32 @@ public class OAuth2LoginConfigurerTests { } + @Configuration + @EnableWebSecurity + static class OAuth2LoginWithOidcSessionRegistry { + + private final OidcSessionRegistry registry = mock(OidcSessionRegistry.class); + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .oauth2Login((oauth2) -> oauth2 + .clientRegistrationRepository( + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)) + .oidcSessionRegistry(this.registry) + ); + // @formatter:on + return http.build(); + } + + @Bean + OidcSessionRegistry oidcSessionRegistry() { + return this.registry; + } + + } + @Configuration @EnableWebSecurity static class OAuth2LoginWithXHREntryPointConfig extends CommonSecurityFilterChainConfig { diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java index 8d779cdfec..ec686bf851 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,11 +34,13 @@ import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration; +import org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2LoginSpec.OidcSessionRegistryAuthenticationWebFilter; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.AuthorityUtils; @@ -54,10 +56,12 @@ import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuth import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager; +import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository; @@ -576,6 +580,27 @@ public class OAuth2LoginTests { // @formatter:on } + @Test + public void oauth2LoginWhenOidcSessionRegistryThenUses() { + this.spring.register(OAuth2LoginWithOidcSessionRegistry.class).autowire(); + SecurityWebFilterChain chain = this.spring.getContext().getBean(SecurityWebFilterChain.class); + assertThat(chain.getWebFilters() + .filter((filter) -> filter instanceof OidcSessionRegistryAuthenticationWebFilter) + .collectList() + .block()).isNotEmpty(); + } + + // gh-14558 + @Test + public void oauth2LoginWhenDefaultsThenNoOidcSessionRegistry() { + this.spring.register(OAuth2LoginWithSingleClientRegistrations.class, OAuth2LoginConfig.class).autowire(); + SecurityWebFilterChain chain = this.spring.getContext().getBean(SecurityWebFilterChain.class); + assertThat(chain.getWebFilters() + .filter((filter) -> filter instanceof OidcSessionRegistryAuthenticationWebFilter) + .collectList() + .block()).isEmpty(); + } + Mono authentication(Authentication authentication) { SecurityContext context = new SecurityContextImpl(); context.setAuthentication(authentication); @@ -624,6 +649,21 @@ public class OAuth2LoginTests { } + @EnableWebFlux + static class OAuth2LoginConfig { + + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { + // @formatter:off + http + .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) + .oauth2Login(Customizer.withDefaults()); + // @formatter:on + return http.build(); + } + + } + @EnableWebFlux static class OAuth2AuthorizeWithMockObjectsConfig { @@ -892,6 +932,35 @@ public class OAuth2LoginTests { } + @Configuration + @EnableWebFluxSecurity + static class OAuth2LoginWithOidcSessionRegistry { + + private final ReactiveOidcSessionRegistry registry = mock(ReactiveOidcSessionRegistry.class); + + private final ReactiveClientRegistrationRepository clients = new InMemoryReactiveClientRegistrationRepository( + TestClientRegistrations.clientRegistration().build()); + + @Bean + SecurityWebFilterChain filterChain(ServerHttpSecurity http) { + // @formatter:off + http + .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) + .oauth2Login((oauth2) -> oauth2 + .clientRegistrationRepository(this.clients) + .oidcSessionRegistry(this.registry) + ); + // @formatter:on + return http.build(); + } + + @Bean + ReactiveOidcSessionRegistry oidcSessionRegistry() { + return this.registry; + } + + } + static class GitHubWebFilter implements WebFilter { @Override diff --git a/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc index 24078cf61f..c8bde99d5e 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc @@ -170,6 +170,33 @@ open fun filterChain(http: HttpSecurity): SecurityFilterChain { ---- ====== +Then, you need a way listen to events published by Spring Security to remove old `OidcSessionInformation` entries, like so: + +[tabs] +====== +Java:: ++ +[source=java,role="primary"] +---- +@Bean +public HttpSessionEventListener sessionEventListener() { + return new HttpSessionEventListener(); +} +---- + +Kotlin:: ++ +[source=kotlin,role="secondary"] +---- +@Bean +open fun sessionEventListener(): HttpSessionEventListener { + return HttpSessionEventListener() +} +---- +====== + +This will make so that if `HttpSession#invalidate` is called, then the session is also removed from memory. + And that's it! 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.