Spring Security exposing AuthenticationManager without WebSecurityConfigurerAdapter

Viewed 8003

I'm trying incoming Spring Boot 2.7.0-SNAPSHOT, which uses Spring Security 5.7.0, which deprecate WebSecurityConfigurerAdapter.

I read this blog post, but I'm not sure to understand how I can expose the default implementation of AuthenticationManager to my JWT authorization filter.

The old WebSecurityConfig, using WebSecurityConfigurerAdapter (works fine) :

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JWTTokenUtils jwtTokenUtils;

    @Bean
    protected AuthenticationManager getAuthenticationManager() throws Exception {
        return authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // disable CSRF as we do not serve browser clients
                .csrf().disable()
                // allow access restriction using request matcher
                .authorizeRequests()
                // authenticate requests to GraphQL endpoint
                .antMatchers("/graphql").authenticated()
                // allow all other requests
                .anyRequest().permitAll().and()
                // JWT authorization filter
                .addFilter(new JWTAuthorizationFilter(getAuthenticationManager(), jwtTokenUtils))
                // make sure we use stateless session, session will not be used to store user's state
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

}

The new WebSecurityConfig :

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Autowired
    private JWTTokenUtils jwtTokenUtils;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        final AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
        http
                // disable CSRF as we do not serve browser clients
                .csrf().disable()
                // allow access restriction using request matcher
                .authorizeRequests()
                // authenticate requests to GraphQL endpoint
                .antMatchers("/graphql").authenticated()
                // allow all other requests
                .anyRequest().permitAll().and()
                // JWT authorization filter
                .addFilter(new JWTAuthorizationFilter(authenticationManager, jwtTokenUtils))
                // make sure we use stateless session, session will not be used to store user's state
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        return http.build();
    }

}

As you see I have no AuthenticationManager exposed bean anymore. I cannot get it from the WebSecurityConfigurerAdapter. So I tried to get it directly from the HttpSecurity in the filterChain method, so I can pass it to my JWT filter directly.

But I still need an AuthenticationManager bean to be exposed to my JWTAuthorizationFilter :

Parameter 0 of constructor in com.example.config.security.JWTAuthorizationFilter required a bean of type 'org.springframework.security.authentication.AuthenticationManager' that could not be found.

How can I expose it?

Here is the JWT authorization filter (checks the token and authenticate the user, I have a custom UserDetailsService which do the credentials check in the database) :

@Component
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    private final JWTTokenUtils jwtTokenUtils;

    public JWTAuthorizationFilter(AuthenticationManager authManager, JWTTokenUtils jwtTokenUtils) {
        super(authManager);
        this.jwtTokenUtils = jwtTokenUtils;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {

        // retrieve request authorization header
        final String authorizationHeader = req.getHeader("Authorization");

        // authorization header must be set and start with Bearer
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {

            // decode JWT token
            final JWTTokenPayload jwtTokenPayload = jwtTokenUtils.decodeToken(authorizationHeader);

            // if user e-mail has been retrieved correctly from the token and if user is not already authenticated
            if (jwtTokenPayload.getEmail() != null && SecurityContextHolder.getContext().getAuthentication() == null) {

                // authenticate user
                final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(jwtTokenPayload.getEmail(), null, Collections.singletonList(jwtTokenPayload.getRole()));

                // set authentication in security context holder
                SecurityContextHolder.getContext().setAuthentication(authentication);

            } else {
                log.error("Valid token contains no user info");
            }
        }
        // no token specified
        else {
            res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        }

        // pass request down the chain, except for OPTIONS requests
        if (!"OPTIONS".equalsIgnoreCase(req.getMethod())) {
            chain.doFilter(req, res);
        }

    }

}

EDIT :

I realized I can manage to get the authenticationManager in my JWT filter using the method provided in this issue, but still I need an AuthenticationManager to be exposed globally because I also need it in my controller.

Here is the authentication controller which need the authenticationManager to be injected :

@RestController
@CrossOrigin
@Component
public class AuthController {

    @Autowired
    private JWTTokenUtils jwtTokenUtils;

    @Autowired
    private AuthenticationManager authenticationManager;

    @RequestMapping(value = "/authenticate", method = RequestMethod.POST)
    public ResponseEntity<?> authenticate(@RequestBody JWTRequest userRequest) {

        // try to authenticate user using specified credentials
        final Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userRequest.getEmail(), userRequest.getPassword()));

        // if authentication succeeded and is not anonymous
        if (authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated()) {

            // set authentication in security context holder
            SecurityContextHolder.getContext().setAuthentication(authentication);

            // get authorities, we should have only one role per member so simply get the first one
            final GrantedAuthority grantedAuthority = authentication.getAuthorities().iterator().next();

            // generate new JWT token
            final String jwtToken = jwtTokenUtils.generateToken(authentication.getPrincipal(), grantedAuthority);

            // return response containing the JWT token
            return ResponseEntity.ok(new JWTResponse(jwtToken));
        }

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

    }

}
3 Answers

Local AuthenticationManager

A solution to be able to get and pass the AuthenticationManager (which you cannot get anymore from the deprecated WebSecurityConfigurerAdapter) to the filter, is to have a dedicated configurer which will be responsible for adding the filter. (This is inspired from the solution provided here. Edit : and now officially in the documentation).

Create a custom HTTP configurer :

