Why RequestHeaderAuthenticationFilter is not registered as part of Spring Security Filter Chain

Background
While I was writing for another blog post, I realize something interesting about RequestHeaderAuthenticationFilter where it isn't registered as part of the SecurityFilterChain. I thought this post should come first, to provide some sort of background knowledge.
Imagine the following configuration
@EnableWebSecurity(debug = true)
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfig {
@Bean
public RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter(AuthenticationManager authenticationManager) {
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter();
requestHeaderAuthenticationFilter.setPrincipalRequestHeader("X-User");
requestHeaderAuthenticationFilter.setExceptionIfHeaderMissing(true);
requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
return requestHeaderAuthenticationFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.build();
}
}
When a request is sent to the server, the following SecurityFilterChain will be logged out
it is logged because I've set
debug = trueon@EnableWebSecurity
Security filter chain: [
DisableEncodeUrlFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
]
Did you notice what was not inside the SecurityFilterChain?
For me, it wasn't immediately obvious as I was expecting to see RequestHeaderAuthenticationFilter as part of the SecurityFilterChain especially when it is being mentioned in the documentation.

RequestHeaderAuthenticationFilter is a sub-class of AbstractPreAuthenticatedProcessingFilter
Understanding
So what happens? Why didn't it get registered as part of SecurityFilterChain?
RequestHeaderAuthenticationFilter
To understand that, we need to first look at the implementation of RequestHeaderAuthenticationFilter.
public class RequestHeaderAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {}
public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware {}
public abstract class GenericFilterBean implements Filter ... {}
What the above shows us is that RequestHeaderAuthenticationFilter is essentially a (Servlet) Filter, and the documentation states
Any
Servlet,Filter, or servlet*Listenerinstance that is a Spring bean is registered with the embedded container.
This means to say, if we look at the registered filters, we should be able to find RequestHeaderAuthenticationFilter.
Let's do that, and see if that's true by listing down all the registered filters which we can do by turning on the log to debug in application.yaml.
logging:
level:
org:
springframework:
security: TRACE
# this will display Mapping filters logs
boot:
web:
servlet:
ServletContextInitializerBeans: DEBUG
Servlet Filter
When the application is started, the mapping filters will be shown
2023-05-25 23:39:19.113 DEBUG 26544 --- [ restartedMain] o.s.b.w.s.ServletContextInitializerBeans : Mapping filters: springSecurityFilterChain urls=[/*] order=-100, filterRegistrationBean urls=[/*] order=2147483647, characterEncodingFilter urls=[/*] order=-2147483648, formContentFilter urls=[/*] order=-9900, requestContextFilter urls=[/*] order=-105, requestHeaderAuthenticationFilter urls=[/*] order=2147483647
We can see that it is indeed registered as part of the (Servlet) Filter.
Behavior
What does this mean? Should I be concerned? In my own opinion, yes, you should and I will explain why.
Default
First, let's see what's the default behavior by updating the current SecurityFilterChain to the following
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// added this
.authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
.build();
}
The new line means that any request to the server must come from an authenticated user. Once we update it, restart the application, and make an HTTP request to the server.
curl localhost:8080
You will encounter the following exception
2023-05-23 23:33:26.515 INFO 20680 --- [nio-8080-exec-2] Spring Security Debugger :
************************************************************
Request received for GET '/filters':
org.apache.catalina.connector.RequestFacade@5085c222
servletPath:/filters
pathInfo:null
headers:
host: localhost:8080
user-agent: curl/8.0.1
accept: */*
Security filter chain: [
DisableEncodeUrlFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
AuthorizationFilter
]
************************************************************
2023-05-23 23:33:26.520 TRACE 20680 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.session.DisableEncodeUrlFilter@50e12d12, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@c92f53f, org.springframework.security.web.context.SecurityContextPersistenceFilter@1f79d63b, org.springframework.security.web.header.HeaderWriterFilter@30aa5e7d, org.springframework.security.web.csrf.CsrfFilter@5534815, org.springframework.security.web.authentication.logout.LogoutFilter@2c8c013d, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@3e37cacf, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@2680de6e, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7ed6cc37, org.springframework.security.web.session.SessionManagementFilter@df1f38e, org.springframework.security.web.access.ExceptionTranslationFilter@48bae627, org.springframework.security.web.access.intercept.AuthorizationFilter@54410ddc]] (1/1)
2023-05-23 23:33:26.522 DEBUG 20680 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Securing GET /filters
2023-05-23 23:33:26.522 TRACE 20680 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/12)
2023-05-23 23:33:26.523 TRACE 20680 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/12)
2023-05-23 23:33:26.524 TRACE 20680 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking SecurityContextPersistenceFilter (3/12)
2023-05-23 23:33:26.524 TRACE 20680 --- [nio-8080-exec-2] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
2023-05-23 23:33:26.524 TRACE 20680 --- [nio-8080-exec-2] w.c.HttpSessionSecurityContextRepository : Created SecurityContextImpl [Null authentication]
2023-05-23 23:33:26.527 DEBUG 20680 --- [nio-8080-exec-2] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2023-05-23 23:33:26.529 TRACE 20680 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/12)
2023-05-23 23:33:26.530 TRACE 20680 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/12)
2023-05-23 23:33:26.531 TRACE 20680 --- [nio-8080-exec-2] o.s.security.web.csrf.CsrfFilter : Did not protect against CSRF since request did not match CsrfNotRequired [TRACE, HEAD, GET, OPTIONS]
2023-05-23 23:33:26.533 TRACE 20680 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking LogoutFilter (6/12)
2023-05-23 23:33:26.567 TRACE 20680 --- [nio-8080-exec-2] o.s.s.w.a.logout.LogoutFilter : Did not match request to Ant [pattern='/logout', POST]
2023-05-23 23:33:26.573 TRACE 20680 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking RequestCacheAwareFilter (7/12)
2023-05-23 23:33:26.603 TRACE 20680 --- [nio-8080-exec-2] o.s.s.w.s.HttpSessionRequestCache : No saved request
2023-05-23 23:33:26.612 TRACE 20680 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderAwareRequestFilter (8/12)
2023-05-23 23:33:26.619 TRACE 20680 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking AnonymousAuthenticationFilter (9/12)
2023-05-23 23:33:26.621 TRACE 20680 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking SessionManagementFilter (10/12)
2023-05-23 23:33:26.622 TRACE 20680 --- [nio-8080-exec-2] 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]]
2023-05-23 23:33:26.625 TRACE 20680 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking ExceptionTranslationFilter (11/12)
2023-05-23 23:33:26.628 TRACE 20680 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking AuthorizationFilter (12/12)
2023-05-23 23:33:26.630 TRACE 20680 --- [nio-8080-exec-2] estMatcherDelegatingAuthorizationManager : Authorizing SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@209a3372]
2023-05-23 23:33:26.637 TRACE 20680 --- [nio-8080-exec-2] estMatcherDelegatingAuthorizationManager : Checking authorization on SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@209a3372] using org.springframework.security.authorization.AuthenticatedAuthorizationManager@1f83758
2023-05-23 23:33:26.653 TRACE 20680 --- [nio-8080-exec-2] 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.access.AccessDeniedException: Access Denied
at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:98) ~[spring-security-web-5.8.3.jar:5.8.3]
I enable TRACE log for org.springframework.security so that it shows all the logs
But... what about my RequestHeaderAuthenticationFilter that supposed to authenticate my user via X-User header? Why is that not being triggered?

