From 2c136f7b6c5b9ea75430bc660489bb8b22c1e466 Mon Sep 17 00:00:00 2001 From: MD Sayem Ahmed Date: Fri, 19 Apr 2019 01:03:38 +0200 Subject: [PATCH] Add Reactive Clear-Site-Data Support 1. A new implementation of ServerHttpHeadersWriter has been created to add Clear-Site-Data header support. 2. A new implementation of ServerLogoutHandler has been created which can be configured to write response headers during logout. 3. Added unit tests for both implementations. Fixes gh-6743 --- .../HeaderWriterServerLogoutHandler.java | 50 ++++++++ .../ClearSiteDataServerHttpHeadersWriter.java | 95 ++++++++++++++ .../HeaderWriterServerLogoutHandlerTests.java | 55 ++++++++ ...rSiteDataServerHttpHeadersWriterTests.java | 120 ++++++++++++++++++ 4 files changed, 320 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/server/authentication/logout/HeaderWriterServerLogoutHandler.java create mode 100644 web/src/main/java/org/springframework/security/web/server/header/ClearSiteDataServerHttpHeadersWriter.java create mode 100644 web/src/test/java/org/springframework/security/web/server/authentication/logout/HeaderWriterServerLogoutHandlerTests.java create mode 100644 web/src/test/java/org/springframework/security/web/server/header/ClearSiteDataServerHttpHeadersWriterTests.java diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/logout/HeaderWriterServerLogoutHandler.java b/web/src/main/java/org/springframework/security/web/server/authentication/logout/HeaderWriterServerLogoutHandler.java new file mode 100644 index 0000000000..ddecd55015 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authentication/logout/HeaderWriterServerLogoutHandler.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2019 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 + * + * https://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 org.springframework.security.web.server.header.ServerHttpHeadersWriter; +import org.springframework.util.Assert; + +import reactor.core.publisher.Mono; + +/** + *

A {@link ServerLogoutHandler} implementation which writes HTTP headers during logout.

+ * + * @author MD Sayem Ahmed + * @since 5.2 + */ +public final class HeaderWriterServerLogoutHandler implements ServerLogoutHandler { + private final ServerHttpHeadersWriter headersWriter; + + /** + *

Constructs a new instance using the {@link ServerHttpHeadersWriter} implementation.

+ + * @param headersWriter a {@link ServerHttpHeadersWriter} implementation + * @throws IllegalArgumentException if the argument is null + */ + public HeaderWriterServerLogoutHandler(ServerHttpHeadersWriter headersWriter) { + Assert.notNull(headersWriter, "headersWriter cannot be null"); + this.headersWriter = headersWriter; + } + + @Override + public Mono logout(WebFilterExchange exchange, Authentication authentication) { + return this.headersWriter + .writeHttpHeaders(exchange.getExchange()); + } +} diff --git a/web/src/main/java/org/springframework/security/web/server/header/ClearSiteDataServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/ClearSiteDataServerHttpHeadersWriter.java new file mode 100644 index 0000000000..c3aed7df0d --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/header/ClearSiteDataServerHttpHeadersWriter.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.header; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +import reactor.core.publisher.Mono; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + *

Writes the {@code Clear-Site-Data} response header when the request is secure.

+ * + *

For further details pleaes consult W3C Documentation.

