2

I have a spring boot API hosted at Heroku and when I try to access it via a Angular app in Google Chrome (In Firefox it works fine) I'm facing the following problem:

enter image description here

It seems that the JSESSIONID cookie was blocked because it wasn't set to SameSite=None. But how can I set it as SameSite=None?

The following are my configuration classes:

SecurityConfig:

@Configuration
@EnableWebSecurity
@Order(SecurityProperties.DEFAULT_FILTER_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private ClienteUserDetailsService clienteUserDetailsService;

    private static final String[] PUBLIC_MATCHERS = {"/login", "/logout", "/error.html", "/error"};

    private static final String[] PUBLIC_MATCHERS_GET = {"/login", "/logout", "/error.html", "/error"};

    private static final String[] PUBLIC_MATCHERS_POST = {"/login", "/logout"};

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable()
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, PUBLIC_MATCHERS_POST).permitAll()
                .antMatchers(HttpMethod.GET, PUBLIC_MATCHERS_GET).permitAll()
                .antMatchers(PUBLIC_MATCHERS).permitAll()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().httpBasic()
                .and().logout().logoutUrl("/logout").logoutSuccessHandler((new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK)))
                .clearAuthentication(true).invalidateHttpSession(true)
                .deleteCookies("JSESSIONID", "XSRF-TOKEN");
    }

    private CsrfTokenRepository getCsrfTokenRepository() {
        CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
        tokenRepository.setCookiePath("/");
        return tokenRepository;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(clienteUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("**")
                        .allowedOrigins("http://localhost:4200", "https://dogwalk-teste.web.app")
                        .allowedMethods("POST, GET, PUT, OPTIONS, DELETE, PATCH")
                        .allowCredentials(true);
            }
        };
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
   

}

CorsFilter:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {

    @Context
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE, PATCH");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers",
                "X-PINGOTHER, Content-Type, X-Requested-With, Accept, Origin, Access-Control-Request-Method, "
                + "Access-Control-Request-Headers, Authorization, if-modified-since, remember-me, "
                + "x-csrf-token, x-xsrf-token, xsrf-token ");
        response.addHeader("Access-Control-Expose-Headers", "xsrf-token");
        response.addHeader("Access-Control-Allow-Headers", "x-csrf-token, x-xsrf-token");
        response.setHeader("Set-Cookie", "locale=pt-BR; HttpOnly; Secure; SameSite=None;");

        chain.doFilter(req, res);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
}
Andre
  • 431
  • 7
  • 23

2 Answers2

2

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();
  }
}
Eugene Maysyuk
  • 2,977
  • 25
  • 24
1

In case basic authentication, response is flushed/committed right after controller returns response object, before SameSiteFilter#addSameSiteCookieAttribute is called.

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 com.dogwalk.dogwalk.config;

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 MyHttpFirewall 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 com.dogwalk.dogwalk.config;

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 com.dogwalk.dogwalk.config;

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

Finally you can remove obsolete SameSiteFilter as all the work will be done within RequestWrapper#overwriteSetCookie.

Pay attention that Postman doesn't render/support SameSite cookie attribute under Cookies section. You need to look at Set-Cookie response header or use curl.

Eugene Maysyuk
  • 2,977
  • 25
  • 24
  • I can push my local changes and create PR to your repo if you add me as collaborator. My GitHub: @emaysyuk – Eugene Maysyuk May 01 '21 at 01:16
  • @Andre you need to annotate MyHttpFirewall with \@Component, in other case request/response will not be wrapped and SameSite=None won't be added – Eugene Maysyuk May 01 '21 at 19:27
  • You are right Eugene! OMG I missed the @Component while creating the MyHttpFirewall class. Now it's working pretty fine in google chrome browser. Thank you a lot for helping my friend, you are the man!!! God bless you! – Andre May 01 '21 at 21:46
  • I'm glad to be of help – Eugene Maysyuk May 02 '21 at 16:17
  • @EugeneMaysyuk how this works for spring security? i'm having throubles with basic Auth!! And I dont really how this works... I have tried this solution, put some logs on the wrapper, but nothing shows. – Daniel Henao Sep 08 '21 at 14:51
  • @DanielHenao would it be possible for you to upload a sample demo to github so I could reproduce your problem? – Eugene Maysyuk Sep 08 '21 at 20:16
  • @DanielHenao here (https://github.com/emaysyuk/same-site-cookie-attribute) you can find demo project that I created, you can find all the necessary configuration details in there, run that demo project and debug it if needed. – Eugene Maysyuk Sep 08 '21 at 20:18
  • @DanielHenao in this (https://stackoverflow.com/questions/58386069/how-to-set-same-site-cookie-flag-in-spring-boot/67366090#67366090) answer you can find other approaches – Eugene Maysyuk Sep 08 '21 at 20:23
  • @DanielHenao BTW, make sure your MyHttpFirewall annotated with @ Component and is discovered and registered by Spring at runtime. – Eugene Maysyuk Sep 08 '21 at 20:27
  • @EugeneMaysyuk your help is very much appriciated! I'll have a look and come back to tell you how it went! – Daniel Henao Sep 09 '21 at 13:56
  • @EugeneMaysyuk I'm using the first approach, the firewall one... but I don't know if the cookie doesn't show up with the sameSite=none, because on the `AccountAuthenticationProvider` I return a JSON in the body of the first login request (for ROL usage) – Daniel Henao Sep 09 '21 at 14:03
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/236947/discussion-between-daniel-henao-and-eugene-maysyuk). – Daniel Henao Sep 09 '21 at 16:55