Spring Boot - permitAll not working using SecurityFilterChain
Asked Answered
S

4

6

I have a Spring Boot application where I need to make certain endpoints accessible to all users without requiring authentication. To achieve this, I am using permitAll().

However, when I try to access one of these endpoints, http://localhost:8080/content/123, a CustomOncePerRequestFilter is being called, which shouldn't happen as it is supposed to be accessible to all users.

The rest of the endpoints are secured and working as expected.

I am seeking guidance on why this is happening and how to fix it. My SecurityConfiguration code is as follows.

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

    @Autowired
    private CustomOncePerRequestFilter tokenFilter;

    @Bean
    protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeHttpRequests()
                .antMatchers("/art/**","/content/**").permitAll()
                .and().authorizeHttpRequests()
                .antMatchers("/api/contenteditor/feed/**").hasAnyRole("SITE_ADMIN", "STANDARD_ADMIN", "DOMAIN_MEMBER")
                .anyRequest().authenticated().and()
                .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        http.cors().configurationSource(source);
        return http.build();
    }

}
Sian answered 23/4, 2023 at 11:24 Comment(0)
S
3

Although it may seem strange, the only effective resolution I have found is to override the shouldNotFilter() method within my custom OncePerRequestFilter implementation and include all the paths that require exemption from authentication.

@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
    return request.getServletPath().startsWith("/art");
}
Sian answered 24/4, 2023 at 5:11 Comment(0)
P
2

In fact, it is working as intended. When you add a filter to the filter chain, and if a previous filter doesn’t short-circuit the chain, then your filter will be called for every single request.
permitAll() doesn’t short-circuit the filter chain, it just adds another entry to the authorization manager. The AuthorizationFilter (replacing a deprecated FilterSecurityInterceptor), which is one of the last filters called, will use the manager to decide if the request should be rejected or not.

So in your CustomOncePerRequestFilter, if you can’t authenticate the user, you just pass the request down the filter chain.

Protecting answered 23/4, 2023 at 13:21 Comment(2)
How to tell permitAll to short-circuit? Why would you permit all without short-circuiting? That doesn't really make sense from an API design point of view?Photometry
@Photometry The design is that each filter should have a precise role, so if you create an authentication filter, and during execution you can’t authenticate the user, you just pass the request to other filters to do their job. permitAll() works as expected and will authorize access to anonymous clients for matching requests. The rules you specify after authorizeHttpRequests() are about authorization and are implemented by a single filter (AuthorizationFilter), which can make a decision after all the auth filters have passed ; these rules do not change the behaviour of other filters.Protecting
P
2

The issue you are facing with your Custom OncePerRequestFilter is that if it is intended for user authentication it will be called before the Internal Spring Security Filter intended for deciding if the URI will be validated http.addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class). What you need to do is just invoke filterChain.doFilter(request, response) ignoring, for example, an empty Authorization header. Don't worry, the SecurityFilterChain will correctly consider your requestMatchings down the road and your requests will be validated as expected. Ex:

@Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Optional<String> token = Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION))
                .map(authHeader -> authHeader.substring(Constants.BEARER_HEADER.length()));

        if (token.isEmpty()) {
            filterChain.doFilter(request, response);
            return;
        }

        BasicJwtAuthenticationToken auth = token
                .map(this::createAuthenticationObject)
                .orElseThrow(UnauthorizedTokenException::new);
        ...
        filterChain.doFilter(request, response);

}
Pillion answered 14/12, 2023 at 6:37 Comment(0)
B
1

Looks like what you want it's something like this.

@Bean
  @Order(2)
  protected SecurityFilterChain filterChainPublic(HttpSecurity http) throws Exception {
    return http.csrf().disable()
        .authorizeHttpRequests()
        .antMatchers("/art/**")
        .permitAll()
        .and()
        .authorizeHttpRequests()
        .antMatchers("/**")
        .authenticated()
        .and()
        .cors()
        .disable()
        .build();
  }

  @Bean
  @Order(1)
  protected SecurityFilterChain filterChainPrivate(HttpSecurity http) throws Exception {
    return http.csrf().disable()
        .requestMatchers()
          .antMatchers("/api/**") // only apply the security configuration to requests that start with /api
          .and()
        .authorizeHttpRequests()
        .antMatchers("/api/contenteditor/feed/**").hasAnyRole("SITE_ADMIN", "STANDARD_ADMIN", "DOMAIN_MEMBER")
        .and()
        .addFilterBefore(new CustomOncePerRequestFilter(), UsernamePasswordAuthenticationFilter.class)
        .cors()
        .disable()
        .build();
  }
  
  
  

