Webkit based browsers on Mac (safari) and iOS (safair, chome, firefox etc) are currently affected by a bug that treats SameSite=None or SameSite=nonesense cookies as SameSite=Strict (https://bugs.webkit.org/show_bug.cgi?id=198181). We believe the fix for this will only take effect from MacOS 10.15 and iOS 13. Consequently, any attempt to maintain the current functional behaviour of cookies by setting SameSite=None on unpatched versions of Webkit will break SSO. |
Following on from IdP SameSite Testing, here we describe a new Servlet Filter (SameSiteSessionCookieFilter
) for appending the same-site cookie flag to specified cookies. The SameSiteSessionCookieFilter
wraps the HttpResponse with a SameSiteResponseProxy
proxy. The proxy overrides the getWriter
, sendError
, getOutputStream
, and sendRedirect Response
methods such that any attempt from a Servlet to commit a response back to the client invokes the 'append same site attribute' logic over the current set of Set-Cookie
headers.
Affected cookies are specified by injecting a set of cookie names into an internal map using XML-based spring bean configuration. Each cookie name is related to a key, the key corresponds to the same-site attribute value to set e.g. one-of {none,lax,strict}. Any Set-Cookie
header that already contains a same-site cookie flag is not affected, even if configured differently in the filters internal same-site map. Any cookie in a Set-Cookie
header that is not contained in the filters internal same-site map is ignored - left as is.
The basic algorithm is:
sameSiteCookies
map.The implementation can be found on my personal repository [git@git.shibboleth.net:philsmart/java-support] feature branch [feature/same-site-filter].
DynamicResponseHeaderFilter
callback function, encapsulating the same logic as the filter described here. You would, however, need to make sure the DynamicResponseHeaderFilter
was ordered before the CookieBufferingFilter
in the web.xml
- which it is not currently.idp.cookie.sameSite.none=JSESSIONID,shib_idp_session.
The Filter is wrapped inside a spring DelegatingFilterProxy
in order to simplify argument injection by using spring bean configuration. The filter must be specified in the web.xml e.g.:
<filter> <filter-name>SameSiteSessionCookieFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetBeanName</param-name> <param-value>shibboleth.SameSiteCookieHeaderFilter</param-value> </init-param> </filter> |
The filter-mapping must be registered before the CookieBufferingFilter
otherwise the Set-Cookie headers are not dumped into the real response when the SameSiteSessionCookieFilter
filter is run. e.g.
<filter-mapping> <filter-name>SameSiteSessionCookieFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>CookieBufferingFilter</filter-name> ... |
The spring SameSiteCookieHeaderFilter
bean is then defined in the global-system.xml
configuration:
<bean id="shibboleth.SameSiteCookieHeaderFilter" class="net.shibboleth.utilities.java.support.net.SameSiteCookieHeaderFilter"> <property name="sameSiteCookies"> <map> <entry key="None" value="JSESSIONID,shib_idp_session, %{idp.storage.clientSessionStorageName:shib_idp_session_ss}, %{idp.storage.clientPersistentStorageName:shib_idp_persistent_ss}"/> </map> </property> </bean> |
The bean configuration above configures the JSESSIONID
, shib_idp_session
, shib_idp_session_ss
and shib_idp_persistent_ss
cookies with the SameSite=None cookie flag.
The test class SameSiteCookieHeaderFilterTest
tests the functionality of the SameSiteCookieHeaderFilter
filter. However, due to a bug I found with the MockHttpServletResponse
[1], the TestNG tests will fail when using spring-test
versions 5.1
up to but not including version 5.1.10
.
Note, the git repository listed in the implementation section will potentially contain a newer version of this static code listing.
/* * 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.HttpServletRequest; 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 conditionally adds the SameSite attribute to cookies. * * <p>Affected cookies are configured and placed into a Map of cookie name to same-site attribute value.</p> * * <p>Cookies with an existing same-site cookie flag, or those not present in the {@code sameSiteCookies} Map, are * left unaltered - copied back into the response without modification. * * <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 { /** Class logger. */ @Nonnull private final Logger log = LoggerFactory.getLogger(SameSiteCookieHeaderFilter.class); /** The name of the same-site cookie attribute.*/ private static final String SAMESITE_ATTRIBITE_NAME="SameSite"; /** 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; } } /** Map of cookie name to same-site attribute value.*/ @Nonnull @NonnullElements private Map<String,SameSiteValue> sameSiteCookies; /** Constructor.*/ public SameSiteCookieHeaderFilter() { sameSiteCookies = Collections.emptyMap(); } /** * 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 (!(request instanceof HttpServletRequest)) { throw new ServletException("Request is not an instance of HttpServletRequest"); } 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 continue; } final SameSiteValue sameSiteValue = sameSiteCookies.get(parsedCookies.get(0).getName()); if (sameSiteValue!=null) { appendSameSiteAttribute(cookieHeader, sameSiteValue.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 is this the first Set-Cookie header. */ private void appendSameSiteAttribute(@Nonnull final String cookieHeader, @Nonnull 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); return; } response.addHeader(HttpHeaders.SET_COOKIE, sameSiteSetCookieValue); } } } |
/* * 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.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.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.mock.http.server.reactive.MockServerHttpResponse; import org.springframework.mock.web.MockCookie; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockFilterConfig; 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}. */ 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 public void testNullInitValues() { SameSiteCookieHeaderFilter filter = new SameSiteCookieHeaderFilter(); filter.setSameSiteCookies(null); } /** Test an empty cookie name is not added to the internal map.*/ @Test 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 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(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 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 the samesite filter works correctly with None values when a redirect response is issued. */ @Test 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 Lax values when a redirect response is issued. */ @Test 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 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 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 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() { } } } |
[1] https://github.com/spring-projects/spring-framework/issues/23512