@Component
public class JWTHttpConfigurer extends AbstractHttpConfigurer<JWTHttpConfigurer, HttpSecurity> {

    private final JWTTokenUtils jwtTokenUtils;

    public JWTHttpConfigurer(JWTTokenUtils jwtTokenUtils) {
        this.jwtTokenUtils = jwtTokenUtils;
    }

    @Override
    public void configure(HttpSecurity http) {
        final AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
        http.antMatcher("/graphql").addFilter(new JWTAuthorizationFilter(authenticationManager, jwtTokenUtils));
    }

}

Then simply apply it in the security config :

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Autowired
    private JWTTokenUtils jwtTokenUtils;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // disable CSRF as we do not serve browser clients
                .csrf().disable()
                // allow access restriction using request matcher
                .authorizeRequests()
                // authenticate requests to GraphQL endpoint
                .antMatchers("/graphql").authenticated()
                // allow all other requests
                .anyRequest().permitAll().and()
                // JWT authorization filter
                .apply(new JWTHttpConfigurer(jwtTokenUtils)).and()
                // make sure we use stateless session, session will not be used to store user's state
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        return http.build();
    }

}

Global AuthenticationManager

In some cases you need to expose the authentication manager globally so it is available anywhere in your application.

A solution to have the AuthenticationManager bean in the Spring context is to get it from the AuthenticationConfiguration which exports the authentication configuration (credits to Andrei Daneliuc's answer below) :

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
    return authenticationConfiguration.getAuthenticationManager();
}

Then if you need to retrieve it in your filter chain, you can use authenticationManager(http.getSharedObject(AuthenticationConfiguration.class)).

So the whole security config would be :

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Autowired
    private JWTTokenUtils jwtTokenUtils;

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // disable CSRF as we do not serve browser clients
                .csrf().disable()
                // match GraphQL endpoint
                .antMatcher("/graphql")
                // add JWT authorization filter
                .addFilter(new JWTAuthorizationFilter(authenticationManager(http.getSharedObject(AuthenticationConfiguration.class)), jwtTokenUtils))
                // allow access restriction using request matcher
                .authorizeRequests()
                // authenticate requests to GraphQL endpoint
                .antMatchers("/graphql").authenticated()
                // allow all other requests
                .anyRequest().permitAll().and()
                // make sure we use stateless session, session will not be used to store user's state
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        return http.build();
    }

}

Another solution to expose an authentication manager globally is to use a custom AuthenticationManager, as a bean available to the entire application, which does quite the same thing as, in our case, the default DaoAuthenticationProvider implementation (i.e. use the custom UserDetailsService to get user details from the database, verify the password using the configured PasswordEncoder, then return a UsernamePasswordAuthenticationToken to present the Authentication) :

@Component
public class CustomAuthenticationManager implements AuthenticationManager {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Bean
    protected PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        final UserDetails userDetail = customUserDetailsService.loadUserByUsername(authentication.getName());
        if (!passwordEncoder().matches(authentication.getCredentials().toString(), userDetail.getPassword())) {
            throw new BadCredentialsException("Wrong password");
        }
        return new UsernamePasswordAuthenticationToken(userDetail.getUsername(), userDetail.getPassword(), userDetail.getAuthorities());
    }

}

So that you can use it in the security config when adding the filter :

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Autowired
    private JWTTokenUtils jwtTokenUtils;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // disable CSRF as we do not serve browser clients
                .csrf().disable()
                // match GraphQL endpoint
                .antMatcher("/graphql")
                // add JWT authorization filter
                .addFilter(new JWTAuthorizationFilter(new CustomAuthenticationManager(), jwtTokenUtils))
                // allow access restriction using request matcher
                .authorizeRequests()
                // authenticate requests to GraphQL endpoint
                .antMatchers("/graphql").authenticated()
                // allow all other requests
                .anyRequest().permitAll().and()
                // make sure we use stateless session, session will not be used to store user's state
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        return http.build();
    }

}

And it can be injected anywhere else in the application, i.e. in a controller :

@RestController
@CrossOrigin
@Component
public class AuthController {

    @Autowired
    private JWTTokenUtils jwtTokenUtils;

    @Autowired
    private CustomAuthenticationManager authenticationManager;

    @RequestMapping(value = "/authenticate", method = RequestMethod.POST)
    public ResponseEntity<?> authenticate(@RequestBody JWTRequest userRequest) {

        // try to authenticate user using specified credentials
        final Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userRequest.getEmail(), userRequest.getPassword()));

        // if authentication succeeded and is not anonymous
        if (authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated()) {

            // set authentication in security context holder
            SecurityContextHolder.getContext().setAuthentication(authentication);

            // get authorities, we should have only one role per member so simply get the first one
            final GrantedAuthority grantedAuthority = authentication.getAuthorities().iterator().next();

            // generate new JWT token
            final String jwtToken = jwtTokenUtils.generateToken(authentication.getPrincipal(), grantedAuthority);

            // return response containing the JWT token
            return ResponseEntity.ok(new JWTResponse(jwtToken));
        }

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

    }

}

Note that you may want to also use a custom AuthenticationEntryPoint, to return a 401 instead of a 500 when BadCredentialsException is raised.

If you want the AuthenticationManager bean to be in the spring context, you can use the following solution.

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
     return authenticationConfiguration.getAuthenticationManager();
}

This approach has solved the problem for me and you can inject AuthenticationManager wherever you need.

Related