springspring-bootspring-security

Spring security migration to 6: JSESSIONID cookie not accepted (401)


I was using Spring Boot 2.7 and I changed to Spring Boot 3, so Spring Security changed to 6.

My previous configuration was:

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
     .csrf().disable()
     .cors().and()
     .authorizeRequests().anyRequest().authenticated().and()
     .httpBasic().authenticationEntryPoint(authenticationEntryPoint()).and()
     .logout().logoutSuccessHandler(logoutSuccessHandler());
    return http.build();
  }

  private AuthenticationEntryPoint authenticationEntryPoint() {
    return new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED);
  }

  private LogoutSuccessHandler logoutSuccessHandler() {
    return new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK);
  }

I changed to:

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(AbstractHttpConfigurer::disable)
        .cors(withDefaults())
        .authorizeHttpRequests(authorizeHttpRequestsConfigurer -> authorizeHttpRequestsConfigurer./*requestMatchers(GET, SWAGGER_WHITELIST).permitAll().*/anyRequest().authenticated())
        .httpBasic(httpBasicConfigurer -> httpBasicConfigurer.authenticationEntryPoint(authenticationEntryPoint()))
        .logout(logoutConfigurer -> logoutConfigurer.logoutSuccessHandler(logoutSuccessHandler()));
    return http.build();
  }

I also changed:

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)

to:

@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)

Now requests including the Authorization header works well:

18:36:00.823 [http-nio-8080-exec-6] DEBUG o.s.security.web.FilterChainProxy - Securing GET /users/current
18:36:00.924 [http-nio-8080-exec-6] DEBUG o.s.s.w.a.w.BasicAuthenticationFilter - Set SecurityContextHolder to UsernamePasswordAuthenticationToken [Principal=com.example.UserDetailsAdapter@e98a174, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ADMIN]]
18:36:00.925 [http-nio-8080-exec-6] DEBUG o.s.security.web.FilterChainProxy - Secured GET /users/current

That request also sets the cookie JSESSIONID. But the next request using only the cookie (no authorization header) returns 401.

The logs are:

18:36:00.948 [http-nio-8080-exec-7] DEBUG o.s.security.web.FilterChainProxy - Securing GET /users/current
18:36:00.949 [http-nio-8080-exec-7] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext

I really don't know what failed in the migration, any advice?

Thanks in advance.

UPDATE:

Logs for request including auth header (that works well, return 200):

