22

Is it possible to set Same-site Cookie flag in Spring Security?

And if not, is it on a roadmap to add support, please? There is already support in some browsers (i.e. Chrome).

Tomáš Hála
  • 231
  • 1
  • 2
  • 4
  • 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:15
  • This worked for me. https://vaadin.com/forum/thread/18124830/18509113 Using Spring Boot 2.4.1 – Avec Jan 08 '21 at 14:35
  • Worked solution for me https://stackoverflow.com/a/64558083/4423695 – ThilankaD Feb 06 '21 at 12:17

8 Answers8

12

New Tomcat version support SameSite cookies via TomcatContextCustomizer. So you should only customize tomcat CookieProcessor, e.g. for Spring Boot:

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

For SameSiteCookies.NONE be aware, that cookies are also Secure (SSL used), otherwise they couldn't be applied.

By default since Chrome 80 cookies considered as SameSite=Lax!

See SameSite Cookie in Spring Boot and SameSite cookie recipes.


For nginx proxy it could be solved easily in nginx config:

if ($scheme = http) {
    return 301 https://$http_host$request_uri;
}

proxy_cookie_path / "/; secure; SameSite=None";
Community
  • 1
  • 1
Grigory Kislin
  • 12,805
  • 7
  • 98
  • 154
9

Instead of a Filter, In your Authentication Success Handler, you can mention in this way.

@Override
public void onAuthenticationSuccess(
        HttpServletRequest request, HttpServletResponse response,
        Authentication authentication) throws IOException {
    response.setStatus(HttpServletResponse.SC_OK);
    clearAuthenticationAttributes(request);
    addSameSiteCookieAttribute(response);
    handle(request, response);
}

private void addSameSiteCookieAttribute(HttpServletResponse response) {
    Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
    boolean firstHeader = true;
    // there can be multiple Set-Cookie attributes
    for (String header : headers) {
        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"));
    }
}

It was mentioned in one of the answers. Couldn't find the link after I've implemented it.

Community
  • 1
  • 1
Shiva kumar
  • 525
  • 5
  • 18
6

You can always set cookie values by yourself in the Java world if you can get an instance of the HttpServletResponse.

Then you can do:

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

In spring-security you can easily do this with a filter, here is an example:

public class CustomFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                       FilterChain chain) throws IOException, ServletException {
        HttpServletResponse resp = (HttpServletResponse) response;
        resp.setHeader("Set-Cookie", "locale=de; HttpOnly; SameSite=strict");
        chain.doFilter(request, response);
    }
}

Add this filter to your SecurityConfig like this:

http.addFilterAfter(new CustomFilter(), BasicAuthenticationFilter.class)

Or via XML:

<http>
    <custom-filter after="BASIC_AUTH_FILTER" ref="myFilter" />
</http>

<beans:bean id="myFilter" class="org.bla.CustomFilter"/>
Community
  • 1
  • 1
unwichtich
  • 13,143
  • 2
  • 46
  • 60
  • 1
    if you want to apply this for all cookies, you can do something like this: `String cookie = resp.getHeader("Set-Cookie");if (cookie != null) { resp.setHeader("Set-Cookie", cookie + "; HttpOnly; SameSite=strict");}` – Adam Kučera Feb 03 '20 at 08:28
6

All possible solutions here failed for me. Every time I tried a filter or interceptor, the Set-Cookie header had not yet been added. The only way I was able to make this work was by adding Spring Session and adding this bean into one of my @Configuration files:

@Bean
public CookieSerializer cookieSerializer() {
    DefaultCookieSerializer serializer = new DefaultCookieSerializer();
    serializer.setSameSite("none");
    return serializer;
}

Anyway hope this helps someone else in my same situation.

Community
  • 1
  • 1
ice1080
  • 111
  • 2
  • 8
5

It isn't possible. There is support for this feature in Spring Session: https://spring.io/blog/2018/10/31/spring-session-bean-ga-released

I came up with a solution similar to Ron's one. But there is one important thing to note:

Cookies for cross-site usage must specify SameSite=None; Secure to enable inclusion in third party context.

So I've included Secure attribute in header. Also, you don't have to override all three methods when you don't use them. It is only required when you are implementing HandlerInterceptor.

import org.apache.commons.lang.StringUtils;

public class CookiesInterceptor extends HandlerInterceptorAdapter {
    final String sameSiteAttribute = "; SameSite=None";
    final String secureAttribute = "; Secure";

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response,
                           Object handler, ModelAndView modelAndView) throws Exception {

        addEtagHeader(request, response);

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

        if (setCookieHeaders == null || setCookieHeaders.isEmpty())
            return;

        setCookieHeaders
            .stream()
            .filter(StringUtils::isNotBlank)
            .map(header -> {
                if (header.toLowerCase().contains("samesite")) {
                    return header;
                } else {
                    return header.concat(sameSiteAttribute);
                }
            })
            .map(header -> {
                if (header.toLowerCase().contains("secure")) {
                    return header;
                } else {
                    return header.concat(secureAttribute);
                }
            })
            .forEach(finalHeader -> response.setHeader(HttpHeaders.SET_COOKIE, finalHeader));
    }
}

I used xml in my project so I had to add this to my configuration file:

<mvc:interceptors>
    <bean class="com.zoetis.widgetserver.mvc.CookiesInterceptor"/>
