Skip to content

Simplify Request Authorization Configuration #13057

Closed
@jzheaux

Description

@jzheaux

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? :)

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions