diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurity.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurity.java index a80ed7c334e..40da61f57f0 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurity.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurity.java @@ -43,6 +43,8 @@ import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.SecurityFilterChain; @@ -53,7 +55,7 @@ import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.logout.LogoutHandler; -import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.csrf.CsrfException; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -499,6 +501,10 @@ protected void setLoginView(HttpSecurity http, /** * Sets up the login page URI of the OAuth2 provider on the specified * HttpSecurity instance. + *
+ *
+ * This method also configures a logout success handler that redirects to + * the application base URL after logout. * * @param http * the http security from {@link #filterChain(HttpSecurity)} @@ -511,10 +517,85 @@ protected void setLoginView(HttpSecurity http, */ protected void setOAuth2LoginPage(HttpSecurity http, String oauth2LoginPage) throws Exception { + setOAuth2LoginPage(http, oauth2LoginPage, "{baseUrl}"); + } + + /** + * Sets up the login page URI of the OAuth2 provider and the post logout URI + * on the specified HttpSecurity instance. + *+ *
+ * The post logout redirect uri can be relative or absolute URI or a + * template. The supported uri template variables are: {baseScheme}, + * {baseHost}, {basePort} and {basePath}. + *+ *
+ * NOTE: "{baseUrl}" is also supported, which is the same as + * "{baseScheme}://{baseHost}{basePort}{basePath}" handler. + * setPostLogoutRedirectUri("{baseUrl}"); + * + * @param http + * the http security from {@link #filterChain(HttpSecurity)} + * @param oauth2LoginPage + * the login page of the OAuth2 provider. This Specifies the URL + * to send users to if login is required. + * @param postLogoutRedirectUri + * the post logout redirect uri. Can be a template. + * @throws Exception + * Re-throws the possible exceptions while activating + * OAuth2LoginConfigurer + */ + protected void setOAuth2LoginPage(HttpSecurity http, String oauth2LoginPage, + String postLogoutRedirectUri) throws Exception { http.oauth2Login(cfg -> cfg.loginPage(oauth2LoginPage).successHandler( getVaadinSavedRequestAwareAuthenticationSuccessHandler(http)) .permitAll()); accessControl.setLoginView(servletContextPath + oauth2LoginPage); + if (postLogoutRedirectUri != null) { + applicationContext + .getBeanProvider(ClientRegistrationRepository.class) + .getIfAvailable(); + var logoutSuccessHandler = oidcLogoutSuccessHandler( + postLogoutRedirectUri); + if (logoutSuccessHandler != null) { + http.logout( + cfg -> cfg.logoutSuccessHandler(logoutSuccessHandler)); + } + } + } + + /** + * Gets a {@code OidcClientInitiatedLogoutSuccessHandler} instance that + * redirects to the given URL after logout. + *+ *
+ * If a {@code ClientRegistrationRepository} bean is not registered in the + * application context, the method returns {@literal null}. + * + * @param postLogoutRedirectUri + * the post logout redirect uri + * @return a {@code OidcClientInitiatedLogoutSuccessHandler}, or + * {@literal null} if a {@code ClientRegistrationRepository} bean is + * not registered in the application context. + */ + // Using base interface as return type to avoid potential + // ClassNotFoundException when Spring Boot introspect configuration class + // during startup, if spring-security-oauth2-client is not on classpath + protected LogoutSuccessHandler oidcLogoutSuccessHandler( + String postLogoutRedirectUri) { + var clientRegistrationRepository = applicationContext + .getBeanProvider(ClientRegistrationRepository.class) + .getIfAvailable(); + if (clientRegistrationRepository != null) { + var logoutHandler = new OidcClientInitiatedLogoutSuccessHandler( + clientRegistrationRepository); + logoutHandler.setRedirectStrategy(new UidlRedirectStrategy()); + logoutHandler.setPostLogoutRedirectUri(postLogoutRedirectUri); + return logoutHandler; + } + LoggerFactory.getLogger(VaadinWebSecurity.class).warn( + "Cannot create OidcClientInitiatedLogoutSuccessHandler because ClientRegistrationRepository bean is not available."); + return null; } /** diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinWebSecurityTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinWebSecurityTest.java index 15b19397897..4ed6759348f 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinWebSecurityTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinWebSecurityTest.java @@ -20,12 +20,15 @@ import jakarta.servlet.http.HttpServletResponse; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.security.config.ObjectPostProcessor; @@ -33,16 +36,21 @@ import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; import org.springframework.security.core.Authentication; -import com.vaadin.flow.spring.security.AuthenticationContext.CompositeLogoutHandler; +import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.util.ReflectionTestUtils; import com.vaadin.flow.server.auth.NavigationAccessControl; +import com.vaadin.flow.spring.security.AuthenticationContext.CompositeLogoutHandler; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @RunWith(SpringRunner.class) @@ -83,12 +91,7 @@ public void navigationAccessControl_enabledByDefault() throws Exception { Map.of(ApplicationContext.class, appCtx)); VaadinWebSecurity testConfig = new VaadinWebSecurity() { }; - NavigationAccessControl accessControl = new NavigationAccessControl(); - ReflectionTestUtils.setField(testConfig, "accessControl", - accessControl); - RequestUtil requestUtil = mock(RequestUtil.class); - Mockito.when(requestUtil.getUrlMapping()).thenReturn("/*"); - ReflectionTestUtils.setField(testConfig, "requestUtil", requestUtil); + mockVaadinWebSecurityInjection(testConfig); testConfig.filterChain(httpSecurity); Assert.assertTrue( @@ -108,12 +111,7 @@ protected boolean enableNavigationAccessControl() { return false; } }; - NavigationAccessControl accessControl = new NavigationAccessControl(); - ReflectionTestUtils.setField(testConfig, "accessControl", - accessControl); - RequestUtil requestUtil = mock(RequestUtil.class); - Mockito.when(requestUtil.getUrlMapping()).thenReturn("/*"); - ReflectionTestUtils.setField(testConfig, "requestUtil", requestUtil); + mockVaadinWebSecurityInjection(testConfig); testConfig.filterChain(httpSecurity); Assert.assertFalse( @@ -121,6 +119,93 @@ protected boolean enableNavigationAccessControl() { testConfig.getNavigationAccessControl().isEnabled()); } + @Test + public void filterChain_oauth2login_configuresLoginPageAndLogoutHandler() + throws Exception { + assertOauth2Configuration(null); + assertOauth2Configuration("/session-ended"); + } + + private void assertOauth2Configuration(String postLogoutUri) + throws Exception { + String expectedLogoutUri = postLogoutUri != null ? postLogoutUri + : "{baseUrl}"; + HttpSecurity httpSecurity = new HttpSecurity(postProcessor, + new AuthenticationManagerBuilder(postProcessor), + Map.of(ApplicationContext.class, appCtx)); + AtomicReference