Skip to content

Commit 82d527e

Browse files
rhamedyjzheaux
authored andcommitted
Add Support for Clear Site Data on Logout
Added an implementation of HeaderWriter for Clear-Site-Data HTTP response header as welll as an implementation of LogoutHanlder that accepts an implementation of HeaderWriter to write headers. - Added ClearSiteDataHeaderWriter and HeaderWriterLogoutHandler that implements HeaderWriter and LogoutHandler respectively - Added unit tests for both implementations's behaviours - Integration tests for HeaderWriterLogoutHandler that uses ClearSiteDataHeaderWriter - Updated the documentation to include link to HeaderWriterLogoutHandler Fixes gh-4187
1 parent 7739a0e commit 82d527e

File tree

6 files changed

+450
-0
lines changed

6 files changed

+450
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
* http://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+
17+
package org.springframework.security.config.annotation.web.configurers;
18+
19+
import org.junit.Rule;
20+
import org.junit.Test;
21+
import org.junit.runner.RunWith;
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
24+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
25+
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
26+
import org.springframework.security.config.test.SpringTestRule;
27+
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
28+
import org.springframework.security.test.context.support.WithMockUser;
29+
import org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler;
30+
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter;
31+
import org.springframework.test.context.junit4.SpringRunner;
32+
import org.springframework.test.web.servlet.MockMvc;
33+
34+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
35+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
36+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
37+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
38+
39+
/**
40+
*
41+
* Tests for {@link HeaderWriterLogoutHandler} that passing {@link ClearSiteDataHeaderWriter}
42+
* implementation.
43+
*
44+
* @author Rafiullah Hamedy
45+
*
46+
*/
47+
@RunWith(SpringRunner.class)
48+
@SecurityTestExecutionListeners
49+
public class LogoutConfigurerClearSiteDataTests {
50+
51+
private static final String CLEAR_SITE_DATA_HEADER = "Clear-Site-Data";
52+
53+
private static final String[] SOURCE = {"cache", "cookies", "storage", "executionContexts"};
54+
55+
private static final String HEADER_VALUE = "\"cache\", \"cookies\", \"storage\", \"executionContexts\"";
56+
57+
@Rule
58+
public final SpringTestRule spring = new SpringTestRule();
59+
60+
@Autowired
61+
MockMvc mvc;
62+
63+
@Test
64+
@WithMockUser
65+
public void logoutWhenRequestTypeGetThenHeaderNotPresentt() throws Exception {
66+
this.spring.register(HttpLogoutConfig.class).autowire();
67+
68+
this.mvc.perform(get("/logout").secure(true).with(csrf()))
69+
.andExpect(header().doesNotExist(CLEAR_SITE_DATA_HEADER));
70+
}
71+
72+
@Test
73+
@WithMockUser
74+
public void logoutWhenRequestTypePostAndNotSecureThenHeaderNotPresent() throws Exception {
75+
this.spring.register(HttpLogoutConfig.class).autowire();
76+
77+
this.mvc.perform(post("/logout").with(csrf()))
78+
.andExpect(header().doesNotExist(CLEAR_SITE_DATA_HEADER));
79+
}
80+
81+
@Test
82+
@WithMockUser
83+
public void logoutWhenRequestTypePostAndSecureThenHeaderIsPresent() throws Exception {
84+
this.spring.register(HttpLogoutConfig.class).autowire();
85+
86+
this.mvc.perform(post("/logout").secure(true).with(csrf()))
87+
.andExpect(header().stringValues(CLEAR_SITE_DATA_HEADER, HEADER_VALUE));
88+
}
89+
90+
@EnableWebSecurity
91+
static class HttpLogoutConfig extends WebSecurityConfigurerAdapter {
92+
@Override
93+
protected void configure(HttpSecurity http) throws Exception {
94+
http
95+
.logout()
96+
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(SOURCE)));
97+
}
98+
}
99+
}

