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

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

·

9 min read

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:
    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.

References