19:26:39.633 [http-nio-8080-exec-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
19:26:39.633 [http-nio-8080-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
19:26:39.635 [http-nio-8080-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 2 ms
19:26:39.695 [http-nio-8080-exec-1] TRACE o.s.security.web.FilterChainProxy - Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.session.DisableEncodeUrlFilter@4a0c04ab, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@453439e, org.springframework.security.web.context.SecurityContextHolderFilter@71de1091, org.springframework.security.web.header.HeaderWriterFilter@50008974, org.springframework.web.filter.CorsFilter@2d33795c, org.springframework.security.web.authentication.logout.LogoutFilter@1d1deb11, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@2aac87ab, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@53dbe7b2, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@520ec7a7, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@779ef5cb, org.springframework.security.web.access.ExceptionTranslationFilter@3a7dcfb7, org.springframework.security.web.access.intercept.AuthorizationFilter@a11efe6]] (1/1)
19:26:39.696 [http-nio-8080-exec-1] DEBUG o.s.security.web.FilterChainProxy - Securing OPTIONS /users/current
19:26:39.698 [http-nio-8080-exec-1] TRACE o.s.security.web.FilterChainProxy - Invoking DisableEncodeUrlFilter (1/12)
19:26:39.701 [http-nio-8080-exec-1] TRACE o.s.security.web.FilterChainProxy - Invoking WebAsyncManagerIntegrationFilter (2/12)
19:26:39.703 [http-nio-8080-exec-1] TRACE o.s.security.web.FilterChainProxy - Invoking SecurityContextHolderFilter (3/12)
19:26:39.704 [http-nio-8080-exec-1] TRACE o.s.security.web.FilterChainProxy - Invoking HeaderWriterFilter (4/12)
19:26:39.705 [http-nio-8080-exec-1] TRACE o.s.security.web.FilterChainProxy - Invoking CorsFilter (5/12)
19:26:39.723 [http-nio-8080-exec-1] TRACE o.s.s.w.h.writers.HstsHeaderWriter - Not injecting HSTS header since it did not match request to [Is Secure]
19:26:39.730 [http-nio-8080-exec-2] TRACE o.s.security.web.FilterChainProxy - Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.session.DisableEncodeUrlFilter@4a0c04ab, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@453439e, org.springframework.security.web.context.SecurityContextHolderFilter@71de1091, org.springframework.security.web.header.HeaderWriterFilter@50008974, org.springframework.web.filter.CorsFilter@2d33795c, org.springframework.security.web.authentication.logout.LogoutFilter@1d1deb11, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@2aac87ab, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@53dbe7b2, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@520ec7a7, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@779ef5cb, org.springframework.security.web.access.ExceptionTranslationFilter@3a7dcfb7, org.springframework.security.web.access.intercept.AuthorizationFilter@a11efe6]] (1/1)
19:26:39.730 [http-nio-8080-exec-2] DEBUG o.s.security.web.FilterChainProxy - Securing GET /users/current
19:26:39.730 [http-nio-8080-exec-2] TRACE o.s.security.web.FilterChainProxy - Invoking DisableEncodeUrlFilter (1/12)
19:26:39.731 [http-nio-8080-exec-2] TRACE o.s.security.web.FilterChainProxy - Invoking WebAsyncManagerIntegrationFilter (2/12)
19:26:39.731 [http-nio-8080-exec-2] TRACE o.s.security.web.FilterChainProxy - Invoking SecurityContextHolderFilter (3/12)
19:26:39.731 [http-nio-8080-exec-2] TRACE o.s.security.web.FilterChainProxy - Invoking HeaderWriterFilter (4/12)
19:26:39.732 [http-nio-8080-exec-2] TRACE o.s.security.web.FilterChainProxy - Invoking CorsFilter (5/12)
19:26:39.735 [http-nio-8080-exec-2] TRACE o.s.security.web.FilterChainProxy - Invoking LogoutFilter (6/12)
19:26:39.736 [http-nio-8080-exec-2] TRACE o.s.s.w.a.logout.LogoutFilter - Did not match request to Or [Ant [pattern='/logout', GET], Ant [pattern='/logout', POST], Ant [pattern='/logout', PUT], Ant [pattern='/logout', DELETE]]
19:26:39.736 [http-nio-8080-exec-2] TRACE o.s.security.web.FilterChainProxy - Invoking BasicAuthenticationFilter (7/12)
19:26:39.738 [http-nio-8080-exec-2] TRACE o.s.s.w.a.w.BasicAuthenticationFilter - Found username 'usp' in Basic Authorization header
19:26:39.738 [http-nio-8080-exec-2] TRACE o.s.s.w.c.HttpSessionSecurityContextRepository - No HttpSession currently exists
19:26:39.738 [http-nio-8080-exec-2] TRACE o.s.s.w.c.SupplierDeferredSecurityContext - Created SecurityContextImpl [Null authentication]
19:26:39.739 [http-nio-8080-exec-2] TRACE o.s.s.w.c.SupplierDeferredSecurityContext - Created SecurityContextImpl [Null authentication]
19:26:39.960 [http-nio-8080-exec-2] DEBUG o.s.s.w.a.w.BasicAuthenticationFilter - Set SecurityContextHolder to UsernamePasswordAuthenticationToken [Principal=com.example.UserDetailsAdapter@13ea0982, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ADMIN]]
19:26:39.961 [http-nio-8080-exec-2] TRACE o.s.security.web.FilterChainProxy - Invoking RequestCacheAwareFilter (8/12)
19:26:39.961 [http-nio-8080-exec-2] TRACE o.s.s.w.s.HttpSessionRequestCache - matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided
19:26:39.961 [http-nio-8080-exec-2] TRACE o.s.security.web.FilterChainProxy - Invoking SecurityContextHolderAwareRequestFilter (9/12)
19:26:39.962 [http-nio-8080-exec-2] TRACE o.s.security.web.FilterChainProxy - Invoking AnonymousAuthenticationFilter (10/12)
19:26:39.962 [http-nio-8080-exec-2] TRACE o.s.security.web.FilterChainProxy - Invoking ExceptionTranslationFilter (11/12)
19:26:39.963 [http-nio-8080-exec-2] TRACE o.s.security.web.FilterChainProxy - Invoking AuthorizationFilter (12/12)
19:26:39.964 [http-nio-8080-exec-2] TRACE o.s.s.w.a.i.RequestMatcherDelegatingAuthorizationManager - Authorizing SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@523c08be]
19:26:39.965 [http-nio-8080-exec-2] TRACE o.s.s.w.a.i.RequestMatcherDelegatingAuthorizationManager - Checking authorization on SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@523c08be] using org.springframework.security.authorization.AuthenticatedAuthorizationManager@55258ae6
19:26:39.965 [http-nio-8080-exec-2] TRACE o.s.s.w.a.AnonymousAuthenticationFilter - Did not set SecurityContextHolder since already authenticated UsernamePasswordAuthenticationToken [Principal=com.example.UserDetailsAdapter@13ea0982, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ADMIN]]

