|
22 | 22 | import java.util.LinkedHashMap;
|
23 | 23 | import java.util.Map;
|
24 | 24 |
|
| 25 | +import jakarta.servlet.http.HttpServletRequest; |
| 26 | +import jakarta.servlet.http.HttpServletResponse; |
| 27 | +import jakarta.servlet.http.HttpSession; |
| 28 | +import org.apache.commons.logging.Log; |
| 29 | +import org.apache.commons.logging.LogFactory; |
| 30 | + |
25 | 31 | import org.springframework.beans.factory.BeanFactoryUtils;
|
26 | 32 | import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
27 | 33 | import org.springframework.context.ApplicationContext;
|
| 34 | +import org.springframework.context.ApplicationListener; |
| 35 | +import org.springframework.context.event.GenericApplicationListenerAdapter; |
| 36 | +import org.springframework.context.event.SmartApplicationListener; |
28 | 37 | import org.springframework.core.ResolvableType;
|
29 | 38 | import org.springframework.security.authentication.AuthenticationProvider;
|
30 | 39 | import org.springframework.security.config.Customizer;
|
31 | 40 | import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
32 | 41 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
33 | 42 | import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
|
34 | 43 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
| 44 | +import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer; |
| 45 | +import org.springframework.security.context.DelegatingApplicationListener; |
35 | 46 | import org.springframework.security.core.Authentication;
|
36 | 47 | import org.springframework.security.core.AuthenticationException;
|
37 | 48 | import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
| 49 | +import org.springframework.security.core.session.AbstractSessionEvent; |
| 50 | +import org.springframework.security.core.session.SessionDestroyedEvent; |
| 51 | +import org.springframework.security.core.session.SessionIdChangedEvent; |
38 | 52 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
|
39 | 53 | import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider;
|
40 | 54 | import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
41 | 55 | import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
|
42 | 56 | import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
43 | 57 | import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
44 | 58 | import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider;
|
| 59 | +import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry; |
| 60 | +import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation; |
| 61 | +import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; |
45 | 62 | import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
46 | 63 | import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
|
47 | 64 | import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
|
67 | 84 | import org.springframework.security.web.RedirectStrategy;
|
68 | 85 | import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
|
69 | 86 | import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
| 87 | +import org.springframework.security.web.authentication.session.SessionAuthenticationException; |
| 88 | +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; |
70 | 89 | import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
| 90 | +import org.springframework.security.web.csrf.CsrfToken; |
71 | 91 | import org.springframework.security.web.savedrequest.RequestCache;
|
72 | 92 | import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
73 | 93 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
|
124 | 144 | * <li>{@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not
|
125 | 145 | * configured and {@code DefaultLoginPageGeneratingFilter} is available, then a default
|
126 | 146 | * login page will be made available</li>
|
| 147 | + * <li>{@link OidcSessionRegistry}</li> |
127 | 148 | * </ul>
|
128 | 149 | *
|
129 | 150 | * @author Joe Grandja
|
@@ -202,6 +223,18 @@ public OAuth2LoginConfigurer<B> loginProcessingUrl(String loginProcessingUrl) {
|
202 | 223 | return this;
|
203 | 224 | }
|
204 | 225 |
|
| 226 | + /** |
| 227 | + * Sets the registry for managing the OIDC client-provider session link |
| 228 | + * @param oidcSessionRegistry the {@link OidcSessionRegistry} to use |
| 229 | + * @return the {@link OAuth2LoginConfigurer} for further configuration |
| 230 | + * @since 6.2 |
| 231 | + */ |
| 232 | + public OAuth2LoginConfigurer<B> oidcSessionRegistry(OidcSessionRegistry oidcSessionRegistry) { |
| 233 | + Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null"); |
| 234 | + getBuilder().setSharedObject(OidcSessionRegistry.class, oidcSessionRegistry); |
| 235 | + return this; |
| 236 | + } |
| 237 | + |
205 | 238 | /**
|
206 | 239 | * Returns the {@link AuthorizationEndpointConfig} for configuring the Authorization
|
207 | 240 | * Server's Authorization Endpoint.
|
@@ -397,6 +430,7 @@ public void configure(B http) throws Exception {
|
397 | 430 | authenticationFilter
|
398 | 431 | .setAuthorizationRequestRepository(this.authorizationEndpointConfig.authorizationRequestRepository);
|
399 | 432 | }
|
| 433 | + configureOidcSessionRegistry(http); |
400 | 434 | super.configure(http);
|
401 | 435 | }
|
402 | 436 |
|
@@ -546,6 +580,29 @@ private RequestMatcher getFormLoginNotEnabledRequestMatcher(B http) {
|
546 | 580 | return AnyRequestMatcher.INSTANCE;
|
547 | 581 | }
|
548 | 582 |
|
| 583 | + private void configureOidcSessionRegistry(B http) { |
| 584 | + OidcSessionRegistry sessionRegistry = OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http); |
| 585 | + SessionManagementConfigurer<B> sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class); |
| 586 | + if (sessionConfigurer != null) { |
| 587 | + OidcSessionRegistryAuthenticationStrategy sessionAuthenticationStrategy = new OidcSessionRegistryAuthenticationStrategy(); |
| 588 | + sessionAuthenticationStrategy.setSessionRegistry(sessionRegistry); |
| 589 | + sessionConfigurer.addSessionAuthenticationStrategy(sessionAuthenticationStrategy); |
| 590 | + } |
| 591 | + OidcClientSessionEventListener listener = new OidcClientSessionEventListener(); |
| 592 | + listener.setSessionRegistry(sessionRegistry); |
| 593 | + registerDelegateApplicationListener(listener); |
| 594 | + } |
| 595 | + |
| 596 | + private void registerDelegateApplicationListener(ApplicationListener<?> delegate) { |
| 597 | + DelegatingApplicationListener delegating = getBeanOrNull( |
| 598 | + ResolvableType.forType(DelegatingApplicationListener.class)); |
| 599 | + if (delegating == null) { |
| 600 | + return; |
| 601 | + } |
| 602 | + SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate); |
| 603 | + delegating.addListener(smartListener); |
| 604 | + } |
| 605 | + |
549 | 606 | /**
|
550 | 607 | * Configuration options for the Authorization Server's Authorization Endpoint.
|
551 | 608 | */
|
@@ -793,4 +850,83 @@ public boolean supports(Class<?> authentication) {
|
793 | 850 |
|
794 | 851 | }
|
795 | 852 |
|
| 853 | + private static final class OidcClientSessionEventListener implements ApplicationListener<AbstractSessionEvent> { |
| 854 | + |
| 855 | + private final Log logger = LogFactory.getLog(OidcClientSessionEventListener.class); |
| 856 | + |
| 857 | + private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); |
| 858 | + |
| 859 | + /** |
| 860 | + * {@inheritDoc} |
| 861 | + */ |
| 862 | + @Override |
| 863 | + public void onApplicationEvent(AbstractSessionEvent event) { |
| 864 | + if (event instanceof SessionDestroyedEvent destroyed) { |
| 865 | + this.logger.debug("Received SessionDestroyedEvent"); |
| 866 | + this.sessionRegistry.removeSessionInformation(destroyed.getId()); |
| 867 | + return; |
| 868 | + } |
| 869 | + if (event instanceof SessionIdChangedEvent changed) { |
| 870 | + this.logger.debug("Received SessionIdChangedEvent"); |
| 871 | + OidcSessionInformation information = this.sessionRegistry.removeSessionInformation(changed.getOldSessionId()); |
| 872 | + if (information == null) { |
| 873 | + this.logger.debug("Failed to register new session id since old session id was not found in registry"); |
| 874 | + return; |
| 875 | + } |
| 876 | + this.sessionRegistry.saveSessionInformation(information.withSessionId(changed.getNewSessionId())); |
| 877 | + } |
| 878 | + } |
| 879 | + |
| 880 | + /** |
| 881 | + * The registry where OIDC Provider sessions are linked to the Client session. |
| 882 | + * Defaults to in-memory storage. |
| 883 | + * @param sessionRegistry the {@link OidcSessionRegistry} to use |
| 884 | + */ |
| 885 | + void setSessionRegistry(OidcSessionRegistry sessionRegistry) { |
| 886 | + Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); |
| 887 | + this.sessionRegistry = sessionRegistry; |
| 888 | + } |
| 889 | + |
| 890 | + } |
| 891 | + |
| 892 | + private static final class OidcSessionRegistryAuthenticationStrategy implements SessionAuthenticationStrategy { |
| 893 | + |
| 894 | + private final Log logger = LogFactory.getLog(getClass()); |
| 895 | + |
| 896 | + private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); |
| 897 | + |
| 898 | + /** |
| 899 | + * {@inheritDoc} |
| 900 | + */ |
| 901 | + @Override |
| 902 | + public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) throws SessionAuthenticationException { |
| 903 | + HttpSession session = request.getSession(false); |
| 904 | + if (session == null) { |
| 905 | + return; |
| 906 | + } |
| 907 | + if (!(authentication.getPrincipal() instanceof OidcUser user)) { |
| 908 | + return; |
| 909 | + } |
| 910 | + String sessionId = session.getId(); |
| 911 | + CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); |
| 912 | + Map<String, String> headers = (csrfToken != null) ? Map.of(csrfToken.getHeaderName(), csrfToken.getToken()) : Collections.emptyMap(); |
| 913 | + OidcSessionInformation registration = new OidcSessionInformation(sessionId, headers, user); |
| 914 | + if (this.logger.isTraceEnabled()) { |
| 915 | + this.logger.trace(String.format("Linking a provider [%s] session to this client's session", user.getIssuer())); |
| 916 | + } |
| 917 | + this.sessionRegistry.saveSessionInformation(registration); |
| 918 | + } |
| 919 | + |
| 920 | + /** |
| 921 | + * The registration for linking OIDC Provider Session information to the Client's |
| 922 | + * session. Defaults to in-memory storage. |
| 923 | + * @param sessionRegistry the {@link OidcSessionRegistry} to use |
| 924 | + */ |
| 925 | + void setSessionRegistry(OidcSessionRegistry sessionRegistry) { |
| 926 | + Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); |
| 927 | + this.sessionRegistry = sessionRegistry; |
| 928 | + } |
| 929 | + |
| 930 | + } |
| 931 | + |
796 | 932 | }
|
0 commit comments