SameSite cookie in Java application

JavaCookiesCsrfFlags

Java Problem Overview


Do you know any Java cookie implementation which allows to set a custom flag for cookie, like SameSite=strict? It seems that javax.servlet.http.Cookie has a strictly limited set of flags which can be added.

Java Solutions


Solution 1 - Java

I am not a JEE expert, but I think that because that cookie property is a somewhat new invention, you cannot expect it to be present in Java EE 7 interfaces or implementations. The Cookie class is missing a setter for generic properties, as it seems. But instead of adding the cookie to your HttpServletResponse via

response.addCookie(myCookie)

you can simply set the corresponding HTTP header field via

response.setHeader("Set-Cookie", "key=value; HttpOnly; SameSite=strict")

Update: Thanks to @mwyrzyk for pointing out that setHeader() overwrites all existing headers of the same name. So if you happen have other Set-Cookie headers in your response already, of course you would use addHeader() with the same parameters instead.

Solution 2 - Java

If you don't wanna update all your code, you can also achieve same by one line config using Apache or Nginx configuration(or any other HTTP server/proxy that you are using)

1 Setting SameSite cookies using Apache configuration

You can add the following line to your Apache configuration

Header always edit Set-Cookie (.*) "$1; SameSite=Lax"

and this will update all your cookies with SameSite=Lax flag

See more here: https://blog.giantgeek.com/?p=1872

2 Setting SameSite cookies using Nginx configuration
location / {
    # your usual config ...
    # hack, set all cookies to secure, httponly and samesite (strict or lax)
    proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
}

Same here, this also will update all your cookies with SameSite=Lax flag

See more here: https://serverfault.com/questions/849888/add-samesite-to-cookies-using-nginx-as-reverse-proxy

Solution 3 - Java

As of today (24.01.20) servlet-api does not let to set sameSite attribute to the cookie. BTW there is an ongoing ticket (LINK) which will release a new (5.0 or 5.1 servlet-api).

Option 1: You are not in a hurry and can wait for servlet-api version, where Cookie class and SessionCookieConfig class have dedicated methods to set sameSite attribute.

Option 2: You are using an old version of servlet-api (e.g. 3.1), consequently old version of Tomcat (e.g. I am having current situation now). It means even when community releases servlet-api with sameSite support, you can not immediately update you version, because it can be too risky to update couple of major versions.
In this case we have found a solution.
There is a Cookie Processor ComponentLINK in Tomcat, which

> The CookieProcessor element represents the component that parses received cookie headers into javax.servlet.http.Cookie objects accessible through HttpServletRequest.getCookies() and converts javax.servlet.http.Cookie objects added to the response through HttpServletResponse.addCookie() to the HTTP headers returned to the client.

The usage of this processor is quite straight forward. Inside of context.xml:

<Context>
    ...
    <CookieProcessor sameSiteCookies="none"/>
</Context>

In this case default implementation of processor is used (org.apache.tomcat.util.http.Rfc6265CookieProcessor), but you can specify any other within an CookieProcessor attribute className.

Solution 4 - Java

If you have existing code, no doubt you've used the java servlet Cookie object. We certainly have, so we wanted the least disruptive option. @kriegaex's answer is clean and concise, but is basically hard coding the cookie and doesn't reuse the cookie object. To expand on his answer, we wrote this function to handle the same site functionality, while at the same time, maintaining the existing Cookie object functionality. This answer is intended to be used in cases where you need to add multiple cookies on your response object, without making changes to existing cookies that may already be on the headers. The other option of course is to write a new cookie class and extend the functionality, but that requires even more changes to existing code than what we've come up with here.

Note that with this solution, only one line of existing code (per cookie) changes in order to add the same site functionality.

Sample usage:

// Existing code that doesn't change:	
Cookie cookie1=new Cookie("cookie1",Util.encodeURL(id));
cookie1.setHttpOnly(false);
cookie1.setPath("/");

Cookie cookie2=new Cookie("cookie2",Util.encodeURL(id));
cookie2.setHttpOnly(false);
cookie2.setPath("/");

