forked from spring-projects/spring-security
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 spring-projectsgh-4187
- Loading branch information
Showing
6 changed files
with
450 additions
and
0 deletions.
There are no files selected for viewing
99 changes: 99 additions & 0 deletions
99
...mework/security/config/annotation/web/configurers/LogoutConfigurerClearSiteDataTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/* | ||
* 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 | ||
* | ||
* 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.config.annotation.web.configurers; | ||
|
||
import org.junit.Rule; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; | ||
import org.springframework.security.config.test.SpringTestRule; | ||
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; | ||
import org.springframework.security.test.context.support.WithMockUser; | ||
import org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler; | ||
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter; | ||
import org.springframework.test.context.junit4.SpringRunner; | ||
import org.springframework.test.web.servlet.MockMvc; | ||
|
||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; | ||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | ||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; | ||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; | ||
|
||
/** | ||
* | ||
* Tests for {@link HeaderWriterLogoutHandler} that passing {@link ClearSiteDataHeaderWriter} | ||
* implementation. | ||
* | ||
* @author Rafiullah Hamedy | ||
* | ||
*/ | ||
@RunWith(SpringRunner.class) | ||
@SecurityTestExecutionListeners | ||
public class LogoutConfigurerClearSiteDataTests { | ||
|
||
private static final String CLEAR_SITE_DATA_HEADER = "Clear-Site-Data"; | ||
|
||
private static final String[] SOURCE = {"cache", "cookies", "storage", "executionContexts"}; | ||
|
||
private static final String HEADER_VALUE = "\"cache\", \"cookies\", \"storage\", \"executionContexts\""; | ||
|
||
@Rule | ||
public final SpringTestRule spring = new SpringTestRule(); | ||
|
||
@Autowired | ||
MockMvc mvc; | ||
|
||
@Test | ||
@WithMockUser | ||
public void logoutWhenRequestTypeGetThenHeaderNotPresentt() throws Exception { | ||
this.spring.register(HttpLogoutConfig.class).autowire(); | ||
|
||
this.mvc.perform(get("/logout").secure(true).with(csrf())) | ||
.andExpect(header().doesNotExist(CLEAR_SITE_DATA_HEADER)); | ||
} | ||
|
||
@Test | ||
@WithMockUser | ||
public void logoutWhenRequestTypePostAndNotSecureThenHeaderNotPresent() throws Exception { | ||
this.spring.register(HttpLogoutConfig.class).autowire(); | ||
|
||
this.mvc.perform(post("/logout").with(csrf())) | ||
.andExpect(header().doesNotExist(CLEAR_SITE_DATA_HEADER)); | ||
} | ||
|
||
@Test | ||
@WithMockUser | ||
public void logoutWhenRequestTypePostAndSecureThenHeaderIsPresent() throws Exception { | ||
this.spring.register(HttpLogoutConfig.class).autowire(); | ||
|
||
this.mvc.perform(post("/logout").secure(true).with(csrf())) | ||
.andExpect(header().stringValues(CLEAR_SITE_DATA_HEADER, HEADER_VALUE)); | ||
} | ||
|
||
@EnableWebSecurity | ||
static class HttpLogoutConfig extends WebSecurityConfigurerAdapter { | ||
@Override | ||
protected void configure(HttpSecurity http) throws Exception { | ||
http | ||
.logout() | ||
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(SOURCE))); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
...ava/org/springframework/security/web/authentication/logout/HeaderWriterLogoutHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
* | ||
* 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.authentication.logout; | ||
|
||
import javax.servlet.http.HttpServletRequest; | ||
import javax.servlet.http.HttpServletResponse; | ||
|
||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.web.header.HeaderWriter; | ||
import org.springframework.util.Assert; | ||
|
||
/** | ||
* | ||
* @author Rafiullah Hamedy | ||
* @since 5.2 | ||
*/ | ||
public final class HeaderWriterLogoutHandler implements LogoutHandler { | ||
private final HeaderWriter headerWriter; | ||
|
||
/** | ||
* Constructs a new instance using the passed {@link HeaderWriter} implementation | ||
* | ||
* @param headerWriter | ||
* @throws {@link IllegalArgumentException} if headerWriter is null. | ||
*/ | ||
public HeaderWriterLogoutHandler(HeaderWriter headerWriter) { | ||
Assert.notNull(headerWriter, "headerWriter cannot be null."); | ||
this.headerWriter = headerWriter; | ||
} | ||
|
||
@Override | ||
public void logout(HttpServletRequest request, HttpServletResponse response, | ||
Authentication authentication) { | ||
this.headerWriter.writeHeaders(request, response); | ||
} | ||
} |
101 changes: 101 additions & 0 deletions
101
.../main/java/org/springframework/security/web/header/writers/ClearSiteDataHeaderWriter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/* | ||
* 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 | ||
* | ||
* 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.header.writers; | ||
|
||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
import javax.servlet.http.HttpServletRequest; | ||
import javax.servlet.http.HttpServletResponse; | ||
|
||
import org.apache.commons.logging.Log; | ||
import org.apache.commons.logging.LogFactory; | ||
import org.springframework.security.web.header.HeaderWriter; | ||
import org.springframework.security.web.util.matcher.RequestMatcher; | ||
import org.springframework.util.Assert; | ||
|
||
/** | ||
* Provides support for <a href="https://w3c.github.io/webappsec-clear-site-data/">Clear | ||
* Site Data</a>. | ||
* | ||
* <p> | ||
* Developers may instruct a user agent to clear various types of relevant data by delivering | ||
* a Clear-Site-Data HTTP response header in response to a request. | ||
* <p> | ||
* | ||
* <p> | ||
* Due to <a href="https://w3c.github.io/webappsec-clear-site-data/#incomplete">Incomplete Clearing</a> | ||
* section the header is only applied if the request is secure. | ||
* </p> | ||
* | ||
* @author Rafiullah Hamedy | ||
* @since 5.2 | ||
*/ | ||
public final class ClearSiteDataHeaderWriter implements HeaderWriter { | ||
|
||
private static final String CLEAR_SITE_DATA_HEADER = "Clear-Site-Data"; | ||
|
||
private final Log logger = LogFactory.getLog(getClass()); | ||
|
||
private final RequestMatcher requestMatcher; | ||
|
||
private String headerValue; | ||
|
||
/** | ||
* <p> | ||
* Creates a new instance of {@link ClearSiteDataHeaderWriter} with given sources. | ||
* The constructor also initializes <b>requestMatcher</b> with a new instance of | ||
* <b>SecureRequestMatcher</b> to ensure that header is only applied if and when | ||
* the request is secure as per the <b>Incomplete Clearing</b> section. | ||
* </p> | ||
* | ||
* @param sources (i.e. "cache", "cookies", "storage", "executionContexts" or "*") | ||
* @throws {@link IllegalArgumentException} if sources is null or empty. | ||
*/ | ||
public ClearSiteDataHeaderWriter(String ...sources) { | ||
Assert.notEmpty(sources, "Sources cannot be empty or null."); | ||
this.requestMatcher = new SecureRequestMatcher(); | ||
this.headerValue = Stream.of(sources).map(this::quote).collect(Collectors.joining(", ")); | ||
} | ||
|
||
@Override | ||
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { | ||
if (this.requestMatcher.matches(request)) { | ||
if (!response.containsHeader(CLEAR_SITE_DATA_HEADER)) { | ||
response.setHeader(CLEAR_SITE_DATA_HEADER, this.headerValue); | ||
} | ||
} else if (logger.isDebugEnabled()) { | ||
logger.debug("Not injecting Clear-Site-Data header since it did not match the " | ||
+ "requestMatcher " + this.requestMatcher); | ||
} | ||
} | ||
|
||
private static final class SecureRequestMatcher implements RequestMatcher { | ||
public boolean matches(HttpServletRequest request) { | ||
return request.isSecure(); | ||
} | ||
} | ||
|
||
private String quote(String source) { | ||
return "\"" + source + "\""; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return getClass().getName() + " [headerValue=" + this.headerValue + "]"; | ||
} | ||
} |
104 changes: 104 additions & 0 deletions
104
...rg/springframework/security/web/authentication/logout/HeaderWriterLogoutHandlerTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
/* | ||
* 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 | ||
* | ||
* 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.authentication.logout; | ||
|
||
import org.junit.Before; | ||
import org.junit.Rule; | ||
import org.junit.Test; | ||
import org.junit.rules.ExpectedException; | ||
import org.springframework.mock.web.MockHttpServletRequest; | ||
import org.springframework.mock.web.MockHttpServletResponse; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.mockito.Mockito.mock; | ||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; | ||
|
||
/** | ||
* | ||
* @author Rafiullah Hamedy | ||
* | ||
* @see {@link HeaderWriterLogoutHandler} | ||
*/ | ||
public class HeaderWriterLogoutHandlerTests { | ||
private static final String HEADER_NAME = "Clear-Site-Data"; | ||
|
||
private MockHttpServletResponse response; | ||
private MockHttpServletRequest request; | ||
|
||
@Rule | ||
public ExpectedException thrown = ExpectedException.none(); | ||
|
||
@Before | ||
public void setup() { | ||
this.response = new MockHttpServletResponse(); | ||
this.request = new MockHttpServletRequest(); | ||
} | ||
|
||
@Test | ||
public void createInstanceWhenHeaderWriterIsNullThenThrowsException() { | ||
this.thrown.expect(IllegalArgumentException.class); | ||
this.thrown.expectMessage("headerWriter cannot be null."); | ||
|
||
new HeaderWriterLogoutHandler(null); | ||
} | ||
|
||
@Test | ||
public void createInstanceWhenSourceIsNullThenThrowsException() { | ||
this.thrown.expect(IllegalArgumentException.class); | ||
this.thrown.expectMessage("Sources cannot be empty or null."); | ||
|
||
new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter()); | ||
} | ||
|
||
@Test | ||
public void logoutWhenRequestIsNotSecureThenHeaderIsNotPresent() { | ||
HeaderWriterLogoutHandler handler = new HeaderWriterLogoutHandler( | ||
new ClearSiteDataHeaderWriter("cache")); | ||
|
||
handler.logout(request, response, mock(Authentication.class)); | ||
|
||
assertThat(header().doesNotExist(HEADER_NAME)); | ||
} | ||
|
||
@Test | ||
public void logoutWhenRequestIsSecureThenHeaderIsPresentMatchesWildCardSource() { | ||
HeaderWriterLogoutHandler handler = new HeaderWriterLogoutHandler( | ||
new ClearSiteDataHeaderWriter("*")); | ||
|
||
this.request.setSecure(true); | ||
|
||
handler.logout(request, response, mock(Authentication.class)); | ||
|
||
assertThat(header().stringValues(HEADER_NAME, "\"*\"")); | ||
} | ||
|
||
@Test | ||
public void logoutWhenRequestIsSecureThenHeaderValueMatchesSource() { | ||
HeaderWriterLogoutHandler handler = new HeaderWriterLogoutHandler( | ||
new ClearSiteDataHeaderWriter("cache", "cookies", "storage", | ||
"executionContexts")); | ||
|
||
this.request.setSecure(true); | ||
|
||
handler.logout(request, response, mock(Authentication.class)); | ||
|
||
assertThat(header().stringValues(HEADER_NAME, "\"cache\", \"cookies\", \"storage\", " | ||
+ "\"executionContexts\"")); | ||
} | ||
} |
Oops, something went wrong.