+ * + * @author MD Sayem Ahmed + * @since 5.2 + */ +public final class ClearSiteDataServerHttpHeadersWriter implements ServerHttpHeadersWriter { + public static final String CLEAR_SITE_DATA_HEADER = "Clear-Site-Data"; + + private final StaticServerHttpHeadersWriter headerWriterDelegate; + + /** + *

Constructs a new instance using the given directives.

+ * + * @param directives directives that will be written as the header value + * @throws IllegalArgumentException if the argument is null or empty + */ + public ClearSiteDataServerHttpHeadersWriter(Directive... directives) { + Assert.notEmpty(directives, "directives cannot be empty or null."); + this.headerWriterDelegate = StaticServerHttpHeadersWriter.builder() + .header(CLEAR_SITE_DATA_HEADER, transformToHeaderValue(directives)) + .build(); + } + + @Override + public Mono writeHttpHeaders(ServerWebExchange exchange) { + if (isSecure(exchange)) { + return this.headerWriterDelegate + .writeHttpHeaders(exchange); + } else { + return Mono.empty(); + } + } + + /** + *

Represents the directive values expected by the {@link ClearSiteDataServerHttpHeadersWriter}

. + */ + public enum Directive { + CACHE("cache"), + COOKIES("cookies"), + STORAGE("storage"), + EXECUTION_CONTEXTS("executionContexts"), + ALL("*"); + + private final String headerValue; + + Directive(String headerValue) { + this.headerValue = "\"" + headerValue + "\""; + } + + public String getHeaderValue() { + return this.headerValue; + } + } + + private String transformToHeaderValue(Directive... directives) { + return Stream.of(directives) + .map(Directive::getHeaderValue) + .collect(Collectors.joining(", ")); + } + + private boolean isSecure(ServerWebExchange exchange) { + String scheme = exchange.getRequest() + .getURI() + .getScheme(); + return scheme != null && scheme.equalsIgnoreCase("https"); + } +} diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/logout/HeaderWriterServerLogoutHandlerTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/logout/HeaderWriterServerLogoutHandlerTests.java new file mode 100644 index 0000000000..b610be095a --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/authentication/logout/HeaderWriterServerLogoutHandlerTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2019 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 + * + * https://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 org.springframework.security.web.server.header.ServerHttpHeadersWriter; +import org.springframework.web.server.ServerWebExchange; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author MD Sayem Ahmed + * @since 5.2 + */ +public class HeaderWriterServerLogoutHandlerTests { + + @Test + public void constructorWhenHeadersWriterIsNullThenExceptionThrown() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> new HeaderWriterServerLogoutHandler(null)); + } + + @Test + public void logoutWhenInvokedThenWritesResponseHeaders() { + ServerHttpHeadersWriter headersWriter = mock(ServerHttpHeadersWriter.class); + HeaderWriterServerLogoutHandler handler = new HeaderWriterServerLogoutHandler(headersWriter); + ServerWebExchange serverWebExchange = mock(ServerWebExchange.class); + WebFilterExchange filterExchange = mock(WebFilterExchange.class); + when(filterExchange.getExchange()).thenReturn(serverWebExchange); + Authentication authentication = mock(Authentication.class); + + handler.logout(filterExchange, authentication); + + verify(headersWriter).writeHttpHeaders(serverWebExchange); + } +} diff --git a/web/src/test/java/org/springframework/security/web/server/header/ClearSiteDataServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/ClearSiteDataServerHttpHeadersWriterTests.java new file mode 100644 index 0000000000..4bbb260643 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/header/ClearSiteDataServerHttpHeadersWriterTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.header; + +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.web.server.header.ClearSiteDataServerHttpHeadersWriter.Directive; +import org.springframework.util.CollectionUtils; +import org.springframework.web.server.ServerWebExchange; + +import org.assertj.core.api.AbstractAssert; +import org.junit.Test; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author MD Sayem Ahmed + * @since 5.2 + */ +public class ClearSiteDataServerHttpHeadersWriterTests { + + @Test + public void constructorWhenMissingDirectivesThenThrowsException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(ClearSiteDataServerHttpHeadersWriter::new); + } + + @Test + public void writeHttpHeadersWhenSecureConnectionThenHeaderWritten() { + ClearSiteDataServerHttpHeadersWriter writer = new ClearSiteDataServerHttpHeadersWriter(Directive.ALL); + ServerWebExchange secureExchange = MockServerWebExchange.from( + MockServerHttpRequest.get("https://localhost") + .build()); + + writer.writeHttpHeaders(secureExchange); + + assertThat(secureExchange.getResponse()).hasClearSiteDataHeaderDirectives(Directive.ALL); + } + + @Test + public void writeHttpHeadersWhenInsecureConnectionThenHeaderNotWritten() { + ClearSiteDataServerHttpHeadersWriter writer = new ClearSiteDataServerHttpHeadersWriter(Directive.ALL); + ServerWebExchange insecureExchange = MockServerWebExchange.from( + MockServerHttpRequest.get("/") + .build()); + + writer.writeHttpHeaders(insecureExchange); + + assertThat(insecureExchange.getResponse()).doesNotHaveClearSiteDataHeaderSet(); + } + + @Test + public void writeHttpHeadersWhenMultipleDirectivesSpecifiedThenHeaderContainsAll() { + ClearSiteDataServerHttpHeadersWriter writer = new ClearSiteDataServerHttpHeadersWriter( + Directive.CACHE, Directive.COOKIES); + ServerWebExchange secureExchange = MockServerWebExchange.from( + MockServerHttpRequest.get("https://localhost") + .build()); + + writer.writeHttpHeaders(secureExchange); + + assertThat(secureExchange.getResponse()).hasClearSiteDataHeaderDirectives(Directive.CACHE, Directive.COOKIES); + } + + private static ClearSiteDataAssert assertThat(ServerHttpResponse response) { + return new ClearSiteDataAssert(response); + } + + private static class ClearSiteDataAssert extends AbstractAssert { + + ClearSiteDataAssert(ServerHttpResponse response) { + super(response, ClearSiteDataAssert.class); + } + + void hasClearSiteDataHeaderDirectives(Directive... directives) { + isNotNull(); + List header = getHeader(); + String actualHeaderValue = String.join("", header); + String expectedHeaderVale = Stream.of(directives) + .map(Directive::getHeaderValue) + .collect(Collectors.joining(", ")); + if (!actualHeaderValue.equals(expectedHeaderVale)) { + failWithMessage("Expected to have %s as Clear-Site-Data header value but found %s", + expectedHeaderVale, actualHeaderValue); + } + } + + void doesNotHaveClearSiteDataHeaderSet() { + isNotNull(); + List header = getHeader(); + if (!CollectionUtils.isEmpty(header)) { + failWithMessage("Expected not to have Clear-Site-Data header set but found %s", + String.join("", header)); + } + } + + List getHeader() { + return actual.getHeaders() + .get(ClearSiteDataServerHttpHeadersWriter.CLEAR_SITE_DATA_HEADER); + } + } +}