23

Is it possible to set Same-Site Cookie flag in Spring Boot?

My problem in Chrome:

A cookie associated with a cross-site resource at http://google.com/ was set without the SameSite attribute. A future release of Chrome will only deliver cookies with cross-site requests if they are set with SameSite=None and Secure. You can review cookies in developer tools under Application>Storage>Cookies and see more details at https://www.chromestatus.com/feature/5088147346030592 and https://www.chromestatus.com/feature/5633521622188032.


How to solve this problem?
Community
  • 1
  • 1
Nikolas Soares
  • 479
  • 2
  • 4
  • 13
  • check this one which used GenericFilterBean / temporary redirect request to solve a same kind of issue https://stackoverflow.com/questions/63939078/how-to-set-samesite-and-secure-attribute-to-jsessionid-cookie/63939775#63939775 – ThilankaD Oct 28 '20 at 05:13

8 Answers8

17

Spring Boot 2.6.0

Spring Boot 2.6.0 now supports configuration of SameSite cookie attribute:

Configuration via properties

server.servlet.session.cookie.same-site=strict

Configuration via code

import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class MySameSiteConfiguration {
    @Bean
    public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
        return CookieSameSiteSupplier.ofStrict();
    }
}

Spring Boot 2.5.0 and below

Spring Boot 2.5.0-SNAPSHOT doesn't support SameSite cookie attribute and there is no setting to enable it.

The Java Servlet 4.0 specification doesn't support the SameSite cookie attribute. You can see available attributes by opening javax.servlet.http.Cookie java class.

However, there are a couple of workarounds. You can override Set-Cookie attribute manually.

The first approach (using custom Spring HttpFirewall) and wrapper around request:

You need to wrap request and adjust cookies right after session is created. You can achieve it by defining the following classes:

one bean (You can define it inside SecurityConfig if you want to hold everything in one place. I just put @Component annotation on it for brevity)

package hello.approach1;

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

import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.stereotype.Component;

@Component
public class CustomHttpFirewall implements HttpFirewall {

    @Override
    public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
        return new RequestWrapper(request);
    }

    @Override
    public HttpServletResponse getFirewalledResponse(HttpServletResponse response) {
        return new ResponseWrapper(response);
    }

}

first wrapper class

package hello.approach1;

import java.util.Collection;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.http.HttpHeaders;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * Wrapper around HttpServletRequest that overwrites Set-Cookie response header and adds SameSite=None portion.
 */
public class RequestWrapper extends FirewalledRequest {

    /**
     * Constructs a request object wrapping the given request.
     *
     * @param request The request to wrap
     * @throws IllegalArgumentException if the request is null
     */
    public RequestWrapper(HttpServletRequest request) {
        super(request);
    }

    /**
     * Must be empty by default in Spring Boot. See FirewalledRequest.
     */
    @Override
    public void reset() {
    }

    @Override
    public HttpSession getSession(boolean create) {
        HttpSession session = super.getSession(create);

        if (create) {
            ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (ra != null) {
                overwriteSetCookie(ra.getResponse());
            }
        }

        return session;
    }

    @Override
    public String changeSessionId() {
        String newSessionId = super.changeSessionId();
        ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (ra != null) {
            overwriteSetCookie(ra.getResponse());
        }
        return newSessionId;
    }

    private void overwriteSetCookie(HttpServletResponse response) {
        if (response != null) {
            Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
            boolean firstHeader = true;
            for (String header : headers) { // there can be multiple Set-Cookie attributes
                if (firstHeader) {
                    response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=None")); // set
                    firstHeader = false;
                    continue;
                }
                response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=None")); // add
            }
        }
    }
}

second wrapper class

package hello.approach1;

import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

/**
 * Dummy implementation.
 * To be aligned with RequestWrapper.
 */
public class ResponseWrapper extends HttpServletResponseWrapper {
    /**
     * Constructs a response adaptor wrapping the given response.
     *
     * @param response The response to be wrapped
     * @throws IllegalArgumentException if the response is null
     */
    public ResponseWrapper(HttpServletResponse response) {
        super(response);
    }
}

The second approach (using Spring's AuthenticationSuccessHandler):

This approach doesn't work for basic authentication. In case basic authentication, response is flushed/committed right after controller returns response object, before SameSiteFilter#addSameSiteCookieAttribute is called.

package hello.approach2;

import java.io.IOException;
import java.util.Collection;

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

import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        addSameSiteCookieAttribute(response);    // add SameSite=strict to Set-Cookie attribute
        response.sendRedirect("/hello"); // redirect to hello.html after success auth
    }

    private void addSameSiteCookieAttribute(HttpServletResponse response) {
        Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
        boolean firstHeader = true;
        for (String header : headers) { // there can be multiple Set-Cookie attributes
            if (firstHeader) {
                response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
                firstHeader = false;
                continue;
            }
            response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
        }
    }
}

