spring-bootspring-securityspring-session

Upgrade spring boot app from 2 to 3, fail to 'Set-Cookie' on auth


I'm attempting to upgrade my angular spring boot 2.x.x app to spring boot 3.x.x. I am seeing my authentication REST call to my user endpoint NOT return the 'Set-Cookie' header after it authenticates via the basic auth header. The call returns a status 200 as the authentication works for this one call. All subsequent secured REST calls return a 401 status as the session cookie is never set due to lack of 'Set-Cookie' return on auth call.

Here are the request response headers for the auth call. Note the lack of 'Set-Cookie' in response. In spring boot 2.x.x, the 'Set-Cookie' was always returned on this call.

===> the request headers
GET /api/users/current HTTP/1.1
Host: retrospect.wspfeiffer.com:8443
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/110.0
Accept: application/json, text/plain, /
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
authorization: Basic XXXXXXXXXXXXXXXXXXXX
Access-Control-Allow-Origin: http://localhost:4200
X-Requested-With: XMLHttpRequest
Connection: keep-alive
Referer: https://retrospect.wspfeiffer.com:8443/login
Cookie: SESSION=N2IzZmU4MDEtZjUwMC00MGU1LTllNDItMzVjODkzZWY1NGE2
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Pragma: no-cache
Cache-Control: no-cache

====> the response headers
HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Cache-Control: no-cache
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 08 Mar 2023 14:48:29 GMT
Keep-Alive: timeout=60
Connection: keep-alive

In spring boot 2.x.x, the 'Set-Cookie' is set and the cookie is used for all subsequent rest calls. This technique is documented here: https://spring.io/guides/tutorials/spring-security-and-angular-js/ and has worked for me up until the spring boot 3 upgrade. Any ideas on this?

Adding security config:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.HashMap;
import java.util.Map;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity(debug = true)
@EnableMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(HttpMethod.OPTIONS).permitAll()
                        .requestMatchers("/open/api/**").permitAll()
                                                .requestMatchers("/index.html").permitAll()
                        .requestMatchers(HttpMethod.GET,
                                         "",
                                         "/",
                                         "/*.html",
                                         "/favicon.ico",
                                         "/*.css",
                                         "/*.js",
                                         "/*.map",
                                         "/assets/**",
                                         "/error").permitAll()
                        .requestMatchers("/api/**").authenticated()
                .anyRequest().authenticated())
                .httpBasic()
                .and()
                .cors()
                .and()
                .csrf()
                    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .and()
                .logout()
                .logoutUrl("/logout");
        http.headers().cacheControl().disable();
        http.headers().frameOptions().disable();
        return http.build();
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("http://localhost:4200");
        config.addAllowedHeader("*");
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("DELETE");
        config.addExposedHeader("Content-Type");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }

    @Bean
    public PasswordEncoder passwordEncoder()
    {
        String idForEncode = "bcrypt";
        Map<String,PasswordEncoder> encoders = new HashMap<>();
        encoders.put(idForEncode, new BCryptPasswordEncoder());
        encoders.put("sha256", new StandardPasswordEncoder());

        return new DelegatingPasswordEncoder(idForEncode, encoders);
    }
}

Solution

  • Spring Security 6 requires explicit save of the SecurityContext, the HTTP Basic is a stateless authentication mechanism, therefore it won't be saved to the session by default. To do that, you must configure the BasicAuthenticationFilter to use a HttpSessionSecurityContextRepository, like so:

    http
            // ...
            .httpBasic((basic) -> basic
                .addObjectPostProcessor(new ObjectPostProcessor<BasicAuthenticationFilter>() {
                    @Override
                    public <O extends BasicAuthenticationFilter> O postProcess(O filter) {
                        filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());
                        return filter;
                    }
                })
            );
    

    This is documented in the Spring Security documentation.

    Spring Security 6.1 will also introduce a new method in the HTTP Basic DSL to configure the SecurityContextRepository.

    EDIT:

    In Spring Security 6.1, you can just do:

    http.httpBasic((basic) -> basic.securityContextRepository(new HttpSessionSecurityContextRepository()))