Do Not Wire Default OidcSessionStrategy without OidcLogoutConfigurer

Closes gh-14558
This commit is contained in:
Josh Cummings 2024-01-29 17:20:56 -07:00
parent eea4279fae
commit 3ab323663a
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5
6 changed files with 172 additions and 13 deletions

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -582,6 +582,10 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
} }
private void configureOidcSessionRegistry(B http) { private void configureOidcSessionRegistry(B http) {
if (http.getConfigurer(OidcLogoutConfigurer.class) == null
&& http.getSharedObject(OidcSessionRegistry.class) == null) {
return;
}
OidcSessionRegistry sessionRegistry = OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http); OidcSessionRegistry sessionRegistry = OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http);
SessionManagementConfigurer<B> sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class); SessionManagementConfigurer<B> sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class);
if (sessionConfigurer != null) { if (sessionConfigurer != null) {

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -3974,8 +3974,10 @@ public class ServerHttpSecurity {
ReactiveAuthenticationManager manager = getAuthenticationManager(); ReactiveAuthenticationManager manager = getAuthenticationManager();
ReactiveOidcSessionRegistry sessionRegistry = getOidcSessionRegistry(); ReactiveOidcSessionRegistry sessionRegistry = getOidcSessionRegistry();
AuthenticationWebFilter authenticationFilter = new OidcSessionRegistryAuthenticationWebFilter(manager, AuthenticationWebFilter authenticationFilter = (sessionRegistry != null)
authorizedClientRepository, sessionRegistry); ? new OidcSessionRegistryAuthenticationWebFilter(manager, authorizedClientRepository,
sessionRegistry)
: new OAuth2LoginAuthenticationWebFilter(manager, authorizedClientRepository);
authenticationFilter.setRequiresAuthenticationMatcher(getAuthenticationMatcher()); authenticationFilter.setRequiresAuthenticationMatcher(getAuthenticationMatcher());
authenticationFilter authenticationFilter
.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository)); .setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository));
@ -3984,8 +3986,10 @@ public class ServerHttpSecurity {
authenticationFilter.setSecurityContextRepository(this.securityContextRepository); authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
setDefaultEntryPoints(http); setDefaultEntryPoints(http);
http.addFilterAfter(new OidcSessionRegistryWebFilter(sessionRegistry), if (sessionRegistry != null) {
SecurityWebFiltersOrder.HTTP_HEADERS_WRITER); http.addFilterAfter(new OidcSessionRegistryWebFilter(sessionRegistry),
SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
}
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC); http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION); http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
} }
@ -4031,6 +4035,9 @@ public class ServerHttpSecurity {
} }
private ReactiveOidcSessionRegistry getOidcSessionRegistry() { private ReactiveOidcSessionRegistry getOidcSessionRegistry() {
if (ServerHttpSecurity.this.oidcLogout == null && this.oidcSessionRegistry == null) {
return null;
}
if (this.oidcSessionRegistry == null) { if (this.oidcSessionRegistry == null) {
this.oidcSessionRegistry = getBeanOrNull(ReactiveOidcSessionRegistry.class); this.oidcSessionRegistry = getBeanOrNull(ReactiveOidcSessionRegistry.class);
} }
@ -4269,8 +4276,7 @@ public class ServerHttpSecurity {
} }
private static final class OidcSessionRegistryAuthenticationWebFilter static final class OidcSessionRegistryAuthenticationWebFilter extends OAuth2LoginAuthenticationWebFilter {
extends OAuth2LoginAuthenticationWebFilter {
private final Log logger = LogFactory.getLog(getClass()); private final Log logger = LogFactory.getLog(getClass());

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import org.apache.http.HttpHeaders; import org.apache.http.HttpHeaders;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
@ -36,6 +37,7 @@ import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.mock.web.MockFilterChain; 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.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.context.DelegatingApplicationListener;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils; 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.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.context.SecurityContextChangedListener; import org.springframework.security.core.context.SecurityContextChangedListener;
import org.springframework.security.core.context.SecurityContextHolderStrategy; 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.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; 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.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration; 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.HttpRequestResponseHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository; 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.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
@ -150,10 +157,10 @@ public class OAuth2LoginConfigurerTests {
@Autowired @Autowired
private FilterChainProxy springSecurityFilterChain; private FilterChainProxy springSecurityFilterChain;
@Autowired @Autowired(required = false)
private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository; private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;
@Autowired @Autowired(required = false)
SecurityContextRepository securityContextRepository; SecurityContextRepository securityContextRepository;
public final SpringTestContext spring = new SpringTestContext(this); public final SpringTestContext spring = new SpringTestContext(this);
@ -642,6 +649,26 @@ public class OAuth2LoginConfigurerTests {
.andExpect(redirectedUrl("https://logout?id_token_hint=id-token")); .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<SmartApplicationListener> listeners = (List<SmartApplicationListener>) ReflectionTestUtils
.getField(listener, "listeners");
assertThat(listeners.stream()
.filter((l) -> l.supportsEventType(SessionDestroyedEvent.class))
.collect(Collectors.toList())).isEmpty();
}
private void loadConfig(Class<?>... configs) { private void loadConfig(Class<?>... configs) {
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext(); AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(configs); 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 @Configuration
@EnableWebSecurity @EnableWebSecurity
static class OAuth2LoginWithXHREntryPointConfig extends CommonSecurityFilterChainConfig { static class OAuth2LoginWithXHREntryPointConfig extends CommonSecurityFilterChainConfig {

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.ReactiveAuthenticationManager;
import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; 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.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration; 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.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils; 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.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager; 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.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler; 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.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; 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.registration.TestClientRegistrations;
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
@ -576,6 +580,27 @@ public class OAuth2LoginTests {
// @formatter:on // @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<SecurityContext> authentication(Authentication authentication) { Mono<SecurityContext> authentication(Authentication authentication) {
SecurityContext context = new SecurityContextImpl(); SecurityContext context = new SecurityContextImpl();
context.setAuthentication(authentication); 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 @EnableWebFlux
static class OAuth2AuthorizeWithMockObjectsConfig { 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 { static class GitHubWebFilter implements WebFilter {
@Override @Override

View File

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