[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