Skip to content

Commit 2c136f7

Browse files
sayembdjzheaux
authored andcommitted
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
1 parent 19e823f commit 2c136f7

File tree

4 files changed

+320
-0
lines changed

4 files changed

+320
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.web.server.authentication.logout;
17+
18+
import org.springframework.security.core.Authentication;
19+
import org.springframework.security.web.server.WebFilterExchange;
20+
import org.springframework.security.web.server.header.ServerHttpHeadersWriter;
21+
import org.springframework.util.Assert;
22+
23+
import reactor.core.publisher.Mono;
24+
25+
/**
26+
* <p>A {@link ServerLogoutHandler} implementation which writes HTTP headers during logout.</p>
27+
*
28+
* @author MD Sayem Ahmed
29+
* @since 5.2
30+
*/
31+
public final class HeaderWriterServerLogoutHandler implements ServerLogoutHandler {
32+
private final ServerHttpHeadersWriter headersWriter;
33+
34+
/**
35+
* <p>Constructs a new instance using the {@link ServerHttpHeadersWriter} implementation.</p>
36+
37+
* @param headersWriter a {@link ServerHttpHeadersWriter} implementation
38+
* @throws IllegalArgumentException if the argument is null
39+
*/
40+
public HeaderWriterServerLogoutHandler(ServerHttpHeadersWriter headersWriter) {
41+
Assert.notNull(headersWriter, "headersWriter cannot be null");
42+
this.headersWriter = headersWriter;
43+
}
44+
45+
@Override
46+
public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
47+
return this.headersWriter
48+
.writeHttpHeaders(exchange.getExchange());
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.web.server.header;
17+
18+
import org.springframework.util.Assert;
19+
import org.springframework.web.server.ServerWebExchange;
20+
21+
import reactor.core.publisher.Mono;
22+
23+
import java.util.stream.Collectors;
24+
import java.util.stream.Stream;
25+
26+
/**
27+
* <p>Writes the {@code Clear-Site-Data} response header when the request is secure.</p>
28+
*
29+
* <p>For further details pleaes consult <a href="https://www.w3.org/TR/clear-site-data/">W3C Documentation</a>.</p>
30+
*
31+
* @author MD Sayem Ahmed
32+
* @since 5.2
33+
*/
34+
public final class ClearSiteDataServerHttpHeadersWriter implements ServerHttpHeadersWriter {
35+
public static final String CLEAR_SITE_DATA_HEADER = "Clear-Site-Data";
36+
37+
private final StaticServerHttpHeadersWriter headerWriterDelegate;
38+
39+
/**
40+
* <p>Constructs a new instance using the given directives.</p>
41+
*
42+
* @param directives directives that will be written as the header value
43+
* @throws IllegalArgumentException if the argument is null or empty
44+
*/
45+
public ClearSiteDataServerHttpHeadersWriter(Directive... directives) {
46+
Assert.notEmpty(directives, "directives cannot be empty or null.");
47+
this.headerWriterDelegate = StaticServerHttpHeadersWriter.builder()
48+
.header(CLEAR_SITE_DATA_HEADER, transformToHeaderValue(directives))
49+
.build();
50+
}
51+
52+
@Override
53+
public Mono<Void> writeHttpHeaders(ServerWebExchange exchange) {
54+
if (isSecure(exchange)) {
55+
return this.headerWriterDelegate
56+
.writeHttpHeaders(exchange);
57+
} else {
58+
return Mono.empty();
59+
}
60+
}
61+
62+
/**
63+
* <p>Represents the directive values expected by the {@link ClearSiteDataServerHttpHeadersWriter}</p>.
64+
*/
65+
public enum Directive {
66+
CACHE("cache"),
67+
COOKIES("cookies"),
68+
STORAGE("storage"),
69+
EXECUTION_CONTEXTS("executionContexts"),
70+
ALL("*");
71+
72+
private final String headerValue;
73+
74+
Directive(String headerValue) {
75+
this.headerValue = "\"" + headerValue + "\"";
76+
}
77+
78+
public String getHeaderValue() {
79+
return this.headerValue;
80+
}
81+
}
82+
83+
private String transformToHeaderValue(Directive... directives) {
84+
return Stream.of(directives)
85+
.map(Directive::getHeaderValue)
86+
.collect(Collectors.joining(", "));
87+
}
88+
89+
private boolean isSecure(ServerWebExchange exchange) {
90+
String scheme = exchange.getRequest()
91+
.getURI()
92+
.getScheme();
93+
return scheme != null && scheme.equalsIgnoreCase("https");
94+
}
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.web.server.authentication.logout;
17+
18+
import org.springframework.security.core.Authentication;
19+
import org.springframework.security.web.server.WebFilterExchange;
20+
import org.springframework.security.web.server.header.ServerHttpHeadersWriter;
21+
import org.springframework.web.server.ServerWebExchange;
22+
23+
import org.junit.Test;
24+
25+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
26+
import static org.mockito.Mockito.mock;
27+
import static org.mockito.Mockito.verify;
28+
import static org.mockito.Mockito.when;
29+
30+
/**
31+
* @author MD Sayem Ahmed
32+
* @since 5.2
33+
*/
34+
public class HeaderWriterServerLogoutHandlerTests {
35+
36+
@Test
37+
public void constructorWhenHeadersWriterIsNullThenExceptionThrown() {
38+
assertThatExceptionOfType(IllegalArgumentException.class)
39+
.isThrownBy(() -> new HeaderWriterServerLogoutHandler(null));
40+
}
41+
42+
@Test
43+
public void logoutWhenInvokedThenWritesResponseHeaders() {
44+
ServerHttpHeadersWriter headersWriter = mock(ServerHttpHeadersWriter.class);
45+
HeaderWriterServerLogoutHandler handler = new HeaderWriterServerLogoutHandler(headersWriter);
46+
ServerWebExchange serverWebExchange = mock(ServerWebExchange.class);
47+
WebFilterExchange filterExchange = mock(WebFilterExchange.class);
48+
when(filterExchange.getExchange()).thenReturn(serverWebExchange);
49+
Authentication authentication = mock(Authentication.class);
50+
51+
handler.logout(filterExchange, authentication);
52+
53+
verify(headersWriter).writeHttpHeaders(serverWebExchange);
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.web.server.header;
17+
18+
import org.springframework.http.server.reactive.ServerHttpResponse;
19+
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
20+
import org.springframework.mock.web.server.MockServerWebExchange;
21+
import org.springframework.security.web.server.header.ClearSiteDataServerHttpHeadersWriter.Directive;
22+
import org.springframework.util.CollectionUtils;
23+
import org.springframework.web.server.ServerWebExchange;
24+
25+
import org.assertj.core.api.AbstractAssert;
26+
import org.junit.Test;
27+
28+
import java.util.List;
29+
import java.util.stream.Collectors;
30+
import java.util.stream.Stream;
31+
32+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
33+
34+
/**
35+
* @author MD Sayem Ahmed
36+
* @since 5.2
37+
*/
38+
public class ClearSiteDataServerHttpHeadersWriterTests {
39+
40+
@Test
41+
public void constructorWhenMissingDirectivesThenThrowsException() {
42+
assertThatExceptionOfType(IllegalArgumentException.class)
43+
.isThrownBy(ClearSiteDataServerHttpHeadersWriter::new);
44+
}
45+
46+
@Test
47+
public void writeHttpHeadersWhenSecureConnectionThenHeaderWritten() {
48+
ClearSiteDataServerHttpHeadersWriter writer = new ClearSiteDataServerHttpHeadersWriter(Directive.ALL);
49+
ServerWebExchange secureExchange = MockServerWebExchange.from(
50+
MockServerHttpRequest.get("https://localhost")
51+
.build());
52+
53+
writer.writeHttpHeaders(secureExchange);
54+
55+
assertThat(secureExchange.getResponse()).hasClearSiteDataHeaderDirectives(Directive.ALL);
56+
}
57+
58+
@Test
59+
public void writeHttpHeadersWhenInsecureConnectionThenHeaderNotWritten() {
60+
ClearSiteDataServerHttpHeadersWriter writer = new ClearSiteDataServerHttpHeadersWriter(Directive.ALL);
61+
ServerWebExchange insecureExchange = MockServerWebExchange.from(
62+
MockServerHttpRequest.get("/")
63+
.build());
64+
65+
writer.writeHttpHeaders(insecureExchange);
66+
67+
assertThat(insecureExchange.getResponse()).doesNotHaveClearSiteDataHeaderSet();
68+
}
69+
70+
@Test
71+
public void writeHttpHeadersWhenMultipleDirectivesSpecifiedThenHeaderContainsAll() {
72+
ClearSiteDataServerHttpHeadersWriter writer = new ClearSiteDataServerHttpHeadersWriter(
73+
Directive.CACHE, Directive.COOKIES);
74+
ServerWebExchange secureExchange = MockServerWebExchange.from(
75+
MockServerHttpRequest.get("https://localhost")
76+
.build());
77+
78+
writer.writeHttpHeaders(secureExchange);
79+
80+
assertThat(secureExchange.getResponse()).hasClearSiteDataHeaderDirectives(Directive.CACHE, Directive.COOKIES);
81+
}
82+
83+
private static ClearSiteDataAssert assertThat(ServerHttpResponse response) {
84+
return new ClearSiteDataAssert(response);
85+
}
86+
87+
private static class ClearSiteDataAssert extends AbstractAssert<ClearSiteDataAssert, ServerHttpResponse> {
88+
89+
ClearSiteDataAssert(ServerHttpResponse response) {
90+
super(response, ClearSiteDataAssert.class);
91+
}
92+
93+
void hasClearSiteDataHeaderDirectives(Directive... directives) {
94+
isNotNull();
95+
List<String> header = getHeader();
96+
String actualHeaderValue = String.join("", header);
97+
String expectedHeaderVale = Stream.of(directives)
98+
.map(Directive::getHeaderValue)
99+
.collect(Collectors.joining(", "));
100+
if (!actualHeaderValue.equals(expectedHeaderVale)) {
101+
failWithMessage("Expected to have %s as Clear-Site-Data header value but found %s",
102+
expectedHeaderVale, actualHeaderValue);
103+
}
104+
}
105+
106+
void doesNotHaveClearSiteDataHeaderSet() {
107+
isNotNull();
108+
List<String> header = getHeader();
109+
if (!CollectionUtils.isEmpty(header)) {
110+
failWithMessage("Expected not to have Clear-Site-Data header set but found %s",
111+
String.join("", header));
112+
}
113+
}
114+
115+
List<String> getHeader() {
116+
return actual.getHeaders()
117+
.get(ClearSiteDataServerHttpHeadersWriter.CLEAR_SITE_DATA_HEADER);
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)