[java-support] branch maint-7 updated: IDP-1476 - Need a filter to append SameSite to cookies
Scott Cantor
cantor.2 at osu.edu
Fri Sep 13 20:25:33 EDT 2019
This is an automated email from the git hooks/post-receive script.
scantor pushed a commit to branch maint-7
in repository java-support.
View the commit online:
http://git.shibboleth.net/view/?p=java-support.git;a=commit;h=5f306e929d64ee08916c6729b276cc55c5a9c551
The following commit(s) were added to refs/heads/maint-7 by this push:
new 5f306e9 IDP-1476 - Need a filter to append SameSite to cookies
5f306e9 is described below
commit 5f306e929d64ee08916c6729b276cc55c5a9c551
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Fri Sep 13 20:25:45 2019 -0400
IDP-1476 - Need a filter to append SameSite to cookies
https://issues.shibboleth.net/jira/browse/IDP-1476
Backport filter from master. Tests left out since they
won't run without a Spring 5.2 fix.
---
.../support/net/SameSiteCookieHeaderFilter.java | 319 +++++++++++++++++++++
1 file changed, 319 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
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list