Issue
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();
}
}
Solution
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.
Answered By - Andrei Daneliuc
Answer Checked By - Marie Seifert (JavaFixing Admin)