Java 11 and Spring Security 2.7.x here. I am trying to upgrade my config away from the (deprecated) WebSecurityConfigurerAdapter
-based implementation to one using SecurityFilterChain
.
What's important about my implementation is that I have the ability to define and configure/wire up my own:
- Authentication Filter (
UsernamePasswordAuthenticationFilter
impl) - Authorization Filter (
BasicAuthenticationFilter
impl) - Custom authentication error handler (
AuthenticationEntryPoint
impl) - Custom authorization error handler (
AccessDeniedHandler
impl)
Here's my current setup based on reading a bunch of blogs and articles:
public class ApiAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private ObjectMapper objectMapper;
private ApiAuthenticationFactory authenticationFactory;
private TokenService tokenService;
public ApiAuthenticationFilter(
AuthenticationManager authenticationManager,
TokenService tokenService,
ObjectMapper objectMapper,
ApiAuthenticationFactory authenticationFactory) {
super(authenticationManager);
this.tokenService = tokenService;
this.objectMapper = objectMapper;
this.authenticationFactory = authenticationFactory;
init();
}
private void init() {
setFilterProcessesUrl("/v1/auth/sign-in");
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
try {
SignInRequest signInRequest = objectMapper.readValue(request.getInputStream(), SignInRequest.class);
Authentication authentication = authenticationFactory
.createAuthentication(signInRequest.getEmail(), signInRequest.getPassword());
// perform authentication and -- if successful -- populate granted authorities
return authenticationManager.authenticate(authentication);
} catch (IOException e) {
throw new BadCredentialsException("malformed sign-in request payload", e);
}
}
@Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain,
Authentication authentication) {
// called if-and-only-if the attemptAuthentication method above is successful
ApiAuthentication apiAuthentication = (ApiAuthentication) authentication;
TokenPair tokenPair = tokenService.generateTokenPair(apiAuthentication);
response.setStatus(HttpServletResponse.SC_OK);
try {
response.getWriter().write(objectMapper.writeValueAsString(tokenPair));
} catch (IOException e) {
throw new ApiServiceException(e);
}
}
}
public class ApiAuthorizationFilter extends BasicAuthenticationFilter implements SecurityConstants {
private ApiAuthenticationFactory authenticationFactory;
private AuthenticationService authenticationService;
private String jwtSecret;
public ApiAuthorizationFilter(
AuthenticationManager authenticationManager,
ApiAuthenticationFactory authenticationFactory,
AuthenticationService authenticationService,
@Value("${myapp.jwt-secret}") String jwtSecret) {
super(authenticationManager);
this.authenticationFactory = authenticationFactory;
this.authenticationService = authenticationService;
this.jwtSecret = jwtSecret;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
String authHeader = request.getHeader(AUTHORIZATION_HEADER);
// allow the request through if no valid auth header is set; spring security
// will throw access denied exceptions downstream if the request is for an
// authenticated url
if (authHeader == null || !authHeader.startsWith(BEARER_TOKEN_PREFIX)) {
filterChain.doFilter(request, response);
return;
}
// otherwise an auth header was specified so lets take a look at it and grant access based
// on what we find
try {
DecodedJWT decodedJWT = JwtUtils.verifyToken(authHeader.replace(BEARER_TOKEN_PREFIX, ""), jwtSecret);
String subject = decodedJWT.getSubject();
ApiAuthentication authentication = authenticationFactory.createAuthentication(subject, null);
// TODO: I believe I need to look up granted authorities here and set them
authenticationService.setCurrentAuthentication(authentication);
filterChain.doFilter(request, response);
} catch (JWTVerificationException jwtVerificationEx) {
throw new AccessDeniedException("access denied", jwtVerificationEx);
}
}
}
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfigV2 {
private boolean securityDebug;
private ObjectMapper objectMapper;
private ApiAuthenticationFactory authenticationFactory;
private TokenService tokenService;
private AuthenticationService authenticationService;
private String jwtSecret;
private ApiUnauthorizedHandler unauthorizedHandler;
private ApiSignInFailureHandler signInFailureHandler;
private BCryptPasswordEncoder passwordEncoder;
private RealmService realmService;
@Autowired
public SecurityConfigV2(
@Value("${spring.security.debug:false}") boolean securityDebug,
ObjectMapper objectMapper,
ApiAuthenticationFactory authenticationFactory,
TokenService tokenService,
AuthenticationService authenticationService,
@Value("${myapp.authentication.jwt-secret}") String jwtSecret,
ApiUnauthorizedHandler unauthorizedHandler,
ApiSignInFailureHandler signInFailureHandler,
BCryptPasswordEncoder passwordEncoder,
RealmService realmService) {
this.securityDebug = securityDebug;
this.objectMapper = objectMapper;
this.authenticationFactory = authenticationFactory;
this.tokenService = tokenService;
this.authenticationService = authenticationService;
this.jwtSecret = jwtSecret;
this.unauthorizedHandler = unauthorizedHandler;
this.signInFailureHandler = signInFailureHandler;
this.passwordEncoder = passwordEncoder;
this.realmService = realmService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// build authentication manager
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(realmService)
.passwordEncoder(passwordEncoder)
.and()
.build(); // <-- calling it once up here, to get an AuthenticationManager instance
// enable CSRF
// TODO: enable once you are ready to provide 'CSRF tokens'
// https://stackoverflow.com/a/75646727/5235665
httpSecurity.csrf().disable();
// add CORS filter
httpSecurity.cors();
// add anonoymous/permitted paths (that is: what paths are allowed to bypass authentication)
httpSecurity.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers(HttpMethod.GET, "/actuator/health").permitAll()
.antMatchers(HttpMethod.POST, "/v*/tokens/refresh").permitAll();
// restrict all other paths and set them to authenticated
httpSecurity.authorizeRequests().anyRequest().authenticated();
// add authn + authz filters -- using AuthenticationManager instance here
httpSecurity.addFilter(apiAuthenticationFilter(authenticationManager));
httpSecurity.addFilter(apiAuthorizationFilter(authenticationManager));
// configure exception-handling for authn and authz
httpSecurity.exceptionHandling().accessDeniedHandler(unauthorizedHandler);
httpSecurity.exceptionHandling().authenticationEntryPoint(signInFailureHandler);
// configure stateless http sessions (appropriate for RESTful web services)
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// and building it a 2nd time here, to complete the filter
// but I believe this is what causes the error
return httpSecurity.build();
}
public ApiAuthenticationFilter apiAuthenticationFilter(AuthenticationManager authenticationManager) {
ApiAuthenticationFilter authenticationFilter = new ApiAuthenticationFilter(
authenticationManager, tokenService, objectMapper, authenticationFactory);
return authenticationFilter;
}
public ApiAuthorizationFilter apiAuthorizationFilter(AuthenticationManager authenticationManager) {
ApiAuthorizationFilter authorizationFilter = new ApiAuthorizationFilter(
authenticationManager,
authenticationFactory,
authenticationService,
jwtSecret);
return authorizationFilter;
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.debug(securityDebug)
.ignoring()
.antMatchers("/css/**", "/js/**", "/img/**", "/lib/**", "/favicon.ico");
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
corsConfiguration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return corsConfigurationSource;
}
}
When I start up my app I am getting:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name
'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration':
Unsatisfied dependency expressed through method 'setFilterChains' parameter 0; nested exception is
org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'filterChain' defined
in class path resource [myapp/ws/security/v2/SecurityConfigV2.class]:
Bean instantiation via factory method failed; nested exception is
org.springframework.beans.BeanInstantiationException:
Failed to instantiate [org.springframework.security.web.SecurityFilterChain]:
Factory method 'filterChain' threw exception;
nested exception is org.springframework.security.config.annotation.AlreadyBuiltException:
This object has already been built
The Google Gods say this is because I'm calling httpSecurity.build()
twice which is not allowed. However:
- My authn and authz filter require an
AuthenticationManager
instance; and - It seems that the only way (please tell me if I'm wrong!) to get an
AuthenticationManager
instance is to runhttpSecurity.build()
; but - I need the authn/authz filter before I can call
httpSecurity.build()
Can anyone help nudge me across the finish line here? Thanks for any and all help!