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 098e2f9372..e878d6b627 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 @@ -24,6 +24,7 @@ import java.util.List; 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.security.web.util.matcher.MediaTypeRequestMatcher; import reactor.core.publisher.Mono; @@ -175,6 +176,7 @@ public class HttpSecurity { filters.add(new LoginPageGeneratingWebFilter()); } filters.add(this.formLogin.build()); + filters.add(new LogoutWebFiter()); } filters.add(new AuthenticationReactorContextFilter()); if(this.authorizeExchangeBuilder != null) { 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 e5ae8355b5..0077404fe3 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 @@ -67,7 +67,14 @@ public class FormLoginTests { .webTestClientSetup(webTestClient) .build(); - DefaultLoginPage loginPage = HomePage.to(driver, DefaultLoginPage.class); + DefaultLoginPage loginPage = HomePage.to(driver, DefaultLoginPage.class) + .assertAt(); + + loginPage = loginPage.loginForm() + .username("user") + .password("invalid") + .submit(DefaultLoginPage.class) + .assertError(); HomePage homePage = loginPage.loginForm() .username("user") @@ -75,6 +82,12 @@ public class FormLoginTests { .submit(HomePage.class); homePage.assertAt(); + + driver.get("http://localhost/logout"); + + DefaultLoginPage.create(driver) + .assertAt() + .assertLogout(); } @Test @@ -161,6 +174,8 @@ public class FormLoginTests { public static class DefaultLoginPage { private WebDriver driver; + @FindBy(css = "div[role=alert]") + private WebElement alert; private LoginForm loginForm; @@ -169,6 +184,25 @@ public class FormLoginTests { this.loginForm = PageFactory.initElements(webDriver, LoginForm.class); } + static DefaultLoginPage create(WebDriver driver) { + return PageFactory.initElements(driver, DefaultLoginPage.class); + } + + public DefaultLoginPage assertAt() { + assertThat(this.driver.getTitle()).isEqualTo("Please sign in"); + return this; + } + + public DefaultLoginPage assertError() { + assertThat(this.alert.getText()).isEqualTo("Invalid credentials"); + return this; + } + + public DefaultLoginPage assertLogout() { + assertThat(this.alert.getText()).isEqualTo("You have been signed out"); + return this; + } + public LoginForm loginForm() { return this.loginForm; } diff --git a/webflux/src/main/java/org/springframework/security/web/server/authentication/logout/LogoutHandler.java b/webflux/src/main/java/org/springframework/security/web/server/authentication/logout/LogoutHandler.java new file mode 100644 index 0000000000..d997a22893 --- /dev/null +++ b/webflux/src/main/java/org/springframework/security/web/server/authentication/logout/LogoutHandler.java @@ -0,0 +1,31 @@ +/* + * + * * 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.authentication.logout; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.WebFilterExchange; +import reactor.core.publisher.Mono; + +/** + * @author Rob Winch + * @since 5.0 + */ +public interface LogoutHandler { + Mono logout(WebFilterExchange exchange, Authentication authentication); +} 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/LogoutWebFiter.java new file mode 100644 index 0000000000..9950a04ca5 --- /dev/null +++ b/webflux/src/main/java/org/springframework/security/web/server/authentication/logout/LogoutWebFiter.java @@ -0,0 +1,54 @@ +/* + * + * * 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.authentication.logout; + +import reactor.core.publisher.Mono; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; + +/** + * @author Rob Winch + * @since 5.0 + */ +public class LogoutWebFiter implements WebFilter { + private AnonymousAuthenticationToken anonymousAuthenticationToken = new AnonymousAuthenticationToken("key", "anonymous", + AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + private LogoutHandler logoutHandler = new SecurityContextRepositoryLogoutHandler(); + + private ServerWebExchangeMatcher requiresLogout = ServerWebExchangeMatchers + .pathMatchers("/logout"); + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return this.requiresLogout.matches(exchange) + .filter( result -> result.isMatch()) + .switchIfEmpty(chain.filter(exchange).then(Mono.empty())) + .flatMap( result -> exchange.getPrincipal().cast(Authentication.class)) + .defaultIfEmpty(this.anonymousAuthenticationToken) + .flatMap( authentication -> this.logoutHandler.logout(new WebFilterExchange(exchange, chain), authentication)); + } +} diff --git a/webflux/src/main/java/org/springframework/security/web/server/authentication/logout/SecurityContextRepositoryLogoutHandler.java b/webflux/src/main/java/org/springframework/security/web/server/authentication/logout/SecurityContextRepositoryLogoutHandler.java new file mode 100644 index 0000000000..8097a2c2b3 --- /dev/null +++ b/webflux/src/main/java/org/springframework/security/web/server/authentication/logout/SecurityContextRepositoryLogoutHandler.java @@ -0,0 +1,48 @@ +/* + * + * * 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.authentication.logout; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.DefaultRedirectStrategy; +import org.springframework.security.web.server.RedirectStrategy; +import org.springframework.security.web.server.context.SecurityContextRepository; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.server.context.WebSessionSecurityContextRepository; +import reactor.core.publisher.Mono; + +import java.net.URI; + +/** + * @author Rob Winch + * @since 5.0 + */ +public class SecurityContextRepositoryLogoutHandler implements LogoutHandler { + private SecurityContextRepository repository = new WebSessionSecurityContextRepository(); + + private URI logoutSuccessUrl = URI.create("/login?logout"); + + private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + + @Override + public Mono logout(WebFilterExchange exchange, + Authentication authentication) { + return this.repository.save(exchange.getExchange(), null) + .then(this.redirectStrategy.sendRedirect(exchange.getExchange(), this.logoutSuccessUrl)); + } +} diff --git a/webflux/src/main/java/org/springframework/security/web/server/context/WebSessionSecurityContextRepository.java b/webflux/src/main/java/org/springframework/security/web/server/context/WebSessionSecurityContextRepository.java index 8fab72c465..6f63ff41a7 100644 --- a/webflux/src/main/java/org/springframework/security/web/server/context/WebSessionSecurityContextRepository.java +++ b/webflux/src/main/java/org/springframework/security/web/server/context/WebSessionSecurityContextRepository.java @@ -32,7 +32,13 @@ public class WebSessionSecurityContextRepository implements SecurityContextRepos public Mono save(ServerWebExchange exchange, SecurityContext context) { return exchange.getSession() - .doOnNext(session -> session.getAttributes().put(SESSION_ATTR, context)) + .doOnNext(session -> { + if(context == null) { + session.getAttributes().remove(SESSION_ATTR); + } else { + session.getAttributes().put(SESSION_ATTR, context); + } + }) .then(); } diff --git a/webflux/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java b/webflux/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java index baedd859b3..3a1f000130 100644 --- a/webflux/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java +++ b/webflux/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java @@ -27,6 +27,7 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpResponse; 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; @@ -50,18 +51,21 @@ public class LoginPageGeneratingWebFilter implements WebFilter { } private Mono render(ServerWebExchange exchange) { - boolean isError = exchange.getRequest().getQueryParams().containsKey("error"); + MultiValueMap queryParams = exchange.getRequest() + .getQueryParams(); + boolean isError = queryParams.containsKey("error"); + boolean isLogoutSuccess = queryParams.containsKey("logout"); ServerHttpResponse result = exchange.getResponse(); result.setStatusCode(HttpStatus.FOUND); result.getHeaders().setContentType(MediaType.TEXT_HTML); - byte[] bytes = createPage(isError); + byte[] bytes = createPage(isError, isLogoutSuccess); DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory(); DataBuffer buffer = bufferFactory.wrap(bytes); return result.writeWith(Mono.just(buffer)) .doOnError( error -> DataBufferUtils.release(buffer)); } - private static byte[] createPage(boolean isError) { + private static byte[] createPage(boolean isError, boolean isLogoutSuccess) { String page = "\n" + "\n" + " \n" @@ -78,6 +82,7 @@ public class LoginPageGeneratingWebFilter implements WebFilter { + "
\n" + " \n" + createError(isError) + + createLogoutSuccess(isLogoutSuccess) + "

\n" + " \n" + " \n" @@ -98,4 +103,8 @@ public class LoginPageGeneratingWebFilter implements WebFilter { private static String createError(boolean isError) { return isError ? "

Invalid credentials
" : ""; } + + private static String createLogoutSuccess(boolean isLogoutSuccess) { + return isLogoutSuccess ? "
You have been signed out
" : ""; + } }