docs/manual/src/docs/asciidoc/_includes/servlet/preface/java-configuration.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ Various implementations are provided:
343343
- {security-api-url}org/springframework/security/web/authentication/logout/CookieClearingLogoutHandler.html[CookieClearingLogoutHandler]
344344
- {security-api-url}org/springframework/security/web/csrf/CsrfLogoutHandler.html[CsrfLogoutHandler]
345345
- {security-api-url}org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[SecurityContextLogoutHandler]
346+
- {security-api-url}org/springframework/security/web/authentication/logout/HeaderWriterLogoutHandler.html[HeaderWriterLogoutHandler]
346347

347348
Please see <<remember-me-impls>> for details.
348349

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+
* http://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+
17+
package org.springframework.security.web.authentication.logout;
18+
19+
import javax.servlet.http.HttpServletRequest;
20+
import javax.servlet.http.HttpServletResponse;
21+
22+
import org.springframework.security.core.Authentication;
23+
import org.springframework.security.web.header.HeaderWriter;
24+
import org.springframework.util.Assert;
25+
26+
/**
27+
*
28+
* @author Rafiullah Hamedy
29+
* @since 5.2
30+
*/
31+
public final class HeaderWriterLogoutHandler implements LogoutHandler {
32+
private final HeaderWriter headerWriter;
33+
34+
/**
35+
* Constructs a new instance using the passed {@link HeaderWriter} implementation
36+
*
37+
* @param headerWriter
38+
* @throws {@link IllegalArgumentException} if headerWriter is null.
39+
*/
40+
public HeaderWriterLogoutHandler(HeaderWriter headerWriter) {
41+
Assert.notNull(headerWriter, "headerWriter cannot be null.");
42+
this.headerWriter = headerWriter;
43+
}
44+
45+
@Override
46+
public void logout(HttpServletRequest request, HttpServletResponse response,
47+
Authentication authentication) {
48+
this.headerWriter.writeHeaders(request, response);
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
* http://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+
17+
package org.springframework.security.web.header.writers;
18+
19+
import java.util.stream.Collectors;
20+
import java.util.stream.Stream;
21+
22+
import javax.servlet.http.HttpServletRequest;
23+
import javax.servlet.http.HttpServletResponse;
24+
25+
import org.apache.commons.logging.Log;
26+
import org.apache.commons.logging.LogFactory;
27+
import org.springframework.security.web.header.HeaderWriter;
28+
import org.springframework.security.web.util.matcher.RequestMatcher;
29+
import org.springframework.util.Assert;
30+
31+
/**
32+
* Provides support for <a href="https://w3c.github.io/webappsec-clear-site-data/">Clear
33+
* Site Data</a>.
34+
*
35+
* <p>
36+
* Developers may instruct a user agent to clear various types of relevant data by delivering
37+
* a Clear-Site-Data HTTP response header in response to a request.
38+
* <p>
39+
*
40+
* <p>
41+
* Due to <a href="https://w3c.github.io/webappsec-clear-site-data/#incomplete">Incomplete Clearing</a>
42+
* section the header is only applied if the request is secure.
43+
* </p>
44+
*
45+
* @author Rafiullah Hamedy
46+
* @since 5.2
47+
*/
48+
public final class ClearSiteDataHeaderWriter implements HeaderWriter {
49+
50+
private static final String CLEAR_SITE_DATA_HEADER = "Clear-Site-Data";
51+
52+
private final Log logger = LogFactory.getLog(getClass());
53+
54+
private final RequestMatcher requestMatcher;
55+
56+
private String headerValue;
57+
58+
/**
59+
* <p>
60+
* Creates a new instance of {@link ClearSiteDataHeaderWriter} with given sources.
61+
* The constructor also initializes <b>requestMatcher</b> with a new instance of
62+
* <b>SecureRequestMatcher</b> to ensure that header is only applied if and when
63+
* the request is secure as per the <b>Incomplete Clearing</b> section.
64+
* </p>
65+
*
66+
* @param sources (i.e. "cache", "cookies", "storage", "executionContexts" or "*")
67+
* @throws {@link IllegalArgumentException} if sources is null or empty.
68+
*/
69+
public ClearSiteDataHeaderWriter(String ...sources) {
70+
Assert.notEmpty(sources, "Sources cannot be empty or null.");
71+
this.requestMatcher = new SecureRequestMatcher();
72+
this.headerValue = Stream.of(sources).map(this::quote).collect(Collectors.joining(", "));
73+
}
74+
75+
@Override
76+
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
77+
if (this.requestMatcher.matches(request)) {
78+
if (!response.containsHeader(CLEAR_SITE_DATA_HEADER)) {
79+
response.setHeader(CLEAR_SITE_DATA_HEADER, this.headerValue);
80+
}
81+
} else if (logger.isDebugEnabled()) {
82+
logger.debug("Not injecting Clear-Site-Data header since it did not match the "
83+
+ "requestMatcher " + this.requestMatcher);
84+
}
85+
}
86+
87+
private static final class SecureRequestMatcher implements RequestMatcher {
88+
public boolean matches(HttpServletRequest request) {
89+
return request.isSecure();
90+
}
91+
}
92+
93+
private String quote(String source) {
94+
return "\"" + source + "\"";
95+
}
96+
97+
@Override
98+
public String toString() {
99+
return getClass().getName() + " [headerValue=" + this.headerValue + "]";
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
* http://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+
17+
package org.springframework.security.web.authentication.logout;
18+
19+
import org.junit.Before;
20+
import org.junit.Rule;
21+
import org.junit.Test;
22+
import org.junit.rules.ExpectedException;
23+
import org.springframework.mock.web.MockHttpServletRequest;
24+
import org.springframework.mock.web.MockHttpServletResponse;
25+
import org.springframework.security.core.Authentication;
26+
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.mockito.Mockito.mock;
30+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
31+
32+
/**
33+
*
34+
* @author Rafiullah Hamedy
35+
*
36+
* @see {@link HeaderWriterLogoutHandler}
37+
*/
38+
public class HeaderWriterLogoutHandlerTests {
39+
private static final String HEADER_NAME = "Clear-Site-Data";
40+
41+
private MockHttpServletResponse response;
42+
private MockHttpServletRequest request;
43+
44+
@Rule
45+
public ExpectedException thrown = ExpectedException.none();
46+
47+
@Before
48+
public void setup() {
49+
this.response = new MockHttpServletResponse();
50+
this.request = new MockHttpServletRequest();
51+
}
52+
53+
@Test
54+
public void createInstanceWhenHeaderWriterIsNullThenThrowsException() {
55+
this.thrown.expect(IllegalArgumentException.class);
56+
this.thrown.expectMessage("headerWriter cannot be null.");
57+
58+
new HeaderWriterLogoutHandler(null);
59+
}
60+
61+
@Test
62+
public void createInstanceWhenSourceIsNullThenThrowsException() {
63+
this.thrown.expect(IllegalArgumentException.class);
64+
this.thrown.expectMessage("Sources cannot be empty or null.");
65+
66+
new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter());
67+
}
68+
69+
@Test
70+
public void logoutWhenRequestIsNotSecureThenHeaderIsNotPresent() {
71+
HeaderWriterLogoutHandler handler = new HeaderWriterLogoutHandler(
72+
new ClearSiteDataHeaderWriter("cache"));
73+
74+
handler.logout(request, response, mock(Authentication.class));
75+
76+
assertThat(header().doesNotExist(HEADER_NAME));
77+
}
78+
79+
@Test
80+
public void logoutWhenRequestIsSecureThenHeaderIsPresentMatchesWildCardSource() {
81+
HeaderWriterLogoutHandler handler = new HeaderWriterLogoutHandler(
82+
new ClearSiteDataHeaderWriter("*"));
83+
84+
this.request.setSecure(true);
85+
86+
handler.logout(request, response, mock(Authentication.class));
87+
88+
assertThat(header().stringValues(HEADER_NAME, "\"*\""));
89+
}
90+
91+
@Test
92+
public void logoutWhenRequestIsSecureThenHeaderValueMatchesSource() {
93+
HeaderWriterLogoutHandler handler = new HeaderWriterLogoutHandler(
94+
new ClearSiteDataHeaderWriter("cache", "cookies", "storage",
95+
"executionContexts"));
96+
97+
this.request.setSecure(true);
98+
99+
handler.logout(request, response, mock(Authentication.class));
100+
101+
assertThat(header().stringValues(HEADER_NAME, "\"cache\", \"cookies\", \"storage\", "
102+
+ "\"executionContexts\""));
103+
}
104+
}

0 commit comments

Comments
 (0)