springsecurityhttp-status-code-403

Receiving 403 Forbidden error for controller methods with HTTP methods PUT or POST. This is happening after migrating from Springboot 2.7.17 to 3.1.5


I have upgraded Springboot from 2.7.17 to 3.1.5. I have updated the security config classes by following the guide here. Some issues arose which I was able to fix. However, Now, I'm stuck with 403 forbidden issue for apis with http methods PUT or POST.

Apis which are failing:

POST /app/order/comment

PUT /app/order

Apis which are successful :

All apis with GET.

My Security Config

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@EnableConfigurationProperties(AppConfigProperties.class)
@EnableMethodSecurity(prePostEnabled = true)
public class WebappSecurityConfiguration {

    private static final String CUSTOM_CSRF_COOKIE_NAME = "CUSTOM-XSRF-TOKEN";

    @Bean
    @Order(1)
    SecurityFilterChain internalAPISecurityConfig(HttpSecurity http, AppConfigProperties configProps) throws Exception {

        String camundaWebAppUrlPattern = "/camunda/**";

        CookieCsrfTokenRepository csrfRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();

       
        csrfRepository.setCookieName(CUSTOM_CSRF_COOKIE_NAME);

        http
            .csrf(csrf -> csrf
                    .ignoringRequestMatchers(AntPathRequestMatcher.antMatcher(camundaWebAppUrlPattern))
                    .csrfTokenRepository(csrfRepository)
            )
            .authorizeHttpRequests(authorize -> {
                authorize.requestMatchers( AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/favicon.ico")).permitAll()
               
                        .requestMatchers(AntPathRequestMatcher.antMatcher(camundaWebAppUrlPattern)).hasRole("ADMIN")
                
                .requestMatchers(AntPathRequestMatcher.antMatcher("/app/**")).hasAnyRole("ADMIN", "USER")
                .requestMatchers(AntPathRequestMatcher.antMatcher( "/engine-rest/**")).hasAnyRole("ADMIN", "USER")
                
                .requestMatchers(AntPathRequestMatcher.antMatcher("/error")).authenticated()
                
                .anyRequest().denyAll();
            }
            )
            .logout(logout -> logout
                .logoutUrl("/app/logout")
                // Redirect URL from config
                .logoutSuccessUrl(configProps.getLogoutUrl())
            )
        ;
       
        http.apply(new RequestHeaderAuthenticationConfigurer());

        return http.build();
    }

My Controller :

@RestController
@RequestMapping("/app")
public class OrderController {
    @Autowired
    private ProductOrderService productOrderService;

    @Autowired
    private AuditService auditService;

    
    @PutMapping("/activities")
    @PreAuthorize("!hasRole('ROLE_VISITOR')")
    public CustomResponse activityUpdate(@RequestBody ActivitiesUpdateDto activitiesUpdateDto)
            throws JsonProcessingException {
        return productOrderService.activityUpdate(activitiesUpdateDto);
    }

    
    @GetMapping("/orders/products")
    public List<OrderDto> getOrders(@RequestParam("search") Optional<String> searchTerm) {
        return productOrderService.fetchOrders(searchTerm.isPresent() ? searchTerm.get() : null);
    }
    
    
    @PutMapping("/order")
    @PreAuthorize("!hasRole('ROLE_VISITOR')")
    public CustomResponse updateOrder(@RequestBody OrderUpdateDto orderUpdateDto) {

        return productOrderService.updateOrder(orderUpdateDto);

    }

    @PostMapping("/order/comment")
    public CustomerResponse addComment(@RequestBody CommentDto commenteDto) {

        return productOrderService.addComment(commenteDto);

    }

}

For all api calls, a request header with header name "Cookie" is being sent. Cookie: Idea-56ec4e87=6d30aeb7-9c46-4d74-be5b-18c896652a0c; CUSTOM-XSRF-TOKEN=527851d7-ac94-424a-b55f-bf7b6a1c1d6c; JSESSIONID=9A9C968680E61F50CD936B6C7199F631

Security config before the springboot upgrade:

SecurityFilterChain internalAPISecurityConfig(HttpSecurity http, AppConfigProperties configProps)
        throws Exception {

    String camundaWebAppUrlPattern = "/camunda/**";

    String[] staticRessourceUrlPatterns = new String[] { "/favicon.ico" };

    CookieCsrfTokenRepository csrfRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
    csrfRepository.setCookieName(CUSTOM_CSRF_COOKIE_NAME);

   
    http
        .csrf(csrf -> csrf
                .ignoringAntMatchers(camundaWebAppUrlPattern)
                .csrfTokenRepository(csrfRepository)
        )
        .authorizeHttpRequests(authorize -> authorize
           .antMatchers(HttpMethod.GET, staticRessourceUrlPatterns).permitAll()
            .antMatchers(camundaWebAppUrlPattern).hasRole("ADMIN")
            .antMatchers("/app/**", "/engine-rest/**").hasAnyRole("ADMIN", "USER")
            .antMatchers("/error").authenticated()
            .anyRequest().denyAll()
        )
        .logout(logout -> logout
            .logoutUrl("/app/logout")
            .logoutSuccessUrl(ConfigProps.getLogoutUrl())
        );
    http.apply(new RequestHeaderAuthenticationConfigurer());

    return http.build();
}

I would be thankful for any help to resolve this issue.

What have I tried so far

  1. I have disabled the csrf using csrf.disable(). Then all apis are accessible. However, it does not meet client's requirement. So, I need to keep the csrf enabled.

  2. Removed this annotation @PreAuthorize("!hasRole('ROLE_VISITOR')") for a POST method addComment(@RequestBody CommentDto commenteDto). Still this was failing with 403

  3. Changed the annotation @PreAuthorize("!hasRole('ROLE_VISITOR')") to @PreAuthorize("hasRole('USER') or hasRole('ADMIN')"). Still the same issue. Even tried by removing ROLE_ prefix. Same result.


Solution

  • Based on Migration Guide.

    In Spring Security 5, the default behavior is that the CsrfToken will be loaded on every request. This means that in a typical setup, the HttpSession must be read for every request even if it is unnecessary.

    In Spring Security 6, the default is that the lookup of the CsrfToken will be deferred until it is needed.

    As a fix you have to change a little bit internalAPISecurityConfig, exactly next one change:

    CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
    // set the name of the attribute the CsrfToken will be populated on
    requestHandler.setCsrfRequestAttributeName("_csrf");
    

    And then in configuration part also set csrfTokenRequestHandler

    http
        .csrf(csrf -> csrf
                .ignoringAntMatchers(camundaWebAppUrlPattern)
                .csrfTokenRepository(csrfRepository)
    
                .csrfTokenRequestHandler(requestHandler)
    
        )
    

    Also more discussion about this topic you can find csrf-protection-not-working-with-spring-security-6