</mvc:interceptors>
Community
  • 1
  • 1
3

Using the interceptor in SpringBoot.

I'm looking for a resolution for adding SameSite as you, and I only want to add the attribute to the existing "Set-Cookie" instead of creating a new "Set-Cookie". I have tried several ways to meet this requirement, including:

  1. adding a custom filter as @unwichtich said,
  2. and more I overrode basicAuthenticationFilter. It does add the SameSite attribute. While the timing when Spring will add the "Set-Cookie" is hard to catch. I thought in onAuthenticationSuccess() method, the response must have this header, but it doesn't. I'm not sure whether it's the fault of my custom basicAuthenticationFilter's order.
  3. using cookieSerializer, but the spring-session version comes up to a problem. Seems only the latest version support it, but I still can't figure out the version number should be added into the dependency list.
    Unfortunately, none of them above can add the samesite well as expected.

Finally, I found the interceptor in spring can help me to make it. It took me a week to get it. Hope this can help you if anyone has the same problem.

@Component
public class CookieServiceInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(
            HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

    @Override
    public void postHandle(
            HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        //check whether it has "set-cookie" in the response, if it has, then add "SameSite" attribute
        //it should be found in the response of the first successful login
        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 afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception exception) throws Exception {
    }
}

and you also need to make this interceptor work in your application, which means you should add a bean as below:

@Autowired
CookieServiceInterceptor cookieServiceInterceptor;

@Bean
public MappedInterceptor myInterceptor() {
    return new MappedInterceptor(null, cookieServiceInterceptor);
}

This interceptor has a flaw, it can't add samesite when the request is redirected(ex.return 302) or failed(ex. return 401), while it makes my app fail when SSO. Eventually, I have to use the Tomcat cookie, because I don't embed tomcat in my springboot app. I add

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

in a context.xml under /META-INF of my app. It will add SameSite attribute in set-cookie header for each response. Note that this behavior is possible since Tomcat 9.0.21 and 8.5.42. according to https://stackoverflow.com/a/57622508/4033979

Community
  • 1
  • 1
Ron
  • 713
  • 2
  • 6
  • 20
1

I have tested this solution for spring-webmvc without spring-security - see the simple feedback form, but I think it should also work for spring-boot.


Using the SessionRepositoryFilter bean from spring-session-core

You can extend default java HttpSession with a spring Session and replace JSESSIONID cookie with a custom one, like this:

Set-Cookie: JSESSIONID=NWU4NzY4NWUtMDY3MC00Y2M1LTg1YmMtNmE1ZWJmODcxNzRj; Path=/; Secure; HttpOnly; SameSite=None

Additional spring Session cookie flags can be set using DefaultCookieSerializer:

@Configuration
@EnableSpringHttpSession
public class WebAppConfig implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) {
        servletContext
                .addFilter("sessionRepositoryFilter", DelegatingFilterProxy.class)
                .addMappingForUrlPatterns(null, false, "/*");
    }

    @Bean
    public MapSessionRepository sessionRepository() {
        final Map<String, Session> sessions = new ConcurrentHashMap<>();
        MapSessionRepository sessionRepository =
                new MapSessionRepository(sessions) {
                    @Override
                    public void save(MapSession session) {
                        sessions.entrySet().stream()
                                .filter(entry -> entry.getValue().isExpired())
                                .forEach(entry -> sessions.remove(entry.getKey()));
                        super.save(session);
                    }
                };
        sessionRepository.setDefaultMaxInactiveInterval(60*5);
        return sessionRepository;
    }

    @Bean
    public SessionRepositoryFilter<?> sessionRepositoryFilter(MapSessionRepository sessionRepository) {
        SessionRepositoryFilter<?> sessionRepositoryFilter =
                new SessionRepositoryFilter<>(sessionRepository);

        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setCookieName("JSESSIONID");
        cookieSerializer.setSameSite("None");
        cookieSerializer.setUseSecureCookie(true);

        CookieHttpSessionIdResolver cookieHttpSessionIdResolver =
                new CookieHttpSessionIdResolver();
        cookieHttpSessionIdResolver.setCookieSerializer(cookieSerializer);

        sessionRepositoryFilter.setHttpSessionIdResolver(cookieHttpSessionIdResolver);

        return sessionRepositoryFilter;
    }
}

I have extended a bit MapSessionRepository implementation, since it does NOT support firing SessionDeletedEvent or SessionExpiredEvent - I have added clearing of expired sessions before adding new ones. I think this might be enough for a small application.

0

For Spring Webflux (reactive environment) this worked for me:

@Configuration
@EnableSpringWebSession
public class SessionModule {

  @Bean
  public ReactiveSessionRepository<MapSession> reactiveSessionRepository() {
    return new ReactiveMapSessionRepository(new ConcurrentHashMap<>());
  }

  @Bean
  public WebSessionIdResolver webSessionIdResolver() {
    CookieWebSessionIdResolver resolver = new CookieWebSessionIdResolver();
    resolver.setCookieName("SESSION");
    resolver.addCookieInitializer((builder) -> {
      builder.path("/")
          .httpOnly(true)
          .secure(true)
          .sameSite("None; Secure");
    });
    return resolver;
  }
}
kane
  • 4,242
  • 3
  • 31
  • 52