Logs for request without auth header, but with JSESSIONID (works bad, returns 401):

19:26:39.967 [http-nio-8080-exec-2] DEBUG o.s.security.web.FilterChainProxy - Secured GET /users/current
19:26:40.032 [http-nio-8080-exec-2] TRACE o.s.s.w.h.writers.HstsHeaderWriter - Not injecting HSTS header since it did not match request to [Is Secure]
19:26:40.064 [http-nio-8080-exec-3] TRACE o.s.security.web.FilterChainProxy - Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.session.DisableEncodeUrlFilter@4a0c04ab, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@453439e, org.springframework.security.web.context.SecurityContextHolderFilter@71de1091, org.springframework.security.web.header.HeaderWriterFilter@50008974, org.springframework.web.filter.CorsFilter@2d33795c, org.springframework.security.web.authentication.logout.LogoutFilter@1d1deb11, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@2aac87ab, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@53dbe7b2, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@520ec7a7, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@779ef5cb, org.springframework.security.web.access.ExceptionTranslationFilter@3a7dcfb7, org.springframework.security.web.access.intercept.AuthorizationFilter@a11efe6]] (1/1)
19:26:40.066 [http-nio-8080-exec-3] DEBUG o.s.security.web.FilterChainProxy - Securing GET /users/current
19:26:40.066 [http-nio-8080-exec-3] TRACE o.s.security.web.FilterChainProxy - Invoking DisableEncodeUrlFilter (1/12)
19:26:40.067 [http-nio-8080-exec-3] TRACE o.s.security.web.FilterChainProxy - Invoking WebAsyncManagerIntegrationFilter (2/12)
19:26:40.068 [http-nio-8080-exec-3] TRACE o.s.security.web.FilterChainProxy - Invoking SecurityContextHolderFilter (3/12)
19:26:40.068 [http-nio-8080-exec-3] TRACE o.s.security.web.FilterChainProxy - Invoking HeaderWriterFilter (4/12)
19:26:40.068 [http-nio-8080-exec-3] TRACE o.s.security.web.FilterChainProxy - Invoking CorsFilter (5/12)
19:26:40.069 [http-nio-8080-exec-3] TRACE o.s.security.web.FilterChainProxy - Invoking LogoutFilter (6/12)
19:26:40.070 [http-nio-8080-exec-3] TRACE o.s.s.w.a.logout.LogoutFilter - Did not match request to Or [Ant [pattern='/logout', GET], Ant [pattern='/logout', POST], Ant [pattern='/logout', PUT], Ant [pattern='/logout', DELETE]]
19:26:40.070 [http-nio-8080-exec-3] TRACE o.s.security.web.FilterChainProxy - Invoking BasicAuthenticationFilter (7/12)
19:26:40.070 [http-nio-8080-exec-3] TRACE o.s.s.w.a.w.BasicAuthenticationFilter - Did not process authentication request since failed to find username and password in Basic Authorization header
19:26:40.070 [http-nio-8080-exec-3] TRACE o.s.security.web.FilterChainProxy - Invoking RequestCacheAwareFilter (8/12)
19:26:40.070 [http-nio-8080-exec-3] TRACE o.s.s.w.s.HttpSessionRequestCache - matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided
19:26:40.070 [http-nio-8080-exec-3] TRACE o.s.security.web.FilterChainProxy - Invoking SecurityContextHolderAwareRequestFilter (9/12)
19:26:40.071 [http-nio-8080-exec-3] TRACE o.s.security.web.FilterChainProxy - Invoking AnonymousAuthenticationFilter (10/12)
19:26:40.071 [http-nio-8080-exec-3] TRACE o.s.security.web.FilterChainProxy - Invoking ExceptionTranslationFilter (11/12)
19:26:40.071 [http-nio-8080-exec-3] TRACE o.s.security.web.FilterChainProxy - Invoking AuthorizationFilter (12/12)
19:26:40.071 [http-nio-8080-exec-3] TRACE o.s.s.w.a.i.RequestMatcherDelegatingAuthorizationManager - Authorizing SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@585e8e91]
19:26:40.071 [http-nio-8080-exec-3] TRACE o.s.s.w.a.i.RequestMatcherDelegatingAuthorizationManager - Checking authorization on SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@585e8e91] using org.springframework.security.authorization.AuthenticatedAuthorizationManager@55258ae6
19:26:40.071 [http-nio-8080-exec-3] TRACE o.s.s.w.c.HttpSessionSecurityContextRepository - No HttpSession currently exists
19:26:40.071 [http-nio-8080-exec-3] TRACE o.s.s.w.c.SupplierDeferredSecurityContext - Created SecurityContextImpl [Null authentication]
19:26:40.071 [http-nio-8080-exec-3] TRACE o.s.s.w.c.SupplierDeferredSecurityContext - Created SecurityContextImpl [Null authentication]
19:26:40.078 [http-nio-8080-exec-3] TRACE o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]]
19:26:40.081 [http-nio-8080-exec-3] TRACE o.s.s.w.a.ExceptionTranslationFilter - Sending AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]] to authentication entry point since access is denied
org.springframework.security.access.AccessDeniedException: Access Denied
    at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:98)
    at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240)