The third approach (using javax.servlet.Filter):

This approach doesn't work for basic authentication. In case basic authentication, response is flushed/committed right after controller returns response object, before SameSiteFilter#addSameSiteCookieAttribute is called.

package hello.approach3;

import java.io.IOException;
import java.util.Collection;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpHeaders;

public class SameSiteFilter implements javax.servlet.Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(request, response);
        addSameSiteCookieAttribute((HttpServletResponse) response); // add SameSite=strict cookie attribute
    }

    private void addSameSiteCookieAttribute(HttpServletResponse response) {
        Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
        boolean firstHeader = true;
        for (String header : headers) { // there can be multiple Set-Cookie attributes
            if (firstHeader) {
                response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
                firstHeader = false;
                continue;
            }
            response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
        }
    }

    @Override
    public void destroy() {

    }
}

You can look at this demo project on the GitHub for more details on the configuration for org.springframework.security.web.authentication.AuthenticationSuccessHandler or javax.servlet.Filter.

The SecurityConfig contains all the necessary configuration.

Using addHeader is not guaranteed to work because basically the Servlet container manages the creation of the Session and Cookie. For example, the second and third approaches won't work in case you return JSON in response body because application server will overwrite Set-Cookie header during flushing of response. However, second and third approaches will work in cases, when you redirect a user to another page after successful authentication.

Pay attention that Postman doesn't render/support SameSite cookie attribute under Cookies section (at least at the time of writing). You can look at Set-Cookie response header or use curl to see if SameSite cookie attribute was added.

Eugene Maysyuk
  • 2,977
  • 25
  • 24
  • 1
    Approach #1 works (although there's no need for the `ResponseWrapper`; just `return response;` in the firewall class)! Thanks for your solution - let's hope for a proper implementation, since this is way more complicated than it should be. – edgraaff May 12 '21 at 08:08
11

This is an open issue with Spring Security (https://github.com/spring-projects/spring-security/issues/7537)

As I inspected in Spring-Boot (2.1.7.RELEASE), By Default it uses DefaultCookieSerializer which carry a property sameSite defaulting to Lax.

You can modify this upon application boot, through the following code.

Note: This is a hack until a real fix (configuration) is exposed upon next spring release.

@Component
@AllArgsConstructor
public class SameSiteInjector {

  private final ApplicationContext applicationContext;

  @EventListener
  public void onApplicationEvent(ContextRefreshedEvent event) {
    DefaultCookieSerializer cookieSerializer = applicationContext.getBean(DefaultCookieSerializer.class);
    log.info("Received DefaultCookieSerializer, Overriding SameSite Strict");
    cookieSerializer.setSameSite("strict");
  }
}
sdoxsee
  • 4,451
  • 1
  • 25
  • 60
Mohsin Mansoor
  • 155
  • 1
  • 1
  • 8
  • 2
    what if I don't have spring-session as a dependency in the project? Would be enough just adding the spring-session-core dependency and the code snippet from above or should I go with another workaround? – nik686 May 15 '20 at 10:41
  • 1
    @nik686 please refer to the following one article i find that touch on that in the same site cookie section https://avatao.com/Secure-development-with-Spring-Framework/ – Ala'a Mezian Aug 21 '20 at 16:55
  • https://stackoverflow.com/questions/63939078/how-to-set-samesite-and-secure-attribute-to-jsessionid-cookie/63939775#63939775 This way I was able to solve the problem without any additional dependencies. – ThilankaD Jul 15 '21 at 18:31
9

From spring boot version 2.6.+ you may specify your samesite cookie either programatically or via configuration file.

Spring boot 2.6.0 documentation

If you would like to set samesite to lax via configuration file then:

server.servlet.session.cookie.same-site=lax

Or programatically

@Configuration
public class MySameSiteConfiguration {

    @Bean
    public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
        return CookieSameSiteSupplier.ofLax();
    }

}
Hapaja
  • 121
  • 1
  • 6
  • 1
    This should be the answer for 2022. Upper will cause Spring to bind the attribute into org.springframework.boot.web.servlet.server.Session.cookie, which will cause servlet factory classes like TomcatServletWebServerFactory to configure a CookieSameSiteSupplier through configureCookieProcessor(). Both create CookieSameSiteSupplier, which will be used by the servlet factory (e.g. TomcatServletWebServerFactory) to generate the session cookie. – SP193 Feb 03 '22 at 01:33
  • But how is it possible to only set SameSite attribute, if it is a secure request (https) and otherwise do not send it? (in a convenient way) – marc3l May 02 '22 at 13:53