// Old Code that is replaced by new code
// httpResponse.addCookie(cookie1);
// httpResponse.addCookie(cookie2);

// New Code - see static helper class below
HttpService.addCookie(httpResponse, cookie1, "none");
HttpService.addCookie(httpResponse, cookie2, "Strict");

Example response headers when using cURL:

< HTTP/1.1 200 OK
< Connection: keep-alive
< X-Powered-By: Undertow/1
< Set-Cookie: cookie1=f871c026e8eb418c9c612f0c7fe05b08; path=/; SameSite=none; secure
< Set-Cookie: cookie2=51b405b9487f4487b50c80b32eabcc24; path=/; SameSite=Strict; secure
< Server: WildFly/9
< Transfer-Encoding: chunked
< Content-Type: image/png
< Date: Tue, 10 Mar 2020 01:55:37 GMT

And finally, the static helper class:

public class HttpService {
    private static final FastDateFormat expiresDateFormat= FastDateFormat.getInstance("EEE, dd MMM yyyy HH:mm:ss zzz", TimeZone.getTimeZone("GMT"));


    public static void addCookie(HttpServletResponse response, Cookie cookie, String sameSite) {

        StringBuilder c = new StringBuilder(64+cookie.getValue().length());

        c.append(cookie.getName());
        c.append('=');
        c.append(cookie.getValue());

        append2cookie(c,"domain",   cookie.getDomain());
        append2cookie(c,"path",     cookie.getPath());
        append2cookie(c,"SameSite", sameSite);

        if (cookie.getSecure()) {
            c.append("; secure");
        }
        if (cookie.isHttpOnly()) {
            c.append("; HttpOnly");
        }
        if (cookie.getMaxAge()>=0) {
            append2cookie(c,"Expires", getExpires(cookie.getMaxAge()));
        }

        response.addHeader("Set-Cookie", c.toString());
    }

    private static String getExpires(int maxAge) {
        if (maxAge<0) {
            return "";
        }
        Calendar expireDate = Calendar.getInstance();
        expireDate.setTime(new Date());
        expireDate.add(Calendar.SECOND,maxAge);

        return expiresDateFormat.format(expireDate);
    }

    private static void append2cookie(StringBuilder cookie, String key, String value) {
        if (key==null || 
                value==null || 
                key.trim().equals("") 
                || value.trim().equals("")) {
            return;
        }

        cookie.append("; ");
        cookie.append(key);
        cookie.append('=');
        cookie.append(value);
    }
}

Solution 5 - Java

Jetty server version 9.4.26.v20200117 allows for setting the SameSite attribute on the cookie. I had to do some digging around but this works.

import static org.eclipse.jetty.http.HttpCookie.SAME_SITE_STRICT_COMMENT;

...

Cookie cookie = new Cookie("my-cookie", "some-value");
cookie.setMaxAge(120); // age in seconds
cookie.setSecure(true);
cookie.setHttpOnly(true);
cookie.setComment(SAME_SITE_STRICT_COMMENT);

response.addCookie(cookie);

The addCookie() method on jetty servers's Response object does a check on the comment to add the SameSite attribute.

Solution 6 - Java

If you happen to use Spring Framework you can take advantage of ResponseCookie class. Ex:

final ResponseCookie responseCookie = ResponseCookie
		.from("<my-cookie-name>", "<my-cookie-value-here>")
		.secure(true)
		.httpOnly(true)
		.path("/auth")
		.maxAge(12345)
		.sameSite("Lax")
		.build();
response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString());

Disclamer: The flags and their values are provided just as an example for the class' API.

Solution 7 - Java

I tried the listed solutions for using javax.servlet.http.Cookie to set the SameSite=strict attribute, but none of them worked.

However, this way worked for me, using javax.servlet.http.Cookie (JRE 1.8 + JBOSS 7.X) :

Cookie cookie = new Cookie(name, value);
path = path.concat("SameSite=Strict;");
cookie.setPath(path);

That's it. tested on

  • Google Chrome Version 81.0.4044.129 (Official Build) (64-bit)
  • Microsoft Edge Version 81.0.416.68 (Official build) (64-bit)
  • Firefox 75.0 (64-bit)

