Describe the bug The documentation mentions here that:

It’s important to remember that when you use annotation-based Method Security, then unannotated methods are not secured. To protect against this, declare a catch-all authorization rule in your HttpSecurity instance.

But when using such a "catch-all", @PreAuthorize becomes unsuseable.

To Reproduce

@RestController
public class TestController {
    @GetMapping("/test")
    @PreAuthorize("permitAll()")
    public void test() {}
}

Expected behavior A call to /test should return a 200 status code instead of a 401.

Sample

A link to a GitHub repository with a minimal, reproducible sample.

Reports that include a sample will take priority over reports that do not. At times, we may require a sample, so it is good to try and include a sample up front.

https://github.com/CidTori/PreAuthorizeSample

Comment From: CidTori

For the record, removing the "catch-all" (the .anyRequest().authenticated(), in a custom security configuration almost identical to the default one), and replacing it with a class-level @PreAuthorize("isAuthenticated()"), does work, but since it's a class-level annotation, not a global-level configuration, it's not a good "catch-all".

Comment From: sjohnr

@CidTori thanks for reaching out, but it feels like this is a question that would be better suited to Stack Overflow. We prefer to use GitHub issues only for bugs and enhancements. Feel free to update this issue with a link to the re-posted question (so that other people can find it).

Expected behavior A call to /test should return a 200 status code instead of a 401.

Having said that, if the application includes request-level authorization rules, those are evaluated first (in the AuthorizationFilter). Method security is a separate layer that is evaluated afterwards. This allows for defense in-depth, and is considered a best practice as indicated by the "catch-all" note in the documentation. Because of this, you cannot "relax" a rule defined at the request level using method-level annotations. You can, however, make rules more strict at the method-level, which is what is intended.

For the record, removing the "catch-all" (the .anyRequest().authenticated(), in a custom security configuration almost identical to the default one), and replacing it with a class-level @PreAuthorize("isAuthenticated()"), does work, but since it's a class-level annotation, not a global-level configuration, it's not a good "catch-all".

You are free to omit request-level authorization and rely on method-level annotations only. However, this is not recommended. One alternative would be to customize method security to apply a default rule at the method-level when no annotation is present.

With the above explanation, I'm going to close this as answered. If you have further questions, please open a question on Stack Overflow and I'll be happy to take a look.

Comment From: CidTori

I still feel that there is at least some ambiguities in the documentation, since I think I did exactly what the documentation suggested (declaring a catch-all, or more precisely keeping Spring Boot's already declared catch-all, to protect unnannotated methods, while keeping the expected protection from annotated methods), but it didn't work.

Comment From: CidTori

Here is my question on SO: https://stackoverflow.com/questions/77745018/how-do-i-make-all-of-my-requestmapping-as-preauthorizeisauthenticated-by

Comment From: sjohnr

@CidTori thanks for the link, I responded in comments. If we can come up with something specific to enhance the docs, I'm happy to re-open this issue (or we can open a new issue). If you want to wait and think about this while we work on your SO question, that's fine too.

Comment From: codeconsole

@sjohnr @CidTori it seems to me like this is seriously lacking and could be improved. In an ideal configuration everything should be locked down and then overridden with specificity. Isn't that how most security systems work?

The more specific rule wins. You should be able to lock down your entire app at the request level, then open it up with specificity where the most specific rule wins.

The current implementation encourages developers who wish to use method level securty to open up their entire app and potentially not lock down at the controller level by missing a single annotation. Missing a single method annotation should not be permitAll, it should be ROLE_SUPER.

I do not see point in method level security if you can not override a generalized non-specific global rule. The burden should be granting access, not prohibiting it.

For applications that have 100s of controllers, adding individual ant patterns for every controller is not a clean maintainable solution.

At the very least, one should be able to do

            http
                .authorizeHttpRequests((authorize) -> authorize
                    .requestMatchers("/assets/**", "/logout").permitAll()
                    .anyRequest().hasRole("ADMIN")
                )

and then define rules at a Controller level in the same location where request mappings are @GetMapping("/hello")

@Controller
public class HelloController {

    @GetMapping("/hello")
        @Preauthorize('permitAll()')
    public String handle(Model model) {
        model.addAttribute("message", "Hello World!");
        return "index";
    }
}

Comment From: codeconsole

@CidTori the Grails Spring Security Core plugin works exactly how you would like, but it is based on the deprecated FilterSecurityInterceptor's https://github.com/spring-projects/spring-security/blob/dd94b119cab4945b7f1ddae0c46faf94100df95a/web/src/main/java/org/springframework/security/web/access/intercept/FilterSecurityInterceptor.java#L47-L51

It configures FilterSecurityInterceptor's securityMetadataSource to AnnotationFilterInvocationDefinition which analyzes both request urls mappings and method level annotations at the same time so that you can have a method level annotation overrides a more secure url mapping.

In my opinion this is the best possible practice because it eliminates security leaks by allowing you to start secure and modify with specificity. Having to do a requestMatcher for every single controller is a bit ridiculous. Having to group a bunch of controllers in a sub path is not a clean and viable solution and is still problematic if you have controllers that have both protected and unprotected urls. Url conciseness and clarity is also important in a well designed app.