From spring boot 2.5.24 to last release 2.7.x, i face a strange behavior : i've got a postMethod in a controller annotated with @RolesAllowed, and the @RequestBody is annotated with @Valid (from JSR250) when i post a message with an unauthorized user and an invalid requestBody i receive a BAD_REQUEST error, but i would expect an UNAUTHORIZED error. Is it a normal behaviour ?

Here is how to reproduce : controller

@RestController
@RequestMapping("rest/api/security")
public class SecurityController {

    @RolesAllowed("ADMIN")
    @PostMapping(value = "/with-valid", produces = APPLICATION_JSON_VALUE)
    public @ResponseBody Data withValid(@RequestBody @Valid Data data) {
        return data;
    }

}

dto

public class Data {

    @NotBlank
    private String str;
}

Spring context

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class SecurityConfig  extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests().anyRequest().permitAll()
                .and()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .httpBasic();
    }


    @Bean
    public UserDetailsService users() {
        // The builder will ensure the passwords are encoded before saving in memory
        User.UserBuilder users = User.withDefaultPasswordEncoder();
        UserDetails user = users
                .username("U809583")
                .password("password")
                .roles("ADMIN")
                .build();
        UserDetails admin = users
                .username("U809584")
                .password("password")
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user, admin);
    }

}

test

@SpringBootTest(classes = {DemoApplication.class},
        webEnvironment = RANDOM_PORT)
public class SecurityE2EIT {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;


    @Test
    public void withSecurityAndUnauthorizedUserAndValidation() {
        Data data = new Data();
        HttpEntity<Data> httpEntity = new HttpEntity<>(data, createHeaders());
        String url = "http://localhost:" + port + "/rest/api/security/with-valid";
        ResponseEntity<String> response = this.restTemplate
                .withBasicAuth("U809584", "password")
                .exchange(url, HttpMethod.POST, httpEntity, String.class);
        assertThat(response.getStatusCode()).isEqualTo(FORBIDDEN);
    }

    private HttpHeaders createHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.add(ACCEPT, "application/json;application/problem+json");
        return headers;
    }

}

Michael

Comment From: marcusdacoregio

Hi @zouroto, thanks for the report.

I tried to simulate using the code that you provided but the test passes with 403 Unauthorized. Is there something that I'm missing?

Maybe if you can provide a minimal, reproducible example that I can clone and run it should be better.

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: zouroto

Hello sry for the late answer

Please, find an exemple on the github link demo repo

Comment From: marcusdacoregio

Thanks for the example @zouroto.

The validation is done in the filter chain before the request reaches the controller, when the payload from the request is binded to your Data class. The @RollesAllowed("ADMIN") validation will trigger before the method from the controller is invoked, this means that the authorization check will be performed after the filter chain completes and before the method is actually invoked. That said, it's expected that the payload validation will be performed before the authorization check from @RolesAllowed.

You could move your authorization check to the security DSL in order to make the authorization check during the filter chain:

http
                .authorizeRequests()
                .antMatchers("/rest/api/security/with-valid").hasRole("ADMIN")
                .anyRequest().authenticated()

I'm closing this as invalid but feel free to continue the discussion if you need further help.