I have a (jwt) security enabled application and have written unit tests to verify the security filtering works as expected. With Spring Boot 3 these unit tests work fine, however, after upgrading to Spring Boot 4 they no longer work. The application, however works just fine and applies the security filtering as expected.
I have debugged this and found the issue to be with the calling order of the filter. With Spring Boot 4, inside the WebMvcTest, my authentication filter is called at the very beginning of the request handling even before trying to match it (and then again when applying the filter chain; this should be the first call). That first call does not happen in the Spring Boot 3 version and neither does it happen when running the application. It's that first call that breaks the test as the authentication is then no longer set properly when running the filter chain (it's a once per request filter so doesn't get called again).
Looks to me like a bug in how security/filtering is set up inside a WebMvcTest; it's clearly different from how the application normally sets it up. But perhaps there's something I overlooked or can/should do to fix this under Spring Boot 4?
I have created a small example project that illustrates the issue on github.
Example test log with Spring Boot 3:
2025-12-22T14:00:49.617+01:00 TRACE 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Trying to match request against DefaultSecurityFilterChain defined as 'filterChain' in [class path resource [com/hayobaan/securitytest/security/SecurityConfiguration.class]] matching [any request] and having filters [DisableEncodeUrl, WebAsyncManagerIntegration, SecurityContextHolder, HeaderWriter, Cors, Logout, JwtAuthentication, RequestCacheAware, SecurityContextHolderAwareRequest, AnonymousAuthentication, SessionManagement, ExceptionTranslation, Authorization] (1/1)
2025-12-22T14:00:49.617+01:00 DEBUG 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Securing GET /getUserInfo
2025-12-22T14:00:49.617+01:00 TRACE 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/13)
2025-12-22T14:00:49.617+01:00 TRACE 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/13)
2025-12-22T14:00:49.617+01:00 TRACE 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/13)
2025-12-22T14:00:49.617+01:00 TRACE 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/13)
2025-12-22T14:00:49.617+01:00 TRACE 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking CorsFilter (5/13)
2025-12-22T14:00:49.617+01:00 TRACE 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking LogoutFilter (6/13)
2025-12-22T14:00:49.617+01:00 TRACE 25899 --- [security-test] [ main] 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]]
2025-12-22T14:00:49.617+01:00 TRACE 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking JwtAuthenticationFilter (7/13)
2025-12-22T14:00:49.617+01:00 DEBUG 25899 --- [security-test] [ main] c.h.s.security.JwtAuthenticationFilter : Inside JwtAuthenticationFilter.doFilter
2025-12-22T14:00:49.617+01:00 DEBUG 25899 --- [security-test] [ main] c.h.s.security.JwtAuthenticationFilter : Inside JwtAuthenticationFilter.doFilterInternal
2025-12-22T14:00:49.618+01:00 DEBUG 25899 --- [security-test] [ main] c.h.s.security.JwtAuthenticationFilter : Authenticated: UsernamePasswordAuthenticationToken [Principal=authenticatedUser, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[User]]
2025-12-22T14:00:49.618+01:00 TRACE 25899 --- [security-test] [ main] .s.s.w.c.SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
2025-12-22T14:00:49.618+01:00 TRACE 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking RequestCacheAwareFilter (8/13)
2025-12-22T14:00:49.618+01:00 TRACE 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderAwareRequestFilter (9/13)
2025-12-22T14:00:49.618+01:00 TRACE 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking AnonymousAuthenticationFilter (10/13)
2025-12-22T14:00:49.618+01:00 TRACE 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking SessionManagementFilter (11/13)
2025-12-22T14:00:49.618+01:00 TRACE 25899 --- [security-test] [ main] o.s.s.w.a.AnonymousAuthenticationFilter : Did not set SecurityContextHolder since already authenticated UsernamePasswordAuthenticationToken [Principal=authenticatedUser, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[User]]
2025-12-22T14:00:49.618+01:00 TRACE 25899 --- [security-test] [ main] s.CompositeSessionAuthenticationStrategy : Preparing session with ChangeSessionIdAuthenticationStrategy (1/1)
2025-12-22T14:00:49.618+01:00 TRACE 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking ExceptionTranslationFilter (12/13)
2025-12-22T14:00:49.618+01:00 TRACE 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking AuthorizationFilter (13/13)
2025-12-22T14:00:49.618+01:00 TRACE 25899 --- [security-test] [ main] estMatcherDelegatingAuthorizationManager : Authorizing GET /getUserInfo
2025-12-22T14:00:49.618+01:00 TRACE 25899 --- [security-test] [ main] estMatcherDelegatingAuthorizationManager : Checking authorization on GET /getUserInfo using AuthorityAuthorizationManager[authorities=[User, Admin]]
2025-12-22T14:00:49.618+01:00 DEBUG 25899 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Secured GET /getUserInfo
2025-12-22T14:00:49.618+01:00 DEBUG 25899 --- [security-test] [ main] c.h.s.security.JwtAuthenticationFilter : Inside JwtAuthenticationFilter.doFilter
2025-12-22T14:00:49.618+01:00 TRACE 25899 --- [security-test] [ main] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
Same test log with Spring Boot 4 (stack trace stripped):
2025-12-22T14:02:58.044+01:00 DEBUG 26270 --- [security-test] [ main] c.h.s.security.JwtAuthenticationFilter : Inside JwtAuthenticationFilter.doFilter
2025-12-22T14:02:58.044+01:00 DEBUG 26270 --- [security-test] [ main] c.h.s.security.JwtAuthenticationFilter : Inside JwtAuthenticationFilter.doFilterInternal
2025-12-22T14:02:58.044+01:00 DEBUG 26270 --- [security-test] [ main] c.h.s.security.JwtAuthenticationFilter : Authenticated: UsernamePasswordAuthenticationToken [Principal=authenticatedUser, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[User]]
2025-12-22T14:02:58.044+01:00 TRACE 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Trying to match request against DefaultSecurityFilterChain defined as 'filterChain' in [class path resource [com/hayobaan/securitytest/security/SecurityConfiguration.class]] matching [any request] and having filters [DisableEncodeUrl, WebAsyncManagerIntegration, SecurityContextHolder, HeaderWriter, Cors, Logout, JwtAuthentication, RequestCacheAware, SecurityContextHolderAwareRequest, AnonymousAuthentication, SessionManagement, ExceptionTranslation, Authorization] (1/1)
2025-12-22T14:02:58.044+01:00 DEBUG 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Securing GET /getUserInfo
2025-12-22T14:02:58.044+01:00 TRACE 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/13)
2025-12-22T14:02:58.044+01:00 TRACE 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/13)
2025-12-22T14:02:58.044+01:00 TRACE 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/13)
2025-12-22T14:02:58.044+01:00 TRACE 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/13)
2025-12-22T14:02:58.044+01:00 TRACE 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking CorsFilter (5/13)
2025-12-22T14:02:58.045+01:00 TRACE 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking LogoutFilter (6/13)
2025-12-22T14:02:58.045+01:00 TRACE 26270 --- [security-test] [ main] o.s.s.w.a.logout.LogoutFilter : Did not match request to Or [PathPattern [GET /logout], PathPattern [POST /logout], PathPattern [PUT /logout], PathPattern [DELETE /logout]]
2025-12-22T14:02:58.045+01:00 TRACE 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking JwtAuthenticationFilter (7/13)
2025-12-22T14:02:58.045+01:00 DEBUG 26270 --- [security-test] [ main] c.h.s.security.JwtAuthenticationFilter : Inside JwtAuthenticationFilter.doFilter
2025-12-22T14:02:58.045+01:00 TRACE 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking RequestCacheAwareFilter (8/13)
2025-12-22T14:02:58.045+01:00 TRACE 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderAwareRequestFilter (9/13)
2025-12-22T14:02:58.045+01:00 TRACE 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking AnonymousAuthenticationFilter (10/13)
2025-12-22T14:02:58.045+01:00 TRACE 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking SessionManagementFilter (11/13)
2025-12-22T14:02:58.045+01:00 TRACE 26270 --- [security-test] [ main] .s.s.w.c.SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
2025-12-22T14:02:58.045+01:00 TRACE 26270 --- [security-test] [ main] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]]
2025-12-22T14:02:58.045+01:00 TRACE 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking ExceptionTranslationFilter (12/13)
2025-12-22T14:02:58.045+01:00 TRACE 26270 --- [security-test] [ main] o.s.security.web.FilterChainProxy : Invoking AuthorizationFilter (13/13)
2025-12-22T14:02:58.045+01:00 TRACE 26270 --- [security-test] [ main] estMatcherDelegatingAuthorizationManager : Authorizing GET /getUserInfo
2025-12-22T14:02:58.045+01:00 TRACE 26270 --- [security-test] [ main] estMatcherDelegatingAuthorizationManager : Checking authorization on GET /getUserInfo using AuthorityAuthorizationManager[authorities=[User, Admin]]
2025-12-22T14:02:58.045+01:00 TRACE 26270 --- [security-test] [ main] o.s.s.w.a.ExceptionTranslationFilter : Sending AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]] to authentication entry point since access is denied
org.springframework.security.authorization.AuthorizationDeniedException: Access Denied
at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:99) ~[spring-security-web-7.0.0.jar:7.0.0]
...
2025-12-22T14:02:58.046+01:00 DEBUG 26270 --- [security-test] [ main] o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access
2025-12-22T14:02:58.046+01:00 TRACE 26270 --- [security-test] [ main] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
Thanks to an answer on Spring support, I found the answer lies in updating the dependencies. Where with Sping Boot 3, org.springframework.security:spring-security-test sufficed as dependency, you now have to replace this with both org.springframework.boot:spring-boot-starter-security-test and org.springframework.boot:spring-boot-starter-webmvc-test. With those dependencies, handling of the filters is again as it should be and all tests succeed again. I have updated the test project on github to illustrate this.