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