...
19:26:40.093 [http-nio-8080-exec-3] TRACE o.s.s.w.s.HttpSessionRequestCache - Did not save request since it did not match [And [Not [Ant [pattern='/**/favicon.*']], Not [MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@3c0e8c92, matchingMediaTypes=[application/json], useEquals=false, ignoredMediaTypes=[*/*]]], Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], Not [MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@3c0e8c92, matchingMediaTypes=[multipart/form-data], useEquals=false, ignoredMediaTypes=[*/*]]], Not [MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@3c0e8c92, matchingMediaTypes=[text/event-stream], useEquals=false, ignoredMediaTypes=[*/*]]]]]
19:26:40.094 [http-nio-8080-exec-3] TRACE o.s.s.w.h.writers.HstsHeaderWriter - Not injecting HSTS header since it did not match request to [Is Secure]

Solution

  • The 5.8 migration guide outlines changes to saving the SecurityContext using the SecurityContextRepository. Each authentication filter is now responsible for saving the SecurityContext. Since HTTP Basic authentication is stateless, the BasicAuthenticationFilter does not save the SecurityContext to a session, and so does not trigger the creation of a session.

    NOTE: Please don't disable CSRF protection. Whether your application is using sessions or HTTP Basic authentication, it is vulnerable to CSRF attacks.

    Having said that, if something in your environment is triggering session creation (it's not clear what from your question, as it is missing some details), you may see a JSESSIONID but your previous request's SecurityContext is not restored simply by providing that cookie. You will need to provide credentials for each request.

    If this breaking change affects you and you cannot change your client (e.g. browser application) to do this, or you have a reason for not wanting to do this (such as performance of validating credentials on each request), you have a couple of options.

    1. You can restore the 5.8 and earlier default of saving the SecurityContext on every request. This is not recommended generally, but could be fine for the meantime to get you through the migration.
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // ...
            .securityContext((securityContext) -> securityContext
                .requireExplicitSave(false)
            );
        return http.build();
    }
    
    1. You can save the SecurityContext yourself, but you would need to call a custom /login endpoint to trigger this.
    @RestController
    class LoginController {
    
        private final SecurityContextHolderStrategy contextHolderStrategy =
                SecurityContextHolder.getContextHolderStrategy();
    
        private final HttpSessionSecurityContextRepository securityContextRepository =
            new HttpSessionSecurityContextRepository();
    
        @PostMapping("/login")
        @ResponseStatus(HttpStatus.NO_CONTENT)
        public void login(
                HttpServletRequest request,
                HttpServletResponse response,
                Authentication authentication) {
    
            var securityContext = this.contextHolderStrategy.createEmptyContext();
            securityContext.setAuthentication(authentication);
            this.securityContextRepository.saveContext(securityContext, request, response);
        }
    
    }
    
    1. You can customize Spring Security's BasicAuthenticationFilter to save the SecurityContext for you.
    @Bean 
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // ...
            .httpBasic((httpBasic) -> httpBasic
                .securityContextRepository(new HttpSessionSecurityContextRepository())
            );
        return http.build();
    }