Description
Nearly every application needs to override Spring Security's default authorization rule that all requests require the they be authenticated.
Many applications have static resources that are permitted for example, and many applications want to permit Spring Boot's /error
endpoint. Beyond that, many applications use RBAC, ABAC or some other form of authorization that requires corresponding Spring Security configuration.
This is quite simple in Spring Security already. For example, if I have a default Spring Security application, I can achieve a configuration identical to the default while changing only the authorization rules by doing the following:
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/my", "/static", "/resources").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authetnicated()
)
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
Some of the above does not have to do with authorization, though. And it would be nice to not have to specify that as well. For example, many applications do not need to specify authentication mechanisms as they are inferrable by Spring Boot.
This leads to confusion and potential misconfiguration issues when a user doesn't realize that even though they specified Boot properties, they still need to specify the mechanism in the DSL. For example, if I use spring-boot-starter-oauth2-resource-server
and am using the Boot properties, I might easily assume that I can specify my authorization rules like so:
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/my", "/static", "/resources").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authetnicated()
)
But, I would be wrong. Because I am overriding authorization rules, I also need to re-specify my authentication mechanisms. Because I don't know this, my application may end up less secure (since I've specified resource server configuration in Boot that is now no longer in effect).
Instead of having to specify authorization alongside authentication, it would be nice to be able to specify it separately.
One way to do this would be with a bean, like so:
@Bean
Customizer<AuthorizeHttpRequestsConfigurer> authorization() {
return (authorize) -> authorize
.requestMatchers("/my", "/static", "/resources").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
}
In this case, HttpSecurityConfiguration
would pick up the Customizer<AuthorizeHttpRequestConfigurer>
bean and apply it before returning a HttpSecurity
prototype instance.
In this case, Boot publishing a SecurityFilterChain
would look something like:
@ConditionalOnMissingBean
@Bean
SecurityFilterChain springSecurityFilterChain(HttpSecurity http) {
http
// ... no need to specify authorizeHttpRequests since was applied when prototype bean was created
.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()))
// ... or whatever authentication mechanisms are relevant
return http.build();
}
The nice thing about this is that it is a pattern that could potentially be followed for other configurers, which may lead to being able to break up HttpSecurity
configuration into smaller consumable chunks. The downside is that it's a departure from how Spring Security is traditionally configured.
Or publishing an AuthorizationManager
bean maintains a more traditional relationship with HttpSecurity
instances:
@Bean
AuthorizationManager<RequestAuthorizationContext> authorization(RequestMatcherDelegatingAuthorizationManager.Builder builder) {
return builder
.requestMatchers("/my", "/static", "/resources").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated().build();
}
(RequestMatcherDelegatingAuthorizationManager
DSL methods do not exist yet, but please reference MessageMatcherDelegatingAuthorizationManager
as an example)
Another nice thing about using AuthorizationManager
is the rest of the API fits in nicely:
@Bean
AuthorizationManager<RequestAuthorizationContext> authorization() {
return AuthenticatedAuthorizationManager.authenticated();
}
The main downside of this route is that it duplicates the DSL already found in AuthorizeHttpRequestConfigurer
.
In that case, Boot publishing a SecurityFilterChain
would look something like:
@ConditionalOnMissingBean
@Bean
SecurityFilterChain springSecurityFilterChain(HttpSecurity http) {
http
.authorizeHttpRequests(Customizer.withDefaults())
// ... the default would be to pick up an AuthorizationManager bean of the correct type
.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()))
// ... or whatever authentication mechanisms are relevant
return http.build();
}
In both cases, because authorization configuration can be declared separately, an application can do so without having to know that they must re-specify authentication mechanisms.
At this point, I feel amenable to both routes, but I slightly prefer the first one because of the broader potential it has for the entire DSL (other Customizer beans, for example). While it's not terribly obvious to a coder that Customizer<AuthorizeHttpRequestsConfigurer>
is the right bean to publish, this could be improved with a marker inferface like RequestAuthorizationCustomizer
. Feedback is welcome.
I distantly recall that @rwinch had a concern, and, Rob, please forgive me, but could you add your concern (if you still have it) here to this ticket for the record so that I don't forget again? :)