I’d like to apply the Principle of Least Privilege in my endpoints by requiring all of them to be authenticated, unless stated otherwise at the method-level (so I don’t need to duplicate pathes from my controllers in a SecurityFilterChain).
Ideally, I’d want all of them to behave as if they were annotated by @PreAuthorize("isAuthenticated()") by default, while still being able to override this by explicitly specifying a @PreAuthorize (or other annotations like @PermitAll). Of course, it can be achieved by annotating the @Controllers themselves like the following, but you can still forget to annotate one, which is the reason why I’d like a more global solution:
@RestController
@PreAuthorize("isAuthenticated()")
public class TestController {
@GetMapping("/test")
@PreAuthorize("permitAll()")
public void test() {}
}
I tried following the documentation that says:
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.
So I kept Spring Boot’s default SecurityFilterChain which does http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) like in the doc:
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin(withDefaults());
http.httpBasic(withDefaults());
return http.build();
}
Then I tried using @PreAuthorize("permitAll()") on some endpoints to override it, with no success: as described in my issue on the subject, SecurityFilterChain rules are evaluated before @PreAuthorize, so they can’t be overridden by it.
I tried using custom method security (with a custom SecurityFilterChain identical to Spring Boot's but with the catch-all line http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) commented out) as advised in the issue, like this:
@Configuration
@EnableMethodSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin(withDefaults());
http.httpBasic(withDefaults());
return http.build();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorize() {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(AuthenticatedAuthorizationManager.authenticated());
}
}
with no success, since AuthorizationManagerBeforeMethodInterceptor.preAuthorize only applies to @PreAuthorize-annotated methods:
public static AuthorizationManagerBeforeMethodInterceptor preAuthorize(
PreAuthorizeAuthorizationManager authorizationManager) {
AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(
AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class), authorizationManager);
interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder());
return interceptor;
}
(and indeed it does work if I annotate the @Controller with @PreAuthorize(""))
I tried creating my own AuthorizationManagerBeforeMethodInterceptor, but to no avail either for now, since I don't really understand how that internal machinery works.
With your additional explanation and code snippets (which were useful for clarity), I can share one possible way to achieve your goal.
WARNING: Before doing so, I will once again reiterate that multiple layers of defense are always better than implementing something like this, because we can be assured that when one layer fails (due to a variety of reasons including misconfiguration), another layer is in place for baseline protection.
To achieve a "fallback" with only method-security, we can target all methods in the application that have the
RequestMappingannotation, including as a meta-annotation (e.g.@GetMapping,@PostMapping, etc.). We can rely on the fact that the existingPreAuthorizeAuthorizationManagerabstains (returns null) when noPreAuthorizeannotation is found on the method/class.NOTE: The major downside of this approach is that endpoints available through (for example) servlet mappings are not protected, so it is still possible to have endpoints that are not protected. A robust set of functional tests for security will be required to ensure you can have confidence here.
The following is an example security configuration:
Another minor note, in testing with this setup there are other gotchas and caveats that pop up, for example the default Spring Boot
/errorendpoint provided byBasicErrorControllerand issues withfavicon.icorequests that return a 404. More configuration may be required to get things fully working with your setup.