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 = true
on @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 *Listener
instance 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:
springframework:
security: TRACE
# this will display Mapping filters logs
boot:
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.
References
https://stackoverflow.com/questions/39314176/filter-invoke-twice-when-register-as-spring-bean
https://stackoverflow.com/questions/28421966/prevent-spring-boot-from-registering-a-servlet-filter