[java-support] branch master updated: IDP-1476 - Need a filter to append SameSite to cookies
Scott Cantor
cantor.2 at osu.edu
Tue Sep 10 12:40:56 EDT 2019
This is an automated email from the git hooks/post-receive script.
scantor pushed a commit to branch master
in repository java-support.
View the commit online:
http://git.shibboleth.net/view/?p=java-support.git;a=commit;h=da7c80c01ab4a6d123f2cfae9aa957dd0a4e63e5
The following commit(s) were added to refs/heads/master by this push:
new da7c80c IDP-1476 - Need a filter to append SameSite to cookies
da7c80c is described below
commit da7c80c01ab4a6d123f2cfae9aa957dd0a4e63e5
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Sep 10 12:40:58 2019 -0400
IDP-1476 - Need a filter to append SameSite to cookies
https://issues.shibboleth.net/jira/browse/IDP-1476
---
.../support/net/SameSiteCookieHeaderFilter.java | 319 ++++++++++++++
.../net/SameSiteCookieHeaderFilterTest.java | 475 +++++++++++++++++++++
2 files changed, 794 insertions(+)
diff --git a/src/main/java/net/shibboleth/utilities/java/support/net/SameSiteCookieHeaderFilter.java b/src/main/java/net/shibboleth/utilities/java/support/net/SameSiteCookieHeaderFilter.java
new file mode 100644
index 0000000..97e0a2c
--- /dev/null
+++ b/src/main/java/net/shibboleth/utilities/java/support/net/SameSiteCookieHeaderFilter.java
@@ -0,0 +1,319 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 net.shibboleth.utilities.java.support.net;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.HttpCookie;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.net.HttpHeaders;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+
+/**
+ * Implementation of an HTTP servlet {@link Filter} which adds the SameSite attribute to cookies, until
+ * the Java API supports it natively, if ever.
+ *
+ * <p>Explicitly named cookies are configured and placed into a Map of cookie name to same-site attribute value.</p>
+ *
+ * <p>All other cookies may be assigned a default value.</p>
+ *
+ * <p>Cookies with an existing same-site cookie flag are left unaltered - copied back into the response
+ * without modification.</p>
+ *
+ * <p>A single cookie can only have at most one same-site value set. Attempts in the configuration to
+ * give more than one same-site value to a cookie are caught during argument injection and throw an
+ * {@link IllegalArgumentException}.</p>
+ *
+ */
+public class SameSiteCookieHeaderFilter implements Filter {
+
+ /** The name of the same-site cookie attribute.*/
+ @Nonnull @NotEmpty private static final String SAMESITE_ATTRIBITE_NAME="SameSite";
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(SameSiteCookieHeaderFilter.class);
+
+ /** The allowed same-site cookie attribute values.*/
+ public enum SameSiteValue{
+
+ /**
+ * Send the cookie for 'same-site' requests only.
+ */
+ Strict("Strict"),
+ /**
+ * Send the cookie for 'same-site' requests along with 'cross-site' top
+ * level navigations using safe HTTP methods (GET, HEAD, OPTIONS, and TRACE).
+ */
+ Lax("Lax"),
+ /**
+ * Send the cookie for 'same-site' and 'cross-site' requests.
+ */
+ None("None");
+
+ /** The same-site attribute value.*/
+ @Nonnull @NotEmpty private String value;
+
+ /**
+ * Constructor.
+ *
+ * @param attrValue the same-site attribute value.
+ */
+ private SameSiteValue(@Nonnull @NotEmpty final String attrValue) {
+ value = Constraint.isNotEmpty(attrValue, "the same-site attribute value can not be empty");
+ }
+
+ /**
+ * Get the same-site attribute value.
+ *
+ * @return Returns the value.
+ */
+ public String getValue() {
+ return value;
+ }
+
+ }
+
+ /** Optional default value to apply. */
+ @Nullable private SameSiteValue defaultValue;
+
+ /** Map of cookie name to same-site attribute value.*/
+ @Nonnull @NonnullElements private Map<String,SameSiteValue> sameSiteCookies;
+
+ /** Constructor. */
+ public SameSiteCookieHeaderFilter() {
+ sameSiteCookies = Collections.emptyMap();
+ }
+
+ /**
+ * Set an optional default value to apply to all unmapped cookies.
+ *
+ * @param value default value
+ */
+ public void setDefaultValue(@Nullable final SameSiteValue value) {
+ defaultValue = value;
+ }
+
+ /**
+ * Set the names of cookies to add the same-site attribute to.
+ *
+ * <p>The argument map is flattened to remove the nested collection. The argument map allows duplicate
+ * cookie names to appear in order to detect configuration errors which would otherwise not be found during
+ * argument injection e.g. trying to set a session identifier cookie as both SameSite=Strict and SameSite=None.
+ * Instead, duplicates are detected here, throwing a terminating {@link IllegalArgumentException} if found.</p>
+ *
+ * @param map the map of same-site attribute values to cookie names.
+ */
+ public void setSameSiteCookies(@Nullable @NonnullElements final Map<SameSiteValue,List<String>> map) {
+ if (map != null) {
+ sameSiteCookies = new HashMap<>(4);
+ for (final Map.Entry<SameSiteValue,List<String>> entry : map.entrySet()) {
+
+ for (final String cookieName : entry.getValue()) {
+ if (sameSiteCookies.get(cookieName) != null) {
+ log.error("Duplicate cookie name '{}' found in SameSite cookie map, "
+ + "please check configuration.",cookieName);
+ throw new IllegalArgumentException("Duplicate cookie name found in SameSite cookie map");
+ }
+ final String trimmedName = StringSupport.trimOrNull(cookieName);
+ if (trimmedName != null) {
+ sameSiteCookies.put(cookieName, entry.getKey());
+ }
+ }
+ }
+ } else {
+ sameSiteCookies = Collections.emptyMap();
+ }
+
+ }
+
+ /** {@inheritDoc} */
+ public void init(@Nonnull final FilterConfig filterConfig) throws ServletException {
+ }
+
+ /** {@inheritDoc} */
+ public void destroy() {
+ }
+
+ /** {@inheritDoc} */
+ public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
+ throws IOException, ServletException {
+
+ if (!(response instanceof HttpServletResponse)) {
+ throw new ServletException("Response is not an instance of HttpServletResponse");
+ }
+
+ chain.doFilter(request, new SameSiteResponseProxy((HttpServletResponse)response));
+
+ }
+
+ /**
+ * An implementation of the {@link HttpServletResponse} which adds the same-site flag to {@literal Set-Cookie}
+ * headers for the set of configured cookies.
+ */
+ private class SameSiteResponseProxy extends HttpServletResponseWrapper{
+
+ /** The response. */
+ @Nonnull private final HttpServletResponse response;
+
+ /**
+ * Constructor.
+ *
+ * @param resp the response to delegate to
+ */
+ public SameSiteResponseProxy(@Nonnull final HttpServletResponse resp) {
+ super(resp);
+ response = resp;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void sendError(final int sc) throws IOException {
+ appendSameSite();
+ super.sendError(sc);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public PrintWriter getWriter() throws IOException {
+ appendSameSite();
+ return super.getWriter();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void sendError(final int sc, final String msg) throws IOException {
+ appendSameSite();
+ super.sendError(sc, msg);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void sendRedirect(final String location) throws IOException {
+ appendSameSite();
+ super.sendRedirect(location);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ServletOutputStream getOutputStream() throws IOException {
+ appendSameSite();
+ return super.getOutputStream();
+ }
+
+ /**
+ * Add the SameSite attribute to those cookies configured in the {@code sameSiteCookies} map iff
+ * they do not already contain the same-site flag. All other cookies are copied over to the response
+ * without modification.
+ * */
+ private void appendSameSite() {
+
+ final Collection<String> cookieheaders = response.getHeaders(HttpHeaders.SET_COOKIE);
+
+ boolean firstHeader = true;
+ for (final String cookieHeader : cookieheaders) {
+
+ if (StringSupport.trimOrNull(cookieHeader)==null) {
+ continue;
+ }
+
+ List<HttpCookie> parsedCookies = null;
+ try {
+ //this parser only parses name and value, we only need the name.
+ parsedCookies = HttpCookie.parse(cookieHeader);
+ } catch(final IllegalArgumentException e) {
+ // Should not get here
+ log.trace("Cookie header '{}' violates the cookie specification and will be ignored", cookieHeader);
+ }
+
+ if (parsedCookies == null || parsedCookies.size() != 1) {
+ // Should be one cookie since we're only looking at Set-Cookie, not Set-Cookie2.
+ continue;
+ }
+
+ final SameSiteValue sameSiteValue = sameSiteCookies.get(parsedCookies.get(0).getName());
+ if (sameSiteValue != null) {
+ appendSameSiteAttribute(cookieHeader, sameSiteValue.getValue(), firstHeader);
+ } else if (defaultValue != null) {
+ appendSameSiteAttribute(cookieHeader, defaultValue.getValue(), firstHeader);
+ } else {
+ // Copy it over unaltered.
+ if (firstHeader) {
+ response.setHeader(HttpHeaders.SET_COOKIE, cookieHeader);
+ } else {
+ response.addHeader(HttpHeaders.SET_COOKIE, cookieHeader);
+ }
+ }
+ firstHeader=false;
+
+ }
+ }
+
+ /**
+ * Append the SameSite cookie attribute with the specified samesite-value to the {@code cookieHeader}
+ * iff it does not already have one set.
+ *
+ * @param cookieHeader the cookie header value
+ * @param sameSiteValue the SameSite attribute value e.g. None, Lax, or Strict
+ * @param first true iff this is the first Set-Cookie header
+ */
+ private void appendSameSiteAttribute(@Nonnull @NotEmpty final String cookieHeader,
+ @Nonnull @NotEmpty final String sameSiteValue,
+ @Nonnull final boolean first) {
+
+ String sameSiteSetCookieValue = cookieHeader;
+
+ //only add if does not already exist, else leave
+ if (!cookieHeader.contains(SAMESITE_ATTRIBITE_NAME)) {
+ sameSiteSetCookieValue = String.format("%s; %s", cookieHeader,
+ SAMESITE_ATTRIBITE_NAME + "=" + sameSiteValue);
+ }
+
+ if (first) {
+ response.setHeader(HttpHeaders.SET_COOKIE,sameSiteSetCookieValue);
+ } else {
+ response.addHeader(HttpHeaders.SET_COOKIE, sameSiteSetCookieValue);
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/net/shibboleth/utilities/java/support/net/SameSiteCookieHeaderFilterTest.java b/src/test/java/net/shibboleth/utilities/java/support/net/SameSiteCookieHeaderFilterTest.java
new file mode 100644
index 0000000..5214952
--- /dev/null
+++ b/src/test/java/net/shibboleth/utilities/java/support/net/SameSiteCookieHeaderFilterTest.java
@@ -0,0 +1,475 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 net.shibboleth.utilities.java.support.net;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.net.HttpCookie;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.Filter;
+import javax.servlet.Servlet;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.mock.web.MockCookie;
+import org.springframework.mock.web.MockFilterChain;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.net.HttpHeaders;
+
+import net.shibboleth.utilities.java.support.net.SameSiteCookieHeaderFilter.SameSiteValue;
+
+/**
+ * Tests for {@link SameSiteCookieHeaderFilter}.
+ *
+ * TODO: tests depend on a fixed spring-test module expected in 5.2
+ */
+public class SameSiteCookieHeaderFilterTest {
+
+ private MockHttpServletRequest request;
+
+ private MockHttpServletResponse response;
+
+ @BeforeMethod public void setUp() {
+ MockHttpServletRequest mockRequest = new MockHttpServletRequest();
+ mockRequest.setMethod("POST");
+ mockRequest.setRequestURI("/foo");
+ request = mockRequest;
+
+ MockHttpServletResponse mockResponse = new MockHttpServletResponse();
+ mockResponse.addHeader(HttpHeaders.SET_COOKIE,
+ "JSESSIONID=jyohu8ttc3dp1g3yqe8g8ff7y;Path=/idp;Secure;HttpOnly");
+ mockResponse.addHeader(HttpHeaders.SET_COOKIE,
+ "shib_idp_session_ss=AAdzZWNyZXQyzL1Rzi9ROe3%2BGk%2B6%2B;Path=/idp;HttpOnly");
+ mockResponse.addHeader(HttpHeaders.SET_COOKIE,
+ "shib_idp_session=8ee460bc0b3695c477b2b5f3e192ddf7297baa7ee01bd2bcf24695f8c21cb3a2;Path=/idp;HttpOnly");
+ //add a cookie with existing SameSite value - should ignore and copy over.
+ mockResponse.addHeader(HttpHeaders.SET_COOKIE,
+ "existing_same_site=already-same-site;Path=/idp;HttpOnly;SameSite=None");
+ //ignore this, copy it over as is.
+ mockResponse.addHeader(HttpHeaders.SET_COOKIE,
+ "ignore_copy_over=copy-over;Path=/idp;HttpOnly");
+
+ response = mockResponse;
+ }
+
+ @AfterMethod public void tearDown() {
+ HttpServletRequestResponseContext.clearCurrent();
+ }
+
+ /** Test a null init value, which should not trigger an exception.*/
+ @Test(enabled=false) public void testNullInitValues() {
+ SameSiteCookieHeaderFilter filter = new SameSiteCookieHeaderFilter();
+ filter.setSameSiteCookies(null);
+ }
+
+ /** Test an empty cookie name is not added to the internal map.*/
+ @Test(enabled=false) public void testEmptyCookieNameInitValue() {
+ SameSiteCookieHeaderFilter filter = new SameSiteCookieHeaderFilter();
+ Map<SameSiteValue,List<String>> cookies = new HashMap<>();
+ List<String> noneCookies = Arrays.asList(new String[] {""});
+ cookies.put(SameSiteValue.None, noneCookies);
+ filter.setSameSiteCookies(cookies);
+
+ testSameSiteMapSize("sameSiteCookies", 0, filter);
+ }
+
+ /** Test the correct number of cookies are added to the internal filter cookie map.*/
+ @Test(enabled=false) public void testInitValues() {
+
+ SameSiteCookieHeaderFilter filter = new SameSiteCookieHeaderFilter();
+ Map<SameSiteValue,List<String>> cookies = new HashMap<>();
+ List<String> noneCookies = Arrays.asList(new String[] {"JSESSIONID","shib_idp_session","shib_idp_session_ss","existing_same_site"});
+ List<String> laxCookies = Arrays.asList(new String[] {"another-cookie-lax"});
+ List<String> strictCookies = Arrays.asList(new String[] {"another-cookie-strict"});
+ cookies.put(SameSiteValue.None, noneCookies);
+ cookies.put(SameSiteValue.Lax, laxCookies);
+ cookies.put(SameSiteValue.Strict, strictCookies);
+ filter.setSameSiteCookies(cookies);
+
+ testSameSiteMapSize("sameSiteCookies", 6, filter);
+ }
+
+ /** Test failure on duplicated cookie names*/
+ @Test(enabled=false,expectedExceptions=IllegalArgumentException.class) public void testDuplicateInitValues() {
+
+ SameSiteCookieHeaderFilter filter = new SameSiteCookieHeaderFilter();
+ Map<SameSiteValue,List<String>> cookies = new HashMap<>();
+ List<String> noneCookies = Arrays.asList(new String[] {"JSESSIONID","shib_idp_session","shib_idp_session_ss","existing_same_site"});
+ List<String> laxCookies = Arrays.asList(new String[] {"JSESSIONID"});
+ cookies.put(SameSiteValue.None, noneCookies);
+ cookies.put(SameSiteValue.Lax, laxCookies);
+ filter.setSameSiteCookies(cookies);
+ }
+
+ /** Test empty SameSite cookie map, which should not trigger an exception, and just copy over the
+ * existing cookies. */
+ @Test(enabled=false) public void testEmptySameSiteCookieMap() throws IOException, ServletException {
+
+ SameSiteCookieHeaderFilter filter = new SameSiteCookieHeaderFilter();
+ filter.setSameSiteCookies(null);
+
+ Servlet redirectServlet = new TestRedirectServlet();
+ MockFilterChain mockRedirectChain = new MockFilterChain(redirectServlet, filter);
+
+ mockRedirectChain.doFilter(request, response);
+
+ Assert.assertTrue(mockRedirectChain.getResponse() instanceof MockHttpServletResponse);
+
+ final Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
+
+ Assert.assertEquals(headers.size(), 5);
+ }
+
+ /** Test empty SameSite cookie map, which should not trigger an exception, and should apply
+ * a default. */
+ @Test(enabled=false) public void testEmptySameSiteCookieMapWithDefault() throws IOException, ServletException {
+
+ SameSiteCookieHeaderFilter filter = new SameSiteCookieHeaderFilter();
+ filter.setSameSiteCookies(null);
+ filter.setDefaultValue(SameSiteValue.Strict);
+
+ Servlet redirectServlet = new TestRedirectServlet();
+ MockFilterChain mockRedirectChain = new MockFilterChain(redirectServlet, filter);
+
+ mockRedirectChain.doFilter(request, response);
+
+ Assert.assertTrue(mockRedirectChain.getResponse() instanceof MockHttpServletResponse);
+
+ final Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
+
+ Assert.assertEquals(headers.size(), 5);
+ testExpectedHeadersInResponse(SameSiteValue.Strict.getValue(),
+ (MockHttpServletResponse)mockRedirectChain.getResponse(),
+ Arrays.asList(new String[] {"JSESSIONID","shib_idp_session","shib_idp_session_ss", "ignore_copy_over"}),
+ Collections.emptyList(), 5);
+ }
+
+ /** Test the samesite filter works correctly with None values when a redirect response is issued. */
+ @Test(enabled=false) public void testRedirectResponseSameSiteNone() throws IOException, ServletException {
+
+ SameSiteCookieHeaderFilter filter = new SameSiteCookieHeaderFilter();
+ Map<SameSiteValue,List<String>> cookies = new HashMap<>();
+ List<String> noneCookies = Arrays.asList(new String[] {"JSESSIONID","shib_idp_session","shib_idp_session_ss","existing_same_site"});
+ cookies.put(SameSiteValue.None, noneCookies);
+ filter.setSameSiteCookies(cookies);
+
+ Servlet redirectServlet = new TestRedirectServlet();
+ MockFilterChain mockRedirectChain = new MockFilterChain(redirectServlet, filter);
+
+ mockRedirectChain.doFilter(request, response);
+
+ Assert.assertTrue(mockRedirectChain.getResponse() instanceof MockHttpServletResponse);
+
+ testExpectedHeadersInResponse("None",(MockHttpServletResponse)mockRedirectChain.getResponse(),
+ Arrays.asList(new String[] {"JSESSIONID","shib_idp_session","shib_idp_session_ss","existing_same_site"}),
+ Arrays.asList(new String[] {"ignore_copy_over"}),5);
+ }
+
+ /** Test the samesite filter works correctly with None values when a redirect response is issued. */
+ @Test(enabled=false) public void testRedirectResponseSameSiteNoneWithDefault() throws IOException, ServletException {
+
+ SameSiteCookieHeaderFilter filter = new SameSiteCookieHeaderFilter();
+ Map<SameSiteValue,List<String>> cookies = new HashMap<>();
+ List<String> noneCookies = Arrays.asList(new String[] {"shib_idp_session","shib_idp_session_ss","existing_same_site"});
+ cookies.put(SameSiteValue.None, noneCookies);
+ filter.setSameSiteCookies(cookies);
+ filter.setDefaultValue(SameSiteValue.None);
+
+ Servlet redirectServlet = new TestRedirectServlet();
+ MockFilterChain mockRedirectChain = new MockFilterChain(redirectServlet, filter);
+
+ mockRedirectChain.doFilter(request, response);
+
+ Assert.assertTrue(mockRedirectChain.getResponse() instanceof MockHttpServletResponse);
+
+ testExpectedHeadersInResponse("None",(MockHttpServletResponse)mockRedirectChain.getResponse(),
+ Arrays.asList(new String[] {"JSESSIONID","shib_idp_session","shib_idp_session_ss","existing_same_site","ignore_copy_over"}),
+ Collections.emptyList(), 5);
+ }
+
+ /** Test the samesite filter works correctly with Lax values when a redirect response is issued. */
+ @Test(enabled=false) public void testRedirectResponseSameSiteLax() throws IOException, ServletException {
+
+ SameSiteCookieHeaderFilter filter = new SameSiteCookieHeaderFilter();
+ Map<SameSiteValue,List<String>> cookies = new HashMap<>();
+ List<String> noneCookies = Arrays.asList(new String[] {"JSESSIONID","shib_idp_session","shib_idp_session_ss"});
+ cookies.put(SameSiteValue.Lax, noneCookies);
+ filter.setSameSiteCookies(cookies);
+
+ Servlet redirectServlet = new TestRedirectServlet();
+ MockFilterChain mockRedirectChain = new MockFilterChain(redirectServlet, filter);
+
+ mockRedirectChain.doFilter(request, response);
+
+ Assert.assertTrue(mockRedirectChain.getResponse() instanceof MockHttpServletResponse);
+
+ //as "existing_same_site" is None, ignore it here.
+ testExpectedHeadersInResponse("Lax",(MockHttpServletResponse)mockRedirectChain.getResponse(),
+ Arrays.asList(new String[] {"JSESSIONID","shib_idp_session","shib_idp_session_ss"}),
+ Arrays.asList(new String[] {"ignore_copy_over"}),5);
+ }
+
+ /** Test the samesite filter works correctly with Strict values when a redirect response is issued. */
+ @Test(enabled=false) public void testRedirectResponseSameSiteStrict() throws IOException, ServletException {
+
+ SameSiteCookieHeaderFilter filter = new SameSiteCookieHeaderFilter();
+ Map<SameSiteValue,List<String>> cookies = new HashMap<>();
+ List<String> noneCookies = Arrays.asList(new String[] {"JSESSIONID","shib_idp_session","shib_idp_session_ss"});
+ cookies.put(SameSiteValue.Strict, noneCookies);
+ filter.setSameSiteCookies(cookies);
+
+ Servlet redirectServlet = new TestRedirectServlet();
+ MockFilterChain mockRedirectChain = new MockFilterChain(redirectServlet, filter);
+
+ mockRedirectChain.doFilter(request, response);
+
+ Assert.assertTrue(mockRedirectChain.getResponse() instanceof MockHttpServletResponse);
+
+ //as "existing_same_site" is None, ignore it here.
+ testExpectedHeadersInResponse("Strict",(MockHttpServletResponse)mockRedirectChain.getResponse(),
+ Arrays.asList(new String[] {"JSESSIONID","shib_idp_session","shib_idp_session_ss"}),
+ Arrays.asList(new String[] {"ignore_copy_over"}),5);
+ }
+
+ /** Test the samesite filter works correctly when an output stream is written to and flushed. */
+ @Test(enabled=false) public void testGetOutputStreamResponse() throws IOException, ServletException {
+
+ SameSiteCookieHeaderFilter filter = new SameSiteCookieHeaderFilter();
+ Map<SameSiteValue,List<String>> cookies = new HashMap<>();
+ List<String> noneCookies = Arrays.asList(new String[] {"JSESSIONID","shib_idp_session","shib_idp_session_ss","existing_same_site"});
+ cookies.put(SameSiteValue.None, noneCookies);
+ filter.setSameSiteCookies(cookies);
+
+ Servlet outputStreamServlet = new TestOutputStreamServlet();
+ MockFilterChain mockRedirectChain = new MockFilterChain(outputStreamServlet, filter);
+
+ mockRedirectChain.doFilter(request, response);
+
+ Assert.assertTrue(mockRedirectChain.getResponse() instanceof MockHttpServletResponse);
+
+ testExpectedHeadersInResponse("None",(MockHttpServletResponse)mockRedirectChain.getResponse(),
+ Arrays.asList(new String[] {"JSESSIONID","shib_idp_session","shib_idp_session_ss","existing_same_site"}),
+ Arrays.asList(new String[] {"ignore_copy_over"}),5);
+ }
+
+ /** Test the samesite filter works correctly when the response print writer is written to and closed.*/
+ @Test(enabled=false) public void testPrintWriterResponse() throws IOException, ServletException {
+
+ SameSiteCookieHeaderFilter filter = new SameSiteCookieHeaderFilter();
+ Map<SameSiteValue,List<String>> cookies = new HashMap<>();
+ List<String> noneCookies = Arrays.asList(new String[] {"JSESSIONID","shib_idp_session","shib_idp_session_ss","existing_same_site"});
+ cookies.put(SameSiteValue.None, noneCookies);
+ filter.setSameSiteCookies(cookies);
+
+ Servlet printWriterServlet = new TestPrintWriterServlet();
+ MockFilterChain mockRedirectChain = new MockFilterChain(printWriterServlet, filter);
+
+ mockRedirectChain.doFilter(request, response);
+
+ Assert.assertTrue(mockRedirectChain.getResponse() instanceof MockHttpServletResponse);
+
+ testExpectedHeadersInResponse("None",(MockHttpServletResponse)mockRedirectChain.getResponse(),
+ Arrays.asList(new String[] {"JSESSIONID","shib_idp_session","shib_idp_session_ss","existing_same_site"}),
+ Arrays.asList(new String[] {"ignore_copy_over"}),5);
+ }
+
+ /**
+ * Get the field from the filter (even if private), check the field is of type {@link Set}, and compare
+ * the size of the set to the expected size.
+ *
+ * @param fieldName the name of the field on the object of type {@link Map}.
+ * @param expectedSize the expected size of the map.
+ * @param filter the filter with the field to get.
+ */
+ private void testSameSiteMapSize(String fieldName, int expectedSize, Filter filter) {
+
+ Object sameSiteSet = ReflectionTestUtils.getField(filter, fieldName);
+ Assert.assertNotNull(sameSiteSet);
+ Assert.assertTrue(sameSiteSet instanceof Map);
+ Assert.assertEquals(((Map<?,?>)sameSiteSet).size(),expectedSize);
+ }
+
+ /**
+ * Test the Set-Cookie headers in the response contain the {@literal SameSite=<sameSiteValue>} attribute if they are named
+ * in the {@code cookiesWithSamesite} list, and do not if named in the {@code cookiesWithoutSameSite} list.
+ * <p>
+ * Also checks the number of Set-Cookie headers matches {@code numberOfHeaders}. This makes sure the filter
+ * is not adding or removing headers during operation - it should only ever append the SameSite attribute
+ * to existing cookies.
+ * </p>
+ *
+ * @param sameSiteValue the value of samesite to check for.
+ * @param response the http servlet response.
+ * @param cookiesWithSamesite the list of cookies that should have the {@literal SameSite=None} attribute set.
+ * @param cookiesWithoutSameSite the list of cookies that should not have the {@literal SameSite} attribute set.
+ * @param numberOfHeaders the number of Set-Cookie headers expected in the response.
+ */
+ private void testExpectedHeadersInResponse(final String sameSiteValue, final MockHttpServletResponse response,
+ final List<String> cookiesWithSamesite, final List<String> cookiesWithoutSameSite, final int numberOfHeaders) {
+
+ final Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
+
+ Assert.assertEquals(headers.size(), numberOfHeaders);
+
+ for (String header : headers) {
+
+ List<HttpCookie> cookies = HttpCookie.parse(header);
+ Assert.assertNotNull(cookies);
+ Assert.assertTrue(cookies.size()==1);
+ Cookie cookie = response.getCookie(cookies.get(0).getName());
+ Assert.assertNotNull(cookie);
+ Assert.assertTrue(cookie instanceof MockCookie);
+ MockCookie mockCookie = (MockCookie)cookie;
+
+ if (cookiesWithSamesite.contains(mockCookie.getName())) {
+ Assert.assertNotNull(mockCookie.getSameSite());
+ Assert.assertEquals(mockCookie.getSameSite(),sameSiteValue);
+
+ }
+ else if (cookiesWithoutSameSite.contains(mockCookie.getName())) {
+ Assert.assertNull(mockCookie.getSameSite());
+
+ }
+ }
+ }
+
+ /**
+ * Servlet that initiates a redirect on the response.
+ */
+ public class TestRedirectServlet implements Servlet {
+
+ /** {@inheritDoc} */
+ public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
+ Assert.assertNotNull(req, "HttpServletRequest was null");
+ Assert.assertNotNull(res, "HttpServletResponse was null");
+ ((HttpServletResponse) res).sendRedirect("/redirect");
+ }
+
+ /** {@inheritDoc} */
+ public void init(ServletConfig config) throws ServletException {
+ }
+
+ /** {@inheritDoc} */
+ public ServletConfig getServletConfig() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getServletInfo() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public void destroy() {
+ }
+
+ }
+
+ /**
+ * Servlet that opens an output stream on the response.
+ */
+ public class TestOutputStreamServlet implements Servlet {
+
+ /** {@inheritDoc} */
+ public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
+ Assert.assertNotNull(req, "HttpServletRequest was null");
+ Assert.assertNotNull(res, "HttpServletResponse was null");
+
+ // write nothing to the output stream.
+ final Writer out = new OutputStreamWriter(((HttpServletResponse) res).getOutputStream(), "UTF-8");
+ out.flush();
+
+ }
+
+ /** {@inheritDoc} */
+ public void init(ServletConfig config) throws ServletException {
+ }
+
+ /** {@inheritDoc} */
+ public ServletConfig getServletConfig() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getServletInfo() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public void destroy() {
+ }
+
+ }
+
+ /**
+ * Servlet that opens a print writer on the response.
+ */
+ public class TestPrintWriterServlet implements Servlet {
+
+ /** {@inheritDoc} */
+ public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
+ Assert.assertNotNull(req, "HttpServletRequest was null");
+ Assert.assertNotNull(res, "HttpServletResponse was null");
+
+ // write nothing to the print writer.
+ PrintWriter writer = ((HttpServletResponse) res).getWriter();
+ writer.flush();
+
+ }
+
+ /** {@inheritDoc} */
+ public void init(ServletConfig config) throws ServletException {
+ }
+
+ /** {@inheritDoc} */
+ public ServletConfig getServletConfig() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public String getServletInfo() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public void destroy() {
+ }
+
+ }
+
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list