That's because based on the order of the SecurityFilterChain, AnonymousAuthenticationFilter will get processed first and throws Access Denied exception thus it will not reach the servlet Filter which is where RequestHeaderAuthenticationFilter is registered on.
AnonymousAuthenticationFilter
What if we disable AnonymousAuthenticationFilter?
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
return http
.anonymous(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
.build();
}
In that case, you will encounter the following
2023-05-25 22:10:48.429 INFO 29080 --- [nio-8080-exec-1] Spring Security Debugger :
************************************************************
Request received for GET '/filters':
org.apache.catalina.connector.RequestFacade@21a9c23e
servletPath:/filters
pathInfo:null
headers:
host: localhost:8080
user-agent: curl/8.0.1
accept: */*
x-user: A
Security filter chain: [
DisableEncodeUrlFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
SessionManagementFilter
ExceptionTranslationFilter
AuthorizationFilter
]
************************************************************
2023-05-25 22:10:48.438 TRACE 29080 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.session.DisableEncodeUrlFilter@7f18dabf, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@593ea2b2, org.springframework.security.web.context.SecurityContextPersistenceFilter@78115e1b, org.springframework.security.web.header.HeaderWriterFilter@6cfa908e, org.springframework.security.web.csrf.CsrfFilter@59929929, org.springframework.security.web.authentication.logout.LogoutFilter@58be5f1e, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@3d9ed909, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@62d540a8, org.springframework.security.web.session.SessionManagementFilter@7db987f9, org.springframework.security.web.access.ExceptionTranslationFilter@75c8948d, org.springframework.security.web.access.intercept.AuthorizationFilter@43fcbf99]] (1/1)
2023-05-25 22:10:48.442 DEBUG 29080 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Securing GET /filters
2023-05-25 22:10:48.448 TRACE 29080 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/11)
2023-05-25 22:10:48.456 TRACE 29080 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/11)
2023-05-25 22:10:48.460 TRACE 29080 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking SecurityContextPersistenceFilter (3/11)
2023-05-25 22:10:48.464 TRACE 29080 --- [nio-8080-exec-1] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
2023-05-25 22:10:48.467 TRACE 29080 --- [nio-8080-exec-1] w.c.HttpSessionSecurityContextRepository : Created SecurityContextImpl [Null authentication]
2023-05-25 22:10:48.467 DEBUG 29080 --- [nio-8080-exec-1] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2023-05-25 22:10:48.468 TRACE 29080 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/11)
2023-05-25 22:10:48.469 TRACE 29080 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/11)
2023-05-25 22:10:48.470 TRACE 29080 --- [nio-8080-exec-1] o.s.security.web.csrf.CsrfFilter : Did not protect against CSRF since request did not match CsrfNotRequired [TRACE, HEAD, GET, OPTIONS]
2023-05-25 22:10:48.472 TRACE 29080 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking LogoutFilter (6/11)
2023-05-25 22:10:48.473 TRACE 29080 --- [nio-8080-exec-1] o.s.s.w.a.logout.LogoutFilter : Did not match request to Ant [pattern='/logout', POST]
2023-05-25 22:10:48.475 TRACE 29080 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking RequestCacheAwareFilter (7/11)
2023-05-25 22:10:48.478 TRACE 29080 --- [nio-8080-exec-1] o.s.s.w.s.HttpSessionRequestCache : No saved request
2023-05-25 22:10:48.479 TRACE 29080 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderAwareRequestFilter (8/11)
2023-05-25 22:10:48.480 TRACE 29080 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking SessionManagementFilter (9/11)
2023-05-25 22:10:48.482 TRACE 29080 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking ExceptionTranslationFilter (10/11)
2023-05-25 22:10:48.483 TRACE 29080 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking AuthorizationFilter (11/11)
2023-05-25 22:10:48.484 TRACE 29080 --- [nio-8080-exec-1] estMatcherDelegatingAuthorizationManager : Authorizing SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@3e406d3a]
2023-05-25 22:10:48.485 TRACE 29080 --- [nio-8080-exec-1] estMatcherDelegatingAuthorizationManager : Checking authorization on SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@3e406d3a] using org.springframework.security.authorization.AuthenticatedAuthorizationManager@13bd82ef
2023-05-25 22:10:48.488 TRACE 29080 --- [nio-8080-exec-1] o.s.s.w.a.ExceptionTranslationFilter : Sending to authentication entry point since authentication failed
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
at org.springframework.security.web.access.intercept.AuthorizationFilter.getAuthentication(AuthorizationFilter.java:143) ~[spring-security-web-5.8.3.jar:5.8.3]
at org.springframework.security.authorization.AuthenticatedAuthorizationManager.check(AuthenticatedAuthorizationManager.java:115) ~[spring-security-core-5.8.3.jar:5.8.3]
// omitted
Let's zoom in on this particular message - An Authentication object was not found in the SecurityContext. This means there is no chance for Authentication to happen and SecurityContext was not constructed, since no Filter is handling it.
In short, you need to ensure that the Authentication filter is registered as part of the SecurityFilterChain. If that is the case, what can we do to ensure that?
Solution
Do not register as @Bean
Simply remove @Bean annotation on RequestHeaderAuthenticationFilter and register it as part of SecurityFilterChain manually
// removed @Bean
public RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter(AuthenticationManager authenticationManager) {
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter();
requestHeaderAuthenticationFilter.setPrincipalRequestHeader("X-User");
requestHeaderAuthenticationFilter.setExceptionIfHeaderMissing(true);
requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
return requestHeaderAuthenticationFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
return http
.anonymous(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
// register the filter manually
.addFilter(requestHeaderAuthenticationFilter(authenticationManager))
.build();
}
Now, in the SecurityFilterChain, we can see the following
Security filter chain: [
DisableEncodeUrlFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
RequestHeaderAuthenticationFilter << look at this
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
AuthorizationFilter
]
Notice that RequestHeaderAuthenticationFilter is now part of the SecurityFilterChain and no longer exist as part of the servlet Filter?
2023-05-25 23:45:05.014 DEBUG 26544 --- [ restartedMain] o.s.b.w.s.ServletContextInitializerBeans : Mapping filters: springSecurityFilterChain urls=[/*] order=-100, filterRegistrationBean urls=[/*] order=2147483647, filterRegistrationBean urls=[/*] order=2147483647, characterEncodingFilter urls=[/*] order=-2147483648, formContentFilter urls=[/*] order=-9900, requestContextFilter urls=[/*] order=-105
Now, if you make an HTTP request to the server, you will encounter the following error
curl localhost:8080
2023-05-23 23:45:53.779 ERROR 14644 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException: X-User header not found in request.
at org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter.getPreAuthenticatedPrincipal(RequestHeaderAuthenticationFilter.java:64) ~[spring-security-web-5.8.3.jar:5.8.3]
at org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter.doAuthenticate(AbstractPreAuthenticatedProcessingFilter.java:189) ~[spring-security-web-5.8.3.jar:5.8.3]
// omitted
We can see that RequestHeaderAuthenticationFilter is being triggered, and that it requires X-User header now, as per what we have configured.
Configure FilterRegistrationBean
What if I must register RequestHeaderAuthenticationFilter as a @Bean and I want to exclude it from registering with the Servlet Filter?
You can declare a FilterRegistrationBean and set it to false
@Bean
public RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter(AuthenticationManager authenticationManager) {
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter();
requestHeaderAuthenticationFilter.setPrincipalRequestHeader("X-User");
requestHeaderAuthenticationFilter.setExceptionIfHeaderMissing(true);
requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
return requestHeaderAuthenticationFilter;
}
@Bean
public FilterRegistrationBean<RequestHeaderAuthenticationFilter> registration(RequestHeaderAuthenticationFilter filter) {
FilterRegistrationBean<RequestHeaderAuthenticationFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
When the application start-up, it will show that RequestHeaderAuthenticationFilter is not being registered. Similar to what we have, when we don't register RequestHeaderAuthenticationFilter as a @Bean.
2023-05-25 23:45:05.014 DEBUG 26544 --- [ restartedMain] o.s.b.w.s.ServletContextInitializerBeans : Mapping filters: springSecurityFilterChain urls=[/*] order=-100, filterRegistrationBean urls=[/*] order=2147483647, filterRegistrationBean urls=[/*] order=2147483647, characterEncodingFilter urls=[/*] order=-2147483648, formContentFilter urls=[/*] order=-9900, requestContextFilter urls=[/*] order=-105
Conclusion
We looked at why a Filter - RequestHeaderAuthenticationFilter - is not automatically registered as part of SecurityFilterChain. And moved on to see how we can register it manually, and how to disable the automatic registration of Filter in Servlet via FilterRegistrationBean.
Knowing that Spring Boot automatically registers any Servlet, Filter, or servlet *Listener instance that is a Spring bean with the embedded container is important because sometimes you may encounter an issue where your Filter gets invoked twice.
Source Code
As usual, the full source code is available on GitHub.