The provided code creates two security filter chains to filter elements based on their security level. The second bean applies to secured URLs, while the first bean is more open and does not require any special security configurations. Another option it's you just filtering it out the paths you don't want in your filter object.

How this code works

The filter chains allow you to have multiple chains for different base paths. The requestMatchers define the base paths that the SecurityFilterChain will work with. The antMatchers define pipeline elements conforming to patterns, and can be used with requestMatchers and authorizeHttpRequests().

Let's take as an example, the code I proposed the first time. What we are doing here it's define a more restrictive element and a open one for general security configurations.

Let's see this on code:

 http.csrf().disable()
    .requestMatchers()
    .antMatchers("/api/**") // only apply the security configuration to requests that start with /api
    .and()
    .authorizeHttpRequests()
    .antMatchers("/api/contenteditor/**")
    .hasAnyRole("SITE_ADMIN", "STANDARD_ADMIN", "DOMAIN_MEMBER")
    .and()
    .addFilterBefore(new CustomOnPerRequestFilter(), UsernamePasswordAuthenticationFilter.class)

This code specifies that the filter chain should only work with requests that start with /api/** and use the filter only in these base paths. It also specifies that requests conforming to /api/contenteditor/** require the SITE_ADMIN, STANDARD_ADMIN, or DOMAIN_MEMBER roles. The addFilterBefore method specifies that a custom CustomOnPerRequestFilter filter should be added before the UsernamePasswordAuthenticationFilter. Notice that your custom filter would be executed in any /api/** path not only in/api/contenteditor/**.

If we instead do like the first bean

http.csrf().disable()
    .authorizeHttpRequests()
    .antMatchers("/art/**")
    .permitAll()
    .and()
    .authorizeHttpRequests()
    .antMatchers("/**")
    .authenticated()

The requestMatcher by default is any path, so here we are operating over all the urls if we incluide a filter in here would be executed in any path that doesn't match the previous filter. The art endpoint is public but the rest are under authentication.

It's important to note that the order of the beans is essential, as Spring will use the first bean that conforms to the URL. Therefore, the most specific should be the first in order, and the most general should be the last.

enter image description here

Lastly, it's important not to annotate custom security filters as @Component or any other IOD annotation in this approach because Spring scans every bean that implements the filter and adds it to the regular filter chain.

Note that the provided code works with Spring Security 5, but in Spring Security 6, things will change. In Spring Security 6, you can use the following code:

    @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .authorizeHttpRequests(authorize -> {
            authorize.requestMatchers("/art/**")
                .permitAll();

            authorize.requestMatchers("/api/**")
                .hasAnyRole("SITE_ADMIN", "STANDARD_ADMIN", "DOMAIN_MEMBER")
                .anyRequest()
                .authenticated().and()
                .addFilterBefore(new CustomOnPerRequestFilter(), UsernamePasswordAuthenticationFilter.class);
        })
        .cors()
        .disable()
        .build(

); }

Basement answered 23/4, 2023 at 13:29 Comment(4)
I tried your code snippet. It didn't work. Is there official documentation supporting your statement?Sian
yeah of course docs.spring.io/spring-security/reference/servlet/…. Are you keeping the component annotation in the filters? Because then you are not registering the filters in the security chain but in the regular chain. Keep me updated and I will edit the original response with a more detailed answer if this works in your case.Basement
I removed Component annotation from my filter class. Yes please elaborate your answer. My requirement is that "/art/**" should be open for all. "/api/**" should be access by user having role "ADMIN" and rest should be access by an authenticated user.Sian
Hi Harsh, I wanted to let you know that I have made some updates to the comment. Please keep in mind that maybe we are using different versions, and I have also included some minor considerations that I think may be helpful. I have tested this on my local machine and it seems to be running properly based on your requirements. Let me know if you have any questions or concerns.Basement

© 2022 - 2024 — McMap. All rights reserved.