Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Learn › Security › Authorization

Method security

When does an authorization check on a method run, and when is it skipped?

Method security is a way to write "who may run this method" on a method instead of on a URL. @PreAuthorize evaluates its expression before the method runs, and @PostAuthorize evaluates its expression after the method returns a value. If the expression is false, the call is denied. But these annotations sometimes do nothing just because you added them.

Checked against real execution · All 36 combinations were run on Spring Security 7.1.1, and whether the check ran, whether the body ran, and the value or exception received were checked against the results

Concepts in this lab

Method security (@PreAuthorize, @PostAuthorize)
A Spring Security feature that puts authorization conditions on methods instead of on URLs. @PreAuthorize evaluates its expression before the method runs and, if the result is false, does not run the body. @PostAuthorize evaluates its expression after the body has run, looking at the value to be returned, and, if the result is false, does not return the value. The expression can use not only a role check such as hasRole('ADMIN') but also the arguments of the method (#name), the value to be returned (returnObject), and the logged-in user (authentication). It works only when the configuration class has @EnableMethodSecurity. Without it, the annotations are ignored without any error. The check is done by the proxy that wraps the bean, so it does not apply to a call made inside the same class.
Authentication and authorization
Authentication checks who a user is. Authorization decides whether that verified user has permission to use a specific resource or feature. Authentication happens first, and authorization is decided based on its result. In HTTP status codes, a failed authentication corresponds to 401 and a failed authorization corresponds to 403.
Proxy
This is a stand-in object that receives calls in place of the real object. When it receives a call, it does some additional work and then passes the call on to the real object. Spring wraps a bean that needs a feature such as AOP or @Transactional in such an object and registers it in the container, so what other beans receive by injection is this stand-in, not the real object.
Self-invocation
This means that an object directly calls another of its own methods from inside the same object. Proxy-based features apply only to calls that come in from outside through the proxy, so settings such as @Transactional, @Async, and @Cacheable do not apply to a method that is called internally through this. The usual fix is to move that method to another bean.
SecurityContext
The place where Spring Security keeps the authentication information of the user who sent the current request (who the user is and what permissions the user has). When authentication succeeds, the information is stored here. Later, controllers and services read it from here to look up the current user. By default, it is kept per thread.

Try these

Running with the default settings

  1. When the application starts: the configuration class has no @EnableMethodSecurity. An annotation is only a mark on a method, and nothing reads it to do the check. No proxy is created, and the controller receives the real object as it is. No error or warning appears.
  2. orderService.deleteAll(): the controller calls the method. The user logged in now is kim (role USER). There is no proxy, so the call goes straight to the method of the real object.
  3. deleteAll() body runs: it deletes all the orders and returns "Deleted all". The call did not go through an authorization check, so a method that the logged-in user must not run was run as it is.
  4. Caller (the controller): it receives "Deleted all". A call that should have been denied finished normally.

Result of this run

Code

@Configuration
// there is no @EnableMethodSecurity
class SecurityConfig { }

@Service
class OrderService {
    @PreAuthorize("hasRole('ADMIN')")                 // check before the method runs
    public String deleteAll() {
        orders.clear();
        return "Deleted all";
    }
}

@RestController
class OrderController {
    private final OrderService orderService;       // the bean that Spring injected
    OrderController(OrderService orderService) { this.orderService = orderService; }

    @GetMapping("/try")
    Object call() {                                // the user logged in now: kim (ROLE_USER)
        return orderService.deleteAll();
    }
}

Assumptions of this model

All 36 combinations were run and checked on the PC used to write this lab with Spring Security 7.1.1 (Spring Framework 7.0.9, JDK 21), using code of the same structure as on the screen. The combinations were checked without a web server, by calling the methods of the service bean directly from a Spring context, and the logged-in user was put directly into the SecurityContext. Whether the authorization check ran was counted with an AuthorizationEventPublisher bean, which is called each time a check is made, and whether the method body ran, the value or the kind of exception the caller received, and whether the bean was a proxy were checked together. That AuthorizationDeniedException, which is thrown on denial, is a kind of AccessDeniedException was also checked by execution. That this exception becomes a 403 response in a web request was checked separately by starting a Spring Boot 4.1.1 server on 127.0.0.1. A request as kim, who has USER, to the URL where the controller calls deleteAll() returned 403 and the body did not run, and a request as park, who has ADMIN, returned 200. To use the name of an argument such as #owner in an expression, the -parameters option is needed at compile time. When the code was compiled without this option and run, #owner was empty and even the call by kim, the owner, was denied (the documentation says that the build tools of Spring Boot turn this option on by default, but that part was not verified). When a method with a check is called with no login information at all, AuthenticationCredentialsNotFoundException came out, not AuthorizationDeniedException. The body code on the screen (orders.clear() and so on) is abbreviated to show the meaning, and in the actual run, the number of times the body ran was counted. Spring Boot did not turn this setting on by itself. On the same Spring Boot server, when it was run with only @EnableMethodSecurity removed, even the request from kim with USER got 200 and the body ran. That the check is skipped on a call inside the same class is a general limitation of the proxy approach, and it is covered in detail in the "Proxies and self-invocation" lab. @PreFilter, @PostFilter, @Secured, and calling a bean you made yourself from an expression were not covered.

Previous lab: Access control · Next lab: Three web attacks

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these