Solution 8 - Java

If using spring boot with Tom cat then this has been answered in another question. In summary, set the attribute on the tom cat config. This is global, all cookies will then have same site enabled. (from the other question https://stackoverflow.com/a/60860531/400048)

@Configuration
public class MvcConfiguration implements WebMvcConfigurer {

  @Bean
  public TomcatContextCustomizer sameSiteCookiesConfig() {
    return context -> {
        final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor();
        cookieProcessor.setSameSiteCookies(SameSiteCookies.NONE.getValue());
        context.setCookieProcessor(cookieProcessor);
    };
  }

Solution 9 - Java

I found that our cookies which were being created on a successful return were not changed by "Header edit" or "Header always edit". Apparently apache has two buckets of cookies - see this

What did work for me was

Header onsuccess edit Set-Cookie (.*) "$1; SameSite=Lax"

Solution 10 - Java

Solution without using spring boot or spring session.

for more details about the solution https://stackoverflow.com/questions/64069650/samesite-for-jessessionid-cookie-can-be-set-only-from-response/64314117#64314117

        package com.cookie.example.filters.cookie;


  import com.google.common.net.HttpHeaders;
  import org.apache.commons.collections.CollectionUtils;
  import org.apache.commons.lang3.StringUtils;
  import org.springframework.beans.factory.InitializingBean;
  import org.springframework.web.filter.DelegatingFilterProxy;

  import javax.annotation.Nonnull;
  import javax.servlet.*;
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  import javax.servlet.http.HttpServletResponseWrapper;
  import java.io.IOException;
  import java.io.PrintWriter;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.List;

  /**
   * Implementation of an HTTP filter {@link Filter} which which allow customization of {@literal Set-Cookie} header.
   * customization is delegated to implementations of {@link CookieHeaderCustomizer}
   */
  public class CookieHeaderCustomizerFilter extends DelegatingFilterProxy implements InitializingBean {

    private final List<CookieHeaderCustomizer> cookieHeaderCustomizers;

    @Override
    public void afterPropertiesSet() throws ServletException {
      super.afterPropertiesSet();
      if(CollectionUtils.isEmpty(cookieHeaderCustomizers)){
        throw new IllegalArgumentException("cookieHeaderCustomizers is mandatory");
      }
    }

    public CookieHeaderCustomizerFilter(final List<CookieHeaderCustomizer> cookieHeaderCustomizers) {
      this.cookieHeaderCustomizers = cookieHeaderCustomizers;
    }

    public CookieHeaderCustomizerFilter() {
      this.cookieHeaderCustomizers = Collections.emptyList();
    }


    /** {@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 CookieHeaderResponseWrapper((HttpServletRequest) request, (HttpServletResponse)response ));

    }


    /**
     * An implementation of the {@link HttpServletResponse} which customize {@literal Set-Cookie}
     */
    private class CookieHeaderResponseWrapper extends HttpServletResponseWrapper{

      @Nonnull private final HttpServletRequest request;

      @Nonnull private final HttpServletResponse response;


      public CookieHeaderResponseWrapper(@Nonnull final HttpServletRequest req, @Nonnull final HttpServletResponse resp) {
        super(resp);
        this.request = req;
        this.response = resp;

      }

      /** {@inheritDoc} */
      @Override
      public void sendError(final int sc) throws IOException {
        applyCustomizers();
        super.sendError(sc);
      }

      /** {@inheritDoc} */
      @Override
      public PrintWriter getWriter() throws IOException {
        applyCustomizers();
        return super.getWriter();
      }

      /** {@inheritDoc} */
      @Override
      public void sendError(final int sc, final String msg) throws IOException {
        applyCustomizers();
        super.sendError(sc, msg);
      }

      /** {@inheritDoc} */
      @Override
      public void sendRedirect(final String location) throws IOException {
        applyCustomizers();
        super.sendRedirect(location);
      }

      /** {@inheritDoc} */
      @Override
      public ServletOutputStream getOutputStream() throws IOException {
        applyCustomizers();
        return super.getOutputStream();
      }

      private void applyCustomizers(){

        final Collection<String> cookiesHeaders = response.getHeaders(HttpHeaders.SET_COOKIE);

        boolean firstHeader = true;

        for (final String cookieHeader : cookiesHeaders) {

          if (StringUtils.isBlank(cookieHeader)) {
            continue;
          }

          String customizedCookieHeader = cookieHeader;

          for(CookieHeaderCustomizer cookieHeaderCustomizer : cookieHeaderCustomizers){

            customizedCookieHeader = cookieHeaderCustomizer.customize(request, response, customizedCookieHeader);

          }

          if (firstHeader) {
            response.setHeader(HttpHeaders.SET_COOKIE,customizedCookieHeader);
            firstHeader=false;
          } else {
            response.addHeader(HttpHeaders.SET_COOKIE, customizedCookieHeader);
          }

        }

      }

    }

  }



  /**
   * Implement this interface and inject add it to {@link SameSiteCookieHeaderCustomizer}
   */
  public interface CookieHeaderCustomizer {
    String customize(@Nonnull final HttpServletRequest request, @Nonnull final HttpServletResponse response, @Nonnull final String cookieHeader);
  }


    package com.cookie.example.filters.cookie;

      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;

      import javax.annotation.Nonnull;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;

  /**
   *Add SameSite attribute if not already exist
   *SameSite attribute value is defined by property "cookie.sameSite"
   */
  public class SameSiteCookieHeaderCustomizer implements CookieHeaderCustomizer {

    private static final Logger LOGGER = LoggerFactory.getLogger(SameSiteCookieHeaderCustomizer.class);

    private static final String SAME_SITE_ATTRIBUTE_NAME ="SameSite";

    private static final String SECURE_ATTRIBUTE_NAME="Secure";

    private final SameSiteValue sameSiteValue;

    public SameSiteCookieHeaderCustomizer(SameSiteValue sameSiteValue) {
      this.sameSiteValue = sameSiteValue;
    }


    @Override
    public String customize(@Nonnull final HttpServletRequest request, @Nonnull final HttpServletResponse response, @Nonnull final String cookieHeader) {
      StringBuilder sb = new StringBuilder(cookieHeader);
      if (!cookieHeader.contains(SAME_SITE_ATTRIBUTE_NAME)) {
        sb.append("; ").append(SAME_SITE_ATTRIBUTE_NAME).append("=").append(sameSiteValue.value);
      }
      if(SameSiteValue.None == sameSiteValue && !cookieHeader.contains(SECURE_ATTRIBUTE_NAME)){
        sb.append("; ").append(SECURE_ATTRIBUTE_NAME);
      }
      return sb.toString();
    }

    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.*/
      private String value;

      /**
       * Constructor.
       *
       * @param attrValue the same-site attribute value.
       */
      SameSiteValue(@Nonnull final String attrValue) {
        value = attrValue;
      }

      /**
       * Get the same-site attribute value.
       *
       * @return Returns the value.
       */
      public String getValue() {
        return value;
      }

    }

  }

Solution 11 - Java

ravinder5 did implement this and open sourced it: CookieHeader

Sample usage:

import com.tgt.egs.auth.cookie.CookieHeader;
...

CookieHeader.createSetCookieHeader(cookieName, cookieValue, domain, path, sameSite, secure, httpOnly, expiry);
        

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMichal_SzulcView Question on Stackoverflow
Solution 1 - JavakriegaexView Answer on Stackoverflow
Solution 2 - JavaMarty AghajanyanView Answer on Stackoverflow
Solution 3 - JavaAlexander MartyushovView Answer on Stackoverflow
Solution 4 - JavawryanView Answer on Stackoverflow
Solution 5 - JavavijayView Answer on Stackoverflow
Solution 6 - JavanyxzView Answer on Stackoverflow
Solution 7 - Javavarunsinghal65View Answer on Stackoverflow
Solution 8 - JavazcourtsView Answer on Stackoverflow
Solution 9 - JavaFred AndrewsView Answer on Stackoverflow
Solution 10 - JavaaouamriView Answer on Stackoverflow
Solution 11 - JavaLonzakView Answer on Stackoverflow