1

Google chrome has introduced changes that require setting the Same-Site header. In order to achieve this, I added a custom filter as follows,

public class SameSiteFilter extends GenericFilterBean {
    private Logger LOG = LoggerFactory.getLogger(SameSiteFilter.class);

    @Override
    public void doFilter(ServletRequest request,  ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse resp = (HttpServletResponse)response;
        response = addSameSiteCookieAttribute((HttpServletResponse) response);
        chain.doFilter(request, response);
    }    

    private HttpServletResponse addSameSiteCookieAttribute(HttpServletResponse response) {
        Collection<String> header = response.getHeaders(HttpHeaders.SET_COOKIE);
        LOG.info(String.format("%s; %s", header, "SameSite=None; Secure"));
        response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=None; Secure"));

        return response;
    }
}

Following is the code for Security Configuration

@Configuration
@EnableWebMvcSecurity
public class CustomSecurityConfiguration extends WebSecurityConfigurerAdapter { 
    @Autowired
    private OnyxUserDetailsService onyxUserDetailsService;

    @Autowired
    private CustomAuthenticationProvider customAuthenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/rest/user", "/info/**/*","/rest/version/check")
                .permitAll().antMatchers("/data/**/*")
                .access("hasRole('ROLE_ADMIN')").anyRequest()
                .fullyAuthenticated().and().httpBasic().realmName("ADOBENET")
                .and().logout().
                logoutSuccessHandler((new LogoutSuccessHandler() {

                    @Override
                    public void onLogoutSuccess(HttpServletRequest request,
                            HttpServletResponse response, Authentication authentication)
                            throws IOException, ServletException {
                        response.setStatus(HttpStatus.OK.value());
                        response.getWriter().flush();
                    }
                })).deleteCookies("JSESSIONID", "XSRF-TOKEN")
                .invalidateHttpSession(true).logoutUrl("/rest/logout")
                .logoutSuccessUrl("/rest/user").and()
                .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)   
                .addFilterAfter(new SameSiteFilter(), BasicAuthenticationFilter.class)          
                .csrf().disable();
    }

    @Override
    @Order(Ordered.HIGHEST_PRECEDENCE)
    protected void configure(AuthenticationManagerBuilder auth)
            throws Exception {
        auth.authenticationProvider(customAuthenticationProvider);
    }
}

However, when I look at the headers received, I get this

enter image description here

The filter adds the required fields in all the responses exception the one containing the JSESSIONID cookie. How do I add the headers to this cookie. I tried configuring tomcat settings, but we deploy the code as a WAR file, so that did also not work.

arjunkhera
  • 929
  • 6
  • 23
  • I'm facing the same problem you was, please look https://stackoverflow.com/questions/67320068/how-to-set-samesite-none-in-jsessionid-cookie . Any tip for me to solve this? – Andre Apr 29 '21 at 15:10

2 Answers2

0

In order to get around this problem, I added a Filter for sifting through all the responses. Here is the code for the same,

@Component
public class SameSiteFilter implements Filter {
    private Logger LOG = LoggerFactory.getLogger(SameSiteFilter.class);

    @Override
    public void init(final FilterConfig filterConfig) throws ServletException {
        LOG.info("Same Site Filter Initializing filter :{}", this);
    }

    @Override
    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        LOG.info("Same Site Filter Logging Response :{}", res.getContentType());

        Collection<String> headers = res.getHeaders(HttpHeaders.SET_COOKIE);
        boolean firstHeader = true;
        for (String header : headers) { // there can be multiple Set-Cookie attributes
            if (firstHeader) {
                res.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s",  header, "SameSite=None"));
                LOG.info(String.format("Same Site Filter First Header %s; %s", header, "SameSite=None; Secure"));

                firstHeader = false;
                continue;
            }

            res.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s",  header, "SameSite=None"));
            LOG.info(String.format("Same Site Filter Remaining Headers %s; %s", header, "SameSite=None; Secure"));
        }

        chain.doFilter(req, res);
    }

    @Override
    public void destroy() {
        LOG.warn("Same Site Filter Destructing filter :{}", this);
    }
}

This allows for addition of the required headers in the response containing the cookie

arjunkhera
  • 929
  • 6
  • 23
0

Solution without using spring boot or spring session.

for more details about the solution Samesite for jessessionId cookie can be set only from response

  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;
      }

    }

  }
aouamri
  • 41
  • 2