From 3e140228087edfbdaf85861d1068fe6611331b2c Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 25 Jul 2023 17:44:48 -0600 Subject: [PATCH] Move OidcSessionRegistry Login Configuration to oauth2Login --- .../client/OAuth2ClientConfigurerUtils.java | 11 ++ .../oauth2/client/OAuth2LoginConfigurer.java | 135 ++++++++++++++ .../oauth2/client/OidcLogoutConfigurer.java | 164 +----------------- .../client/OidcLogoutConfigurerTests.java | 3 +- 4 files changed, 150 insertions(+), 163 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java index 0f1dc7ab8f0..7b4df033357 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java @@ -25,6 +25,8 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry; +import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; @@ -112,4 +114,13 @@ private static > OAuth2AuthorizedClientService return (!authorizedClientServiceMap.isEmpty() ? authorizedClientServiceMap.values().iterator().next() : null); } + static > OidcSessionRegistry getOidcSessionRegistry(B builder) { + OidcSessionRegistry sessionRegistry = builder.getSharedObject(OidcSessionRegistry.class); + if (sessionRegistry == null) { + sessionRegistry = new InMemoryOidcSessionRegistry(); + builder.setSharedObject(OidcSessionRegistry.class, sessionRegistry); + } + return sessionRegistry; + } + } 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 b7a2ccc61fa..c50813f03b2 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 @@ -22,9 +22,18 @@ import java.util.LinkedHashMap; import java.util.Map; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.GenericApplicationListenerAdapter; +import org.springframework.context.event.SmartApplicationListener; import org.springframework.core.ResolvableType; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.Customizer; @@ -32,9 +41,14 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer; +import org.springframework.security.context.DelegatingApplicationListener; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.session.AbstractSessionEvent; +import org.springframework.security.core.session.SessionDestroyedEvent; +import org.springframework.security.core.session.SessionIdChangedEvent; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider; import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; @@ -42,6 +56,9 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider; +import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry; +import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation; +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.OidcUserService; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -67,7 +84,10 @@ import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.session.SessionAuthenticationException; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -124,6 +144,7 @@ *
  • {@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not * configured and {@code DefaultLoginPageGeneratingFilter} is available, then a default * login page will be made available
  • + *
  • {@link OidcSessionRegistry}
  • * * * @author Joe Grandja @@ -202,6 +223,17 @@ public OAuth2LoginConfigurer loginProcessingUrl(String loginProcessingUrl) { return this; } + /** + * Sets the registry for managing the OIDC client-provider session link + * @param sessionRegistry the {@link OidcSessionRegistry} to use + * @return the {@link OAuth2LoginConfigurer} for further configuration + */ + public OAuth2LoginConfigurer oidcSessionRegistry(OidcSessionRegistry sessionRegistry) { + Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); + this.getBuilder().setSharedObject(OidcSessionRegistry.class, sessionRegistry); + return this; + } + /** * Returns the {@link AuthorizationEndpointConfig} for configuring the Authorization * Server's Authorization Endpoint. @@ -400,6 +432,7 @@ public void configure(B http) throws Exception { authenticationFilter .setAuthorizationRequestRepository(this.authorizationEndpointConfig.authorizationRequestRepository); } + configureOidcSessionRegistry(http); super.configure(http); } @@ -539,6 +572,29 @@ private RequestMatcher getFormLoginNotEnabledRequestMatcher(B http) { return AnyRequestMatcher.INSTANCE; } + private void configureOidcSessionRegistry(B http) { + OidcSessionRegistry sessionRegistry = OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http); + SessionManagementConfigurer sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class); + if (sessionConfigurer != null) { + OidcSessionRegistryAuthenticationStrategy sessionAuthenticationStrategy = new OidcSessionRegistryAuthenticationStrategy(); + sessionAuthenticationStrategy.setSessionRegistry(sessionRegistry); + sessionConfigurer.addSessionAuthenticationStrategy(sessionAuthenticationStrategy); + } + OidcClientSessionEventListener listener = new OidcClientSessionEventListener(); + listener.setSessionRegistry(sessionRegistry); + registerDelegateApplicationListener(listener); + } + + private void registerDelegateApplicationListener(ApplicationListener delegate) { + DelegatingApplicationListener delegating = getBeanOrNull( + ResolvableType.forType(DelegatingApplicationListener.class)); + if (delegating == null) { + return; + } + SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate); + delegating.addListener(smartListener); + } + /** * Configuration options for the Authorization Server's Authorization Endpoint. */ @@ -786,4 +842,83 @@ public boolean supports(Class authentication) { } + private static final class OidcClientSessionEventListener implements ApplicationListener { + + private final Log logger = LogFactory.getLog(OidcClientSessionEventListener.class); + + private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); + + /** + * {@inheritDoc} + */ + @Override + public void onApplicationEvent(AbstractSessionEvent event) { + if (event instanceof SessionDestroyedEvent destroyed) { + this.logger.debug("Received SessionDestroyedEvent"); + this.sessionRegistry.removeSessionInformation(destroyed.getId()); + return; + } + if (event instanceof SessionIdChangedEvent changed) { + this.logger.debug("Received SessionIdChangedEvent"); + OidcSessionInformation information = this.sessionRegistry.removeSessionInformation(changed.getOldSessionId()); + if (information == null) { + this.logger.debug("Failed to register new session id since old session id was not found in registry"); + return; + } + this.sessionRegistry.saveSessionInformation(information.withSessionId(changed.getNewSessionId())); + } + } + + /** + * The registry where OIDC Provider sessions are linked to the Client session. + * Defaults to in-memory storage. + * @param sessionRegistry the {@link OidcSessionRegistry} to use + */ + void setSessionRegistry(OidcSessionRegistry sessionRegistry) { + Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); + this.sessionRegistry = sessionRegistry; + } + + } + + private static final class OidcSessionRegistryAuthenticationStrategy implements SessionAuthenticationStrategy { + + private final Log logger = LogFactory.getLog(getClass()); + + private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); + + /** + * {@inheritDoc} + */ + @Override + public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) throws SessionAuthenticationException { + HttpSession session = request.getSession(false); + if (session == null) { + return; + } + if (!(authentication.getPrincipal() instanceof OidcUser user)) { + return; + } + String sessionId = session.getId(); + CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + Map headers = (csrfToken != null) ? Map.of(csrfToken.getHeaderName(), csrfToken.getToken()) : Collections.emptyMap(); + OidcSessionInformation registration = new OidcSessionInformation(sessionId, headers, user); + if (this.logger.isTraceEnabled()) { + this.logger.trace(String.format("Linking a provider [%s] session to this client's session", user.getIssuer())); + } + this.sessionRegistry.saveSessionInformation(registration); + } + + /** + * The registration for linking OIDC Provider Session information to the Client's + * session. Defaults to in-memory storage. + * @param sessionRegistry the {@link OidcSessionRegistry} to use + */ + void setSessionRegistry(OidcSessionRegistry sessionRegistry) { + Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); + this.sessionRegistry = sessionRegistry; + } + + } + } 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 39c50265f08..b8f0c486bbd 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 @@ -16,46 +16,20 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.client; -import java.util.Collections; -import java.util.Map; import java.util.function.Consumer; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.GenericApplicationListenerAdapter; -import org.springframework.context.event.SmartApplicationListener; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer; -import org.springframework.security.context.DelegatingApplicationListener; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.session.AbstractSessionEvent; -import org.springframework.security.core.session.SessionDestroyedEvent; -import org.springframework.security.core.session.SessionIdChangedEvent; import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcBackChannelLogoutAuthenticationProvider; -import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; import org.springframework.security.oauth2.client.oidc.web.OidcBackChannelLogoutFilter; import org.springframework.security.oauth2.client.oidc.web.logout.OidcBackChannelLogoutHandler; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.authentication.logout.LogoutHandler; -import org.springframework.security.web.authentication.session.SessionAuthenticationException; -import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.security.web.csrf.CsrfFilter; -import org.springframework.security.web.csrf.CsrfToken; import org.springframework.util.Assert; /** @@ -117,28 +91,6 @@ public void configure(B builder) throws Exception { } } - private void registerDelegateApplicationListener(ApplicationListener delegate) { - DelegatingApplicationListener delegating = getBeanOrNull(DelegatingApplicationListener.class); - if (delegating == null) { - return; - } - SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate); - delegating.addListener(smartListener); - } - - private T getBeanOrNull(Class type) { - ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); - if (context == null) { - return null; - } - try { - return context.getBean(type); - } - catch (NoSuchBeanDefinitionException ex) { - return null; - } - } - /** * A configurer for configuring OIDC Back-Channel Logout */ @@ -147,8 +99,6 @@ public final class BackChannelLogoutConfigurer { private AuthenticationManager authenticationManager = new ProviderManager( new OidcBackChannelLogoutAuthenticationProvider()); - private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); - private LogoutHandler logoutHandler; /** @@ -162,18 +112,6 @@ public BackChannelLogoutConfigurer authenticationManager(AuthenticationManager a return this; } - /** - * Use this {@link OidcSessionRegistry} for managing the client-provider session - * link - * @param sessionRegistry the {@link OidcSessionRegistry} to use - * @return the {@link BackChannelLogoutConfigurer} for further configuration - */ - public BackChannelLogoutConfigurer oidcSessionRegistry(OidcSessionRegistry sessionRegistry) { - Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); - this.sessionRegistry = sessionRegistry; - return this; - } - /** * Use this {@link LogoutHandler} for invalidating each session identified by the * OIDC Back-Channel Logout Token @@ -189,118 +127,22 @@ private AuthenticationManager authenticationManager() { return this.authenticationManager; } - private OidcSessionRegistry oidcSessionRegistry() { - return this.sessionRegistry; - } - - private LogoutHandler logoutHandler() { + private LogoutHandler logoutHandler(B http) { if (this.logoutHandler == null) { OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(); - logoutHandler.setSessionRegistry(this.sessionRegistry); + logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http)); this.logoutHandler = logoutHandler; } return this.logoutHandler; } - private SessionAuthenticationStrategy sessionAuthenticationStrategy() { - OidcSessionRegistryAuthenticationStrategy strategy = new OidcSessionRegistryAuthenticationStrategy(); - strategy.setSessionRegistry(oidcSessionRegistry()); - return strategy; - } - void configure(B http) { ClientRegistrationRepository clientRegistrationRepository = OAuth2ClientConfigurerUtils .getClientRegistrationRepository(http); OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(clientRegistrationRepository, authenticationManager()); - filter.setLogoutHandler(logoutHandler()); + filter.setLogoutHandler(logoutHandler(http)); http.addFilterBefore(filter, CsrfFilter.class); - SessionManagementConfigurer sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class); - if (sessionConfigurer != null) { - sessionConfigurer.addSessionAuthenticationStrategy(sessionAuthenticationStrategy()); - } - OidcClientSessionEventListener listener = new OidcClientSessionEventListener(); - listener.setSessionRegistry(this.sessionRegistry); - registerDelegateApplicationListener(listener); - } - - static final class OidcClientSessionEventListener implements ApplicationListener { - - private final Log logger = LogFactory.getLog(OidcClientSessionEventListener.class); - - private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); - - /** - * {@inheritDoc} - */ - @Override - public void onApplicationEvent(AbstractSessionEvent event) { - if (event instanceof SessionDestroyedEvent destroyed) { - this.logger.debug("Received SessionDestroyedEvent"); - this.sessionRegistry.removeSessionInformation(destroyed.getId()); - return; - } - if (event instanceof SessionIdChangedEvent changed) { - this.logger.debug("Received SessionIdChangedEvent"); - OidcSessionInformation information = this.sessionRegistry.removeSessionInformation(changed.getOldSessionId()); - if (information == null) { - this.logger.debug("Failed to register new session id since old session id was not found in registry"); - return; - } - this.sessionRegistry.saveSessionInformation(information.withSessionId(changed.getNewSessionId())); - } - } - - /** - * The registry where OIDC Provider sessions are linked to the Client session. - * Defaults to in-memory storage. - * @param sessionRegistry the {@link OidcSessionRegistry} to use - */ - void setSessionRegistry(OidcSessionRegistry sessionRegistry) { - Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); - this.sessionRegistry = sessionRegistry; - } - - } - - static final class OidcSessionRegistryAuthenticationStrategy implements SessionAuthenticationStrategy { - - private final Log logger = LogFactory.getLog(getClass()); - - private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); - - /** - * {@inheritDoc} - */ - @Override - public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) throws SessionAuthenticationException { - HttpSession session = request.getSession(false); - if (session == null) { - return; - } - if (!(authentication.getPrincipal() instanceof OidcUser user)) { - return; - } - String sessionId = session.getId(); - CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); - Map headers = (csrfToken != null) ? Map.of(csrfToken.getHeaderName(), csrfToken.getToken()) : Collections.emptyMap(); - OidcSessionInformation registration = new OidcSessionInformation(sessionId, headers, user); - if (this.logger.isTraceEnabled()) { - this.logger.trace(String.format("Linking a provider [%s] session to this client's session", user.getIssuer())); - } - this.sessionRegistry.saveSessionInformation(registration); - } - - /** - * The registration for linking OIDC Provider Session information to the - * Client's session. Defaults to in-memory storage. - * @param sessionRegistry the {@link OidcSessionRegistry} to use - */ - void setSessionRegistry(OidcSessionRegistry sessionRegistry) { - Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); - this.sessionRegistry = sessionRegistry; - } - } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java index 4b0bcfc08ab..e7d062190a0 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java @@ -246,10 +246,9 @@ SecurityFilterChain filters(HttpSecurity http) throws Exception { // @formatter:off http .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) - .oauth2Login(Customizer.withDefaults()) + .oauth2Login((oauth2) -> oauth2.oidcSessionRegistry(this.sessionRegistry)) .oidcLogout((oidc) -> oidc.backChannel((logout) -> logout .authenticationManager(this.authenticationManager) - .oidcSessionRegistry(this.sessionRegistry) .logoutHandler(this.logoutHandler) )); // @formatter:on