Description
Multiple SecurityFilterChain configured in security configuration doesn't work when deprecated authorizeRequests method is replaced by authorizeHttpRequests.
Expected behavior
The code with authorizeHttpRequests handle multiple SecurityFilterChains to process them.
Sample The code which was working (both endpoints response with 200):
@Configuration
@EnableWebSecurity
public class SecurityConfigInfo {
@Order(1)
@Bean
public SecurityFilterChain firstConfig(HttpSecurity http) throws Exception {
http.authorizeRequests(auth -> auth.requestMatchers("/actuator/health**").permitAll());
http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
http.httpBasic().disable();
http.csrf().disable().headers().frameOptions().sameOrigin();
return http.build();
}
@Order(2)
@Bean
public SecurityFilterChain secondConfig(HttpSecurity http) throws Exception {
http.authorizeRequests(auth -> auth.requestMatchers("/actuator/info").permitAll());
http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
http.httpBasic().disable();
http.csrf().disable().headers().frameOptions().sameOrigin();
return http.build();
}
}
when the authorizeHttpRequests method instead of deprecated authorizeRequests is used it doesn't work
@Configuration
@EnableWebSecurity
public class SecurityConfigInfo {
@Order(1)
@Bean
public SecurityFilterChain firstConfig(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth.requestMatchers("/actuator/health**").permitAll());
http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
http.httpBasic().disable();
http.csrf().disable().headers().frameOptions().sameOrigin();
return http.build();
}
@Order(2)
@Bean
public SecurityFilterChain secondConfig(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().requestMatchers("/actuator/info").permitAll();
http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
http.httpBasic().disable();
http.csrf().disable().headers().frameOptions().sameOrigin();
return http.build();
}
}
/actuator/health endpoint is working, however /actuator/info doesn't (status code 403). If I change the order the other way around then /info works and /health doesn't. So it looks like only first Bean is processed.
Thanks for your attention.
Comment From: marcusdacoregio
Hi @roman-vi,
Only the first SecurityFilterChain is picked up when you made the request because you are not using http.securityMatchers(...). Take a look at the documentation.
I also do not follow why you have two security filter chains with the same config. They could be simplified to:
@Order(1)
@Bean
public SecurityFilterChain firstConfig(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth.requestMatchers("/actuator/health", "/actuator/info").permitAll());
http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
http.httpBasic().disable();
http.csrf().disable().headers().frameOptions().sameOrigin();
return http.build();
}
Comment From: roman-vi
Hello @marcusdacoregio, thank you for the hint. I will try it. Could you please also explain a little bit about
http
.securityMatcher("/api/**")
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/user/**").hasRole("USER")
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(withDefaults());
Does it mean that /user/ and /admin/ should be under /api/ or it's not connected?
This was just a simple example to figure out why several SecurityFilterChain doesn't work together. In reality we have more complex configurations per set of endpoints to meet different usecase requirements.
Comment From: marcusdacoregio
Hi @roman-vi,
When you specify .securityMatcher("/api/**") you are telling that that SecurityFilterChain should be invoked for every request that starts with /api/.
The requestMatchers(...) on the other hand, determine the authorization rules that we should apply to a given request.
Does it mean that /user/ and /admin/ should be under /api/ or it's not connected?
Not necessarily, Spring Security won't consider the matcher that you used for securityMatcher in order to apply the requestMatchers.
I'll close this as solved but feel free to ask questions if you are in doubt yet.
Comment From: roman-vi
Thanks @marcusdacoregio for explanation, now it's clear.
Comment From: ImpThomasHein
Hi @marcusdacoregio , thanks a lot for your answers and your time. I am a colleague of Roman and I like to give a little bit more context. We used to work with spring booth 2.6 and did a configuration as Roman has described above. Since we moved to 3.x.x our security configuration ist not working anymore. With your help we came a step further.
Now we can make the authentifcation and authorization work, but the session creation is not working anymore. We have endpoints wich require different types of session creation. It would be great if you can support. Also a link to detailed documentation would be great. I think there is not enough hands on documentation for more complicated topics.
But thanks for that great work to the Spring framework.
http.authorizeHttpRequests()
.requestMatchers("/myinfineoninterestservice/**",
"/h2-console/**",
"/webjars/**",
"/swagger-resources/**",
"/api/applicaiton/v1/interests/evictCaches",
"/v2/api-docs")
.hasRole("DEVELOPMENT")
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.and()
.csrf()
.disable()
.headers()
.frameOptions()
.sameOrigin();
http.authorizeHttpRequests()
.requestMatchers("/actuator/prometheus**",
"/actuator/health**",
"/actuator/info**",
"/api/application/v1/interests/structure**")
.permitAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.httpBasic()
.disable()
.csrf()
.disable()
.headers()
.frameOptions()
.sameOrigin();
http.authorizeHttpRequests()
.requestMatchers("/api/application/v1/interests/anonymous/login*")
.permitAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.and()
.httpBasic()
.disable()
.csrf()
.disable();
Comment From: marcusdacoregio
Hi @ImpThomasHein.
but the session creation is not working anymore. We have endpoints which require different types of session creation.
If you can provide more details about how the sessions should be handled it would be helpful. But I think I got a suggestion based on the code you provided. You would move what is in the requestMatchers to securityMatcher in order to make the SecurityFilterChain be invoked only for those endpoints, like so:
http.securityMatchers((matchers) -> matchers
.requestMatchers("/myinfineoninterestservice/**",
"/h2-console/**",
"/webjars/**",
"/swagger-resources/**",
"/api/applicaiton/v1/interests/evictCaches",
"/v2/api-docs")
)
authorizeHttpRequests().anyRequest().hasRole("DEVELOPMENT")
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.and()
.csrf()
.disable()
.headers()
.frameOptions()
.sameOrigin();
http.securityMatchers((matchers) -> matchers
.requestMatchers("/actuator/prometheus**",
"/actuator/health**",
"/actuator/info**",
"/api/application/v1/interests/structure**")
)
.authorizeHttpRequests().anyRequest().permitAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.httpBasic()
.disable()
.csrf()
.disable()
.headers()
.frameOptions()
.sameOrigin();
http.securityMatchers((matchers) -> matches
.requestMatchers("/api/application/v1/interests/anonymous/login*")
)
.authorizeHttpRequests().anyRequest().permitAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.and()
.httpBasic()
.disable()
.csrf()
.disable();
Comment From: ImpThomasHein
Hi @marcusdacoregio, we finally made it work, thanks to your help. I wrote this small guide for our team. Maybe these points could be more considered in your documentation
Comment From: ImpThomasHein
In general what has changed
Regarding security, spring adapted their filters to get more performance, to simplify their matchers and to make the inclusion of beans more prominent. They really changed some core components and default implementations, which has impact on their api A good documentation of the API can be found here
https://docs.spring.io/spring-security/reference/5.8/migration/servlet/config.html https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html
What changed * Saving of the user authentication to sessions needs to be done manullay * Filters now need to be done with beans * The Security Matchers was included to divide between authn and authz
Switch to Beans and SecurityFilterChain
Old
@Configuration
public static class AnonymousSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(final HttpSecurity http) throws Exception {
//@formatter:off
final List<String> endpoints = Arrays.asList(
"/application/anonymous/profile*",
"/application/anonymous/profile/subscriptions*"
);
http
.requestMatchers()
.antMatchers(endpoints.toArray(new String[0]))
...
.authenticated();
//@formatter:on
}
}
New * Now a more common bean appraoch can be taken to define the filters
@Bean
public SecurityFilterChain noAuthenticationFilterChain(HttpSecurity http) throws Exception {
//@formatter:off
http.securityMatchers(matchers -> matchers.requestMatchers("/application/anonymous/profile*"))
.authorizeHttpRequests()
...
return http.build();
}
- It is important to know, that each configuration needs to be done within a specific beans
- 2 configurations in one bean would overwrite the previous settings
// not allowed
@Bean
public SecurityFilterChain noAuthenticationFilterChain(HttpSecurity http) throws Exception {
//@formatter:off
http.securityMatchers(matchers -> matchers.requestMatchers("/application/anonymous/profile*"))
.authorizeHttpRequests()
...
// another configuration
http.securityMatchers(matchers -> matchers.requestMatchers("/application/anonymous/aSecondEndpoint*"))
.authorizeHttpRequests()
...
return http.build();
}
// allowed
@Bean
public SecurityFilterChain firstAuthenticationFilterChain(HttpSecurity http) throws Exception {
//@formatter:off
http.securityMatchers(matchers -> matchers.requestMatchers("/application/anonymous/profile*"))
.authorizeHttpRequests()
...
return http.build();
}
@Bean
public SecurityFilterChain secondAuthenticationFilterChain(HttpSecurity http) throws Exception {
//@formatter:off
http.securityMatchers(matchers -> matchers.requestMatchers("/application/anonymous/aSecondEndpoint*"))
.authorizeHttpRequests()
...
return http.build();
}
Update the Requestmatcher
Old
http
.requestMatchers()
.antMatchers(endpoints.toArray(new String[0]))
.and()
New * In the past there were different types request matchers now there is good default behaviour * Also there is not just the authorizationHTTPRequestMatcher but also the security Requestmachter which is the new standard of matching paths * SecurityRequestmatcher * Pathmatching * Sessionmanagement * CSRF, Basicauth etc. * AuthorizationRequest * Which role is needed to have access and do I need to be authenticated
http.securityMatchers(matchers -> matchers.requestMatchers("/**"))
.authorizeHttpRequests()
.anyRequest <- actually means the one specified in line 1
Save the Sessions
Old
@Override
public AnonymousUserDetails generateAnonymousSession(final String loginHash, final Map<String, ?> parameterMap) {
final UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(null, loginHash);
final Authentication authentication = customAnonymousAuthenticationProvider.authenticate(usernamePasswordAuthenticationToken);
if (authentication == null || authentication.getPrincipal() == null) {
throw new BadCredentialsException("Invalid authentication returned.");
}
SecurityContextHolder.getContext().setAuthentication(authentication);
return ((AnonymousUserDetails) authentication.getPrincipal());
}
New * for performance reasons the sessions now needs to be exclusive saved * But the behaviour can also be turned off, see documentation
@Override
public AnonymousUserDetails generateAnonymousSession(final String loginHash, final Map<String, ?> parameterMap, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
final UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(null, loginHash);
final Authentication authentication = customAnonymousAuthenticationProvider.authenticate(usernamePasswordAuthenticationToken);
if (authentication == null || authentication.getPrincipal() == null) {
throw new BadCredentialsException("Invalid authentication returned.");
}
SecurityContextHolder.getContext().setAuthentication(authentication);
// this is new
securityContextRepository.saveContext(SecurityContextHolder.getContext(), httpServletRequest, httpServletResponse);
return ((AnonymousUserDetails) authentication.getPrincipal());
}
Complete example
Old
@Configuration
public static class AnonymousSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(final HttpSecurity http) throws Exception {
//@formatter:off
final List<String> endpoints = Arrays.asList(
"/application/anonymous/profile*",
"/application/anonymous/profile/subscriptions*"
);
http
.requestMatchers()
.antMatchers(endpoints.toArray(new String[0]))
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.and()
.httpBasic()
.disable()
.csrf()
.disable()
.logout()
.disable()
.authorizeRequests()
.anyRequest()
.authenticated();
//@formatter:on
}
}
New
@Bean
@Order(1)
public SecurityFilterChain noAuthenticationFilterChain(HttpSecurity http) throws Exception {
//@formatter:off
http.securityMatchers(matchers -> matchers.requestMatchers("/application/anonymous/profile*"))
.authorizeHttpRequests()
.anyRequest()
.permitAll()
.and()
.httpBasic()
.disable()
.csrf()
.disable()
.headers() // needed for h2 console to work
.frameOptions()
.sameOrigin()
.and()
.logout()
.disable();
//@formatter:on
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain basicAuthenticationFilterChain(HttpSecurity http) throws Exception {
//@formatter:off
http.securityMatchers(matchers -> matchers.requestMatchers("/application/anonymous/asecondendpoint*"))
.authorizeHttpRequests()
.anyRequest()
.authenticated
.and()
.httpBasic()
and()
.csrf()
.disable()
.headers() // needed for h2 console to work
.frameOptions()
.sameOrigin()
.and()
.logout()
.disable();
//@formatter:on
return http.build();
}
Comment From: doukhahmed
@marcusdacoregio Thank you for your clear and concise explanations. I have a question; is it possible to create another configuration class to separate role management in spring security? this class will contain a securityFilterChain configuration bean and will be annotated with @configuration and @@EnableWebSecurity thanks