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.