Describe the bug AuthorizeHttpRequests is allowing calls with unauthorized access when triggered in parallel (approx 20 parallel calls triggered). Out of them, it allows most of the calls and failing few calls with proper unauthorized access which is defined using success/failure exception handlers.

To Reproduce Below is the snippet we used along with the annotation @EnableGlobalMethodSecurity,

    http.authorizeHttpRequests()
    .anyRequest()
    .hasAnyAuthority("role1,role2")
    .and()
    ..

    ..<some more methods as per our use case>

    ..
    .and()
    .exceptionHandling()
    .accessDeniedHandler(tokenAccessDeniedHandler);

Additional info: We have defined an authenticationFilter wherein it will extract the user and role from the token passed while making an API call and returns a AuthenticationToken object which basically extends AbstractAuthenticationToken and getPrincipal() method is overriden to return a User object with username and roles associated with the token:

new User(tokenValidationResponse.getUser().getUsername(),
        "",
        true,
        true,
        true,
        true, AuthorityUtils.createAuthorityList(roles.toArray(new String[0])));

Workaround Upon removing hasAnyAuthority from the security chain, migrating to @EnableMethodSecurity and annotating @PreAuthorize("hasAnyAuthority('role1','role2')) annotation for all the controller methods, it is working.

Expected behavior Our use case is to have authorization at the global level. Is there any other way with which this can be achieved?

why it is allowing the unauthorized access with @EnableGlobalMethodSecurity ?

Comment From: marcusdacoregio

Hi @sumanth-bhat, this seems to be a complex scenario to reproduce. Only with the snippets you provided, I don't think we can achieve the same results. Are you able to provide a minimal, reproducible sample that consistently shows the behavior?

Comment From: sumanth-bhat

Hi @sumanth-bhat, this seems to be a complex scenario to reproduce. Only with the snippets you provided, I don't think we can achieve the same results. Are you able to provide a minimal, reproducible sample that consistently shows the behavior?

@marcusdacoregio , I have added the minimum code which I used to simulate the issue in this repo: https://github.com/sumanth-bhat/spring-security-test-auth. I tested the parallel API calls using Jmeter and have added the jmx file as well in the repo.

In this test code I am expecting the authorized role (role1) to be passed in as token content via header.

Please note that for the workaround you can uncomment line number 10 in com.parallel.validation.token.controller.WelcomeController and in com.parallel.validation.token.security.SecurityConfig comment line 22 and uncomment line 24

Comment From: marcusdacoregio

Hi @sumanth-bhat.

I see that the problem is where you are placing the chain.doFilter(request, response). You are adding it inside the AuthenticationSuccessHandler but that is not the right place to do it since the interface contract does not contain the FilterChain. You can only call it because you are setting the AuthenticationSuccessHandler inside the doFilter method.

Ideally, you should add the call after running super.doFilter(request, response, chain), like so:

    // ...

    public TokenAuthFilter(RequestMatcher requestMatcher) {
        super(requestMatcher);
        this.setAuthenticationSuccessHandler((request1, response1, authentication) -> {
            AuthenticationToken authToken = (AuthenticationToken) authentication;
            if(authToken!=null) {
                logger.info("Authentication successful, Username: " + authToken.getUser().getUsername());
            }
        });

        this.setAuthenticationFailureHandler((request1, response1, exception) -> {
            if(exception instanceof BadCredentialsException) {
                logger.error("Authentication failed, Exception: ", exception);
                response1.setStatus(HttpStatus.UNAUTHORIZED.value());
                response1.setContentType("application/json");
                response1.setCharacterEncoding("UTF-8");
                new ObjectMapper().writeValue(response1.getOutputStream(),"Auth Failed "+exception.getMessage());
            }else{
                logger.error("Authentication failed,Exception: ",exception);
                response1.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
                response1.setContentType("application/json");
                response1.setCharacterEncoding("UTF-8");
                new ObjectMapper().writeValue(response1.getOutputStream(),"Auth Failed "+exception.getMessage());
            }
        });
    }



    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        super.doFilter(request, response, chain);
        chain.doFilter(request, response);
    }

    // ...

That way, you respect the contract, and your tests pass.