From 5a5ec58ca4537520b267b36dba25269189cd2c8e Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Sun, 29 Oct 2017 00:11:37 -0500 Subject: [PATCH] Add LogoutPageGeneratingWebFilter Fixes gh-4735 --- .../web/server/SecurityWebFiltersOrder.java | 1 + .../config/web/server/ServerHttpSecurity.java | 2 + .../config/web/server/FormLoginTests.java | 32 +++++- .../config/web/server/LogoutBuilderTests.java | 9 +- .../ui/LogoutPageGeneratingWebFilter.java | 102 ++++++++++++++++++ 5 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 web/src/main/java/org/springframework/security/web/server/ui/LogoutPageGeneratingWebFilter.java diff --git a/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java b/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java index 54ada6f56a..ce4e349a97 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java +++ b/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java @@ -41,6 +41,7 @@ public enum SecurityWebFiltersOrder { */ REACTOR_CONTEXT, LOGIN_PAGE_GENERATING, + LOGOUT_PAGE_GENERATING, /** * {@link org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter} */ diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 93aa7f4e85..84844e3d55 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -61,6 +61,7 @@ import org.springframework.security.web.server.header.StrictTransportSecuritySer import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter; import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter; import org.springframework.security.web.server.ui.LoginPageGeneratingWebFilter; +import org.springframework.security.web.server.ui.LogoutPageGeneratingWebFilter; 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; @@ -220,6 +221,7 @@ public class ServerHttpSecurity { } if(this.formLogin.serverAuthenticationEntryPoint == null) { this.webFilters.add(new OrderedWebFilter(new LoginPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING.getOrder())); + this.webFilters.add(new OrderedWebFilter(new LogoutPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING.getOrder())); } this.formLogin.configure(this); } diff --git a/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java b/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java index f69bcd5685..2c7f76b8a8 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java @@ -78,9 +78,11 @@ public class FormLoginTests { homePage.assertAt(); - driver.get("http://localhost/logout"); + loginPage = DefaultLogoutPage.to(driver) + .assertAt() + .logout(); - DefaultLoginPage.create(driver) + loginPage .assertAt() .assertLogout(); } @@ -229,6 +231,32 @@ public class FormLoginTests { } } + public static class DefaultLogoutPage { + + private WebDriver driver; + @FindBy(css = "button[type=submit]") + private WebElement submit; + + public DefaultLogoutPage(WebDriver webDriver) { + this.driver = webDriver; + } + + public DefaultLogoutPage assertAt() { + assertThat(this.driver.getTitle()).isEqualTo("Confirm Log Out?"); + return this; + } + + public DefaultLoginPage logout() { + this.submit.click(); + return DefaultLoginPage.create(this.driver); + } + + static DefaultLogoutPage to(WebDriver driver) { + driver.get("http://localhost/logout"); + return PageFactory.initElements(driver, DefaultLogoutPage.class); + } + + } public static class HomePage { private WebDriver driver; 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 index 7fd688623f..7f0228af80 100644 --- 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 @@ -26,6 +26,7 @@ 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.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; @@ -70,9 +71,11 @@ public class LogoutBuilderTests { homePage.assertAt(); - driver.get("http://localhost/logout"); + loginPage = FormLoginTests.DefaultLogoutPage.to(driver) + .assertAt() + .logout(); - FormLoginTests.DefaultLoginPage.create(driver) + loginPage .assertAt() .assertLogout(); } @@ -85,7 +88,7 @@ public class LogoutBuilderTests { .and() .formLogin().and() .logout() - .logoutUrl("/custom-logout") + .requiresLogout(ServerWebExchangeMatchers.pathMatchers("/custom-logout")) .and() .build(); diff --git a/web/src/main/java/org/springframework/security/web/server/ui/LogoutPageGeneratingWebFilter.java b/web/src/main/java/org/springframework/security/web/server/ui/LogoutPageGeneratingWebFilter.java new file mode 100644 index 0000000000..aa411ce0d0 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/ui/LogoutPageGeneratingWebFilter.java @@ -0,0 +1,102 @@ +/* + * 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.web.server.ui; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.security.web.server.csrf.CsrfToken; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.springframework.util.MultiValueMap; +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.nio.charset.Charset; + +/** + * @author Rob Winch + * @since 5.0 + */ +public class LogoutPageGeneratingWebFilter implements WebFilter { + private ServerWebExchangeMatcher matcher = ServerWebExchangeMatchers + .pathMatchers(HttpMethod.GET, "/logout"); + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return this.matcher.matches(exchange) + .filter(ServerWebExchangeMatcher.MatchResult::isMatch) + .switchIfEmpty(chain.filter(exchange).then(Mono.empty())) + .flatMap(matchResult -> render(exchange)); + } + + private Mono render(ServerWebExchange exchange) { + ServerHttpResponse result = exchange.getResponse(); + result.setStatusCode(HttpStatus.OK); + result.getHeaders().setContentType(MediaType.TEXT_HTML); + return result.writeWith(createBuffer(exchange)); +// .doOnError( error -> DataBufferUtils.release(buffer)); + } + + private Mono createBuffer(ServerWebExchange exchange) { + Mono token = (Mono) exchange.getAttributes() + .getOrDefault(CsrfToken.class.getName(), Mono.empty()); + return token + .map(LogoutPageGeneratingWebFilter::csrfToken) + .defaultIfEmpty("") + .map(csrfTokenHtmlInput -> { + byte[] bytes = createPage(csrfTokenHtmlInput); + DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory(); + return bufferFactory.wrap(bytes); + }); + } + + private static byte[] createPage(String csrfTokenHtmlInput) { + String page = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Confirm Log Out?\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + csrfTokenHtmlInput + + " \n" + + "
\n" + + "
\n" + + " \n" + + ""; + + return page.getBytes(Charset.defaultCharset()); + } + + private static String csrfToken(CsrfToken token) { + return " \n"; + } +}