Getting out of a codependent relationship, or: how I moved to a healthy component-based Spring Security configuration

by Joris KuipersFebruary 25, 2022
Holding Hands Together

In a recent blog post, the Spring Security team announced that they’ll be deprecating the configuration setup where you subclass the WebSecurityConfigurerAdapter. Instead, you simply define beans that allow you to set up the SecurityFilterChain by calling methods on the HttpSecurity object, and configure the WebSecurity object using a configurer.

These options have been available for a while, but if you had a working configuration then there wasn’t really a compelling need to rewrite your configuration. With the announcement that your configuration will eventually stop working, there’s an actual incentive. 

Therefore I tried to update the configuration for my current project, culminating in this blog. 

A Love Triangle

Our configuration defines a custom AuthenticationProvider, which knows how to accept a custom JWT Bearer token and then checks that against one of two configured AWS Cognito identity providers, as we have a dual region setup running on AWS (I might once write a separate blog post on our Cognito setup and Spring Security; ping me if you’re interested). 
This is accompanied by a custom authentication filter, which creates a custom Authentication token holding the JWT and passes that to the provider via the AuthenticationManager.

This is quite a common setup if you want to extend the framework with your own custom authentication mechanism: the idea is that the filter only knows how to create the Authentication token, and it’s the AuthenticationProvider that uses that token to come up with an Authentication object that represents an authenticated principal by using the provided info to perform the actual authentication.

In short, that means that:

  • The provider needs to be registered with the AuthenticationManager
  • Our filter needs to be injected with the AuthenticationManager
  • The filter needs to be inserted into the SecurityFilterChain
  • Only after that chain has been created, the AuthenticationManager is created

Ouch… We’re running into an unhealthy chain of dependencies here for our filter. So how do we untangle this unfortunate triangle?

Some Method That I Used To Know

In the situation where you extend the WebSecurityConfigurerAdapter, this problem is solved by providing you with an authenticationManagerBean method. It returns what’s effectively a lazy-loading proxy to the real AuthenticationManager, so you can inject that into your filter before the real manager has been created.
The typical way to use it is to override it so that you can annotate it with @Bean

@Bean(name = "authenticationManager")
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
   return super.authenticationManagerBean();
}

Now the AuthenticationManager is simply available as a bean that can be dependency injected.

However, with the component-based setup there are no helper methods like that anymore. It wasn’t obvious to me how to address this, so I set out to try some options.

Relationship Experiments

One of the first things I tried, after looking at how Spring Security itself deals with some of these issues, was to inject the HttpSecurity into the @Bean method that created the filter so that I could call httpSecurity.getSharedObject(AuthenticationManager.class) to obtain the AuthenticationManager, allowing me to pass it to the filter.
However, when that bean method is called that manager isn’t created yet, so that code simply returns null.

I experimented with some other setups as well, many of them actually resulting in a circular dependency from the configuration class on itself, preventing our Spring Boot services from starting up (this is not allowed by default since Spring Boot 2.6).

Happily Ever After?

After some further investigation I understood that Spring Security is still providing an AuthenticationManagerBuilder bean that you can use for configuration, which eventually creates the AuthenticationManager that we’re looking for.

For configuration purposes, you can autowire it in your config class, but you have to ensure that your class is annotated with @EnableGlobalAuthentication: either directly, or by using another annotation that has this as a meta-annotation. For example, this is how we can register our custom provider:

@Autowired
void registerProvider(AuthenticationManagerBuilder builder) {
   builder.authenticationProvider(new CognitoJwtAuthProvider());
}

I figured the builder would let me access the manager that it eventually builds, but of course by the time that all this configuration is still taking place that hasn’t happened yet. Spring does make it easy for components to receive a callback when the context is completely created / refreshed using the SmartInitializingSingleton interface, so eventually this is what I ended up with.
In our configuration class:

@Bean
JwtAuthenticationFilter jwtAuthFilter(AuthenticationManagerBuilder builder) {
   return new JwtAuthenticationFilter(builder);
}

In the filter class:

public class JwtAuthenticationFilter extends OncePerRequestFilter
                                     implements SmartInitializingSingleton
{
   private AuthenticationManagerBuilder authMgrBuilder;
   private AuthenticationManager authenticationManager;

   public JwtAuthenticationFilter(AuthenticationManagerBuilder authMgrBuilder) {
       this.authMgrBuilder = authMgrBuilder;
   }

   @Override
   public void afterSingletonsInstantiated() {
       this.authenticationManager = authMgrBuilder.getObject();
   }
…
}

This does the trick, but it feels pretty awkward to set up your filter this way. I had a look at how Spring Security typically does this for its own filters: that involves additional SecurityConfigurer implementations that can set the AuthenticationManager on the filter once it becomes available, thus managing the instantiation of the filter in different steps. I don’t need / want to write such a configurer just for this purpose, so for now I guess I’ll stick to this.

If you’re in a similar situation with a custom filter that needs the AuthenticationManager, I would like to hear about better approaches.
Please leave a comment if you came up with something; after all, I’m not married to my current code 😉