diff --git a/config/src/main/java/org/springframework/security/config/web/server/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/HttpSecurity.java index 4c39dcbfc6..22a26caf54 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/HttpSecurity.java @@ -15,64 +15,45 @@ */ package org.springframework.security.config.web.server; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.http.MediaType; -import org.springframework.security.web.server.DelegatingAuthenticationEntryPoint; -import org.springframework.security.web.server.authentication.AuthenticationFailureHandler; -import org.springframework.security.web.server.authentication.logout.LogoutWebFiter; -import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilterChain; -import reactor.core.publisher.Mono; - import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authorization.AuthenticatedAuthorizationManager; import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.ReactiveAuthorizationManager; -import org.springframework.security.web.server.AuthenticationEntryPoint; -import org.springframework.security.web.server.FormLoginAuthenticationConverter; -import org.springframework.security.web.server.HttpBasicAuthenticationConverter; -import org.springframework.security.web.server.MatcherSecurityWebFilterChain; -import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.security.web.server.authentication.AuthenticationEntryPointFailureHandler; -import org.springframework.security.web.server.authentication.AuthenticationWebFilter; -import org.springframework.security.web.server.authentication.RedirectAuthenticationEntryPoint; -import org.springframework.security.web.server.authentication.RedirectAuthenticationSuccessHandler; +import org.springframework.security.web.server.*; +import org.springframework.security.web.server.authentication.*; +import org.springframework.security.web.server.authentication.logout.LogoutHandler; +import org.springframework.security.web.server.authentication.logout.LogoutWebFilter; +import org.springframework.security.web.server.authentication.logout.SecurityContextRepositoryLogoutHandler; import org.springframework.security.web.server.authentication.www.HttpBasicAuthenticationEntryPoint; import org.springframework.security.web.server.authorization.AuthorizationContext; import org.springframework.security.web.server.authorization.AuthorizationWebFilter; import org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager; import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter; -import org.springframework.security.web.server.context.AuthenticationReactorContextFilter; -import org.springframework.security.web.server.context.SecurityContextRepository; -import org.springframework.security.web.server.context.SecurityContextRepositoryWebFilter; -import org.springframework.security.web.server.context.ServerWebExchangeAttributeSecurityContextRepository; -import org.springframework.security.web.server.context.WebSessionSecurityContextRepository; -import org.springframework.security.web.server.header.CacheControlHttpHeadersWriter; -import org.springframework.security.web.server.header.CompositeHttpHeadersWriter; -import org.springframework.security.web.server.header.ContentTypeOptionsHttpHeadersWriter; -import org.springframework.security.web.server.header.HttpHeaderWriterWebFilter; -import org.springframework.security.web.server.header.HttpHeadersWriter; -import org.springframework.security.web.server.header.StrictTransportSecurityHttpHeadersWriter; -import org.springframework.security.web.server.header.XFrameOptionsHttpHeadersWriter; -import org.springframework.security.web.server.header.XXssProtectionHttpHeadersWriter; +import org.springframework.security.web.server.context.*; +import org.springframework.security.web.server.header.*; import org.springframework.security.web.server.ui.LoginPageGeneratingWebFilter; +import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; -import static org.springframework.security.web.server.DelegatingAuthenticationEntryPoint.*; +import static org.springframework.security.web.server.DelegatingAuthenticationEntryPoint.DelegateEntry; /** * @author Rob Winch @@ -89,6 +70,8 @@ public class HttpSecurity { private FormLoginBuilder formLogin; + private LogoutBuilder logout; + private ReactiveAuthenticationManager authenticationManager; private SecurityContextRepository securityContextRepository; @@ -158,6 +141,13 @@ public class HttpSecurity { return this.authorizeExchangeBuilder; } + public LogoutBuilder logout() { + if (this.logout == null) { + this.logout = new LogoutBuilder(); + } + return this.logout; + } + public HttpSecurity authenticationManager(ReactiveAuthenticationManager manager) { this.authenticationManager = manager; return this; @@ -187,7 +177,10 @@ public class HttpSecurity { this.webFilters.add(new OrderedWebFilter(new LoginPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING.getOrder())); } this.formLogin.configure(this); - this.addFilterAt(new LogoutWebFiter(), SecurityWebFiltersOrder.LOGOUT); + this.addFilterAt(new LogoutWebFilter(), SecurityWebFiltersOrder.LOGOUT); + } + if(this.logout != null) { + this.logout.configure(this); } this.addFilterAt(new AuthenticationReactorContextFilter(), SecurityWebFiltersOrder.AUTHENTICATION_CONTEXT); if(this.authorizeExchangeBuilder != null) { @@ -536,6 +529,48 @@ public class HttpSecurity { } } + /** + * @author Shazin Sadakath + * @since 5.0 + */ + public final class LogoutBuilder { + + private LogoutHandler logoutHandler = new SecurityContextRepositoryLogoutHandler(); + private String logoutUrl = "/logout"; + private ServerWebExchangeMatcher requiresLogout = ServerWebExchangeMatchers + .pathMatchers(logoutUrl); + + public LogoutBuilder logoutHandler(LogoutHandler logoutHandler) { + Assert.notNull(logoutHandler, "logoutHandler must not be null"); + this.logoutHandler = logoutHandler; + return this; + } + + public LogoutBuilder logoutUrl(String logoutUrl) { + Assert.notNull(logoutHandler, "logoutUrl must not be null"); + this.logoutUrl = logoutUrl; + this.requiresLogout = ServerWebExchangeMatchers.pathMatchers(logoutUrl); + return this; + } + + public HttpSecurity and() { + return HttpSecurity.this; + } + + public void configure(HttpSecurity http) { + LogoutWebFilter logoutWebFilter = createLogoutWebFilter(http); + http.addFilterAt(logoutWebFilter, SecurityWebFiltersOrder.LOGOUT); + } + + private LogoutWebFilter createLogoutWebFilter(HttpSecurity http) { + LogoutWebFilter logoutWebFilter = new LogoutWebFilter(); + logoutWebFilter.setLogoutHandler(this.logoutHandler); + logoutWebFilter.setRequiresLogout(this.requiresLogout); + + return logoutWebFilter; + } + } + private static class OrderedWebFilter implements WebFilter, Ordered { private final WebFilter webFilter; private final int order; diff --git a/config/src/test/java/org/springframework/security/config/web/server/LogoutBuilderTests.java b/config/src/test/java/org/springframework/security/config/web/server/LogoutBuilderTests.java new file mode 100644 index 0000000000..bcbbbd6038 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/web/server/LogoutBuilderTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server; + +import org.junit.Test; +import org.openqa.selenium.WebDriver; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.UserDetailsRepositoryAuthenticationManager; +import org.springframework.security.core.userdetails.MapUserDetailsRepository; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; + +/** + * @author Shazin Sadakath + * @since 5.0 + */ +public class LogoutBuilderTests { + + private UserDetails user = User.withUsername("user").password("password").roles("USER").build(); + private HttpSecurity http = HttpSecurity.http(); + + ReactiveAuthenticationManager manager = new UserDetailsRepositoryAuthenticationManager(new MapUserDetailsRepository(this.user)); + + @Test + public void defaultLogout() { + SecurityWebFilterChain securityWebFilter = this.http + .authenticationManager(this.manager) + .authorizeExchange() + .anyExchange().authenticated() + .and() + .formLogin().and() + .build(); + + WebTestClient webTestClient = WebTestClientBuilder + .bindToWebFilters(securityWebFilter) + .build(); + + WebDriver driver = WebTestClientHtmlUnitDriverBuilder + .webTestClientSetup(webTestClient) + .build(); + + FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class) + .assertAt(); + + loginPage = loginPage.loginForm() + .username("user") + .password("invalid") + .submit(FormLoginTests.DefaultLoginPage.class) + .assertError(); + + FormLoginTests.HomePage homePage = loginPage.loginForm() + .username("user") + .password("password") + .submit(FormLoginTests.HomePage.class); + + homePage.assertAt(); + + driver.get("http://localhost/logout"); + + FormLoginTests.DefaultLoginPage.create(driver) + .assertAt() + .assertLogout(); + } + + @Test + public void customLogout() { + SecurityWebFilterChain securityWebFilter = this.http + .authenticationManager(this.manager) + .authorizeExchange() + .anyExchange().authenticated() + .and() + .formLogin().and() + .logout().logoutUrl("/custom-logout").and() + .build(); + + WebTestClient webTestClient = WebTestClientBuilder + .bindToWebFilters(securityWebFilter) + .build(); + + WebDriver driver = WebTestClientHtmlUnitDriverBuilder + .webTestClientSetup(webTestClient) + .build(); + + FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class) + .assertAt(); + + loginPage = loginPage.loginForm() + .username("user") + .password("invalid") + .submit(FormLoginTests.DefaultLoginPage.class) + .assertError(); + + FormLoginTests.HomePage homePage = loginPage.loginForm() + .username("user") + .password("password") + .submit(FormLoginTests.HomePage.class); + + homePage.assertAt(); + + driver.get("http://localhost/custom-logout"); + + FormLoginTests.DefaultLoginPage.create(driver) + .assertAt() + .assertLogout(); + } +} diff --git a/webflux/src/main/java/org/springframework/security/web/server/authentication/logout/LogoutWebFiter.java b/webflux/src/main/java/org/springframework/security/web/server/authentication/logout/LogoutWebFilter.java similarity index 82% rename from webflux/src/main/java/org/springframework/security/web/server/authentication/logout/LogoutWebFiter.java rename to webflux/src/main/java/org/springframework/security/web/server/authentication/logout/LogoutWebFilter.java index edea211940..a2c0a5350e 100644 --- a/webflux/src/main/java/org/springframework/security/web/server/authentication/logout/LogoutWebFiter.java +++ b/webflux/src/main/java/org/springframework/security/web/server/authentication/logout/LogoutWebFilter.java @@ -16,6 +16,7 @@ package org.springframework.security.web.server.authentication.logout; +import org.springframework.util.Assert; import reactor.core.publisher.Mono; import org.springframework.security.authentication.AnonymousAuthenticationToken; @@ -32,7 +33,7 @@ import org.springframework.web.server.WebFilterChain; * @author Rob Winch * @since 5.0 */ -public class LogoutWebFiter implements WebFilter { +public class LogoutWebFilter implements WebFilter { private AnonymousAuthenticationToken anonymousAuthenticationToken = new AnonymousAuthenticationToken("key", "anonymous", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); private LogoutHandler logoutHandler = new SecurityContextRepositoryLogoutHandler(); @@ -54,4 +55,14 @@ public class LogoutWebFiter implements WebFilter { .cast(Authentication.class) .defaultIfEmpty(this.anonymousAuthenticationToken); } + + public final void setLogoutHandler(LogoutHandler logoutHandler) { + Assert.notNull(logoutHandler, "logoutHandler must not be null"); + this.logoutHandler = logoutHandler; + } + + public final void setRequiresLogout(ServerWebExchangeMatcher serverWebExchangeMatcher) { + Assert.notNull(serverWebExchangeMatcher, "serverWebExchangeMatcher must not be null"); + this.requiresLogout = serverWebExchangeMatcher; + } }