2

Ever since the last update, chrome started showing that message to me too. Not really an answer regarding spring, but you can add the cookie flag to the header of the session. In my case, since I'm using spring security, I intend to add it when the user logs in, since I'm already manipulating the session in order to add authentication data.

For more info, check this answer to a similar topic: https://stackoverflow.com/a/43250133

To add the session header right after the user logs in, you can base your code on this topic (by creating a spring component that implements AuthenticationSuccessHandler): Spring Security. Redirect to protected page after authentication

osnofa
  • 41
  • 1
  • 1
  • 7
2

For me none of the above worked. My problem was, that after a login, the SameSite flag created with other methods mentioned in this post was simply ignored by redirect mechanizm.

In our spring boot 2.4.4 application I managed to get it done with custom SameSiteHeaderWriter:

import org.springframework.security.web.header.HeaderWriter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;

import static javax.ws.rs.core.HttpHeaders.SET_COOKIE;


/**
 * This header writer just adds "SameSite=None;" to the Set-Cookie response header
 */
public class SameSiteHeaderWriter implements HeaderWriter {

    private static final String SAME_SITE_NONE = "SameSite=None";

    private static final String SECURE = "Secure";

    @Override
    public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {

        if (response.containsHeader(SET_COOKIE)) {

            var setCookie = response.getHeader(SET_COOKIE);
            var toAdd = new ArrayList<String>();
            toAdd.add(setCookie);

            if (! setCookie.contains(SAME_SITE_NONE)) {
                toAdd.add(SAME_SITE_NONE);
            }

            if (! setCookie.contains(SECURE)) {
                toAdd.add(SECURE);
            }

            response.setHeader(SET_COOKIE, String.join("; ", toAdd));

        }
    }

}

then in my WebSecurityConfigurerAdapter#configure I just added this header writer to the list using:

if (corsEnabled) {
            httpSecurity = httpSecurity
                        .cors()
                    .and()
                        .headers(configurer -> {
                            configurer.frameOptions().disable();
                            configurer.addHeaderWriter(new SameSiteHeaderWriter());
                        });
        }

This feature have to be explicitly enabled in our app by user knowing the risks.

Just thought this might help someone in the future.

ibecar
  • 415
  • 3
  • 10
  • This solution works for Spring boot 1.4 – maxiplay Oct 08 '21 at 16:24
  • My server redirects user to another page on success authentication and above approaches worked perfectly for me. Even in case of redirect, server should have included cookie into response containing new location (redirect location). If you provide code sample, I can take a look at your configuration and investigate why above approaches didn't work for you. – Eugene Maysyuk Jan 07 '22 at 16:56
1

Starting from Spring Boot 2.6.0 this is now possible and easy:

import org.springframework.http.ResponseCookie;
ResponseCookie springCookie = ResponseCookie.from("refresh-token", "000")
  .sameSite("Strict")
  .build();

and return it in a ResponseEntity, could be like this :

ResponseEntity
    .ok()
    .header(HttpHeaders.SET_COOKIE, springCookie.toString())
    .build();
machinus
  • 153
  • 2
  • 13
0

If you use spring-redis-session, you can customize the Cookie () by creating a bean like the following:

@Bean
public CookieSerializer cookieSerializer() {
    DefaultCookieSerializer serializer = new DefaultCookieSerializer();
    serializer.setCookieName("JSESSIONID"); 
    serializer.setCookiePath("/"); 
    serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
    serializer.setSameSite(null);
    return serializer;
}

You can look here more detail information.

fgul
  • 5,763
  • 2
  • 46
  • 32
-2

Follow the documentation to solve this issue: https://github.com/GoogleChromeLabs/samesite-examples

It has examples with different languages

Code Cooker
  • 881
  • 14
  • 19
  • Unfortunately that site does not appear to have any entries relevant to this particular question (at least as of 2020-04-02). – Joel Aelwyn Apr 02 '20 at 17:09
  • THANK YOU. just had to disable the same site required flag. 2-3 hours of debugging finally resolved. – Necmttn Sep 09 '20 at 06:52