spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.86k stars 5.92k forks source link

Reactive equivalents for security expression handling and url-based access control #5867

Open danielfernandez opened 6 years ago

danielfernandez commented 6 years ago

Summary

In the new thymeleaf-extras-springsecurity5 I'm trying to make all the functionality already existing for Servlet-based applications available also for WebFlux ones.

But I'm unable to find in the reactive side of Spring Security any equivalents for the functionality providen by SecurityExpressionHandler<FilterInvocation> or WebInvocationPrivilegeEvaluator in Servlet-based web applications. These allow Thymeleaf to evaluate Spring Security expressions such as hasRole(x), and also perform url-based access control (privilege evaluator).

Also, digging into Spring Security's code, I cannot find any infrastructure for quickly developing reactive equivalents to these. For example, not only there seems to be no reactive object to be used as an expression evaluation root, but actually there is no reactive invocation class --let's say a hypothetic WebFilterInvocation-- that would encapsulate the equivalent ServerWebExchange in order to support matching operations…

Am I looking in the wrong place (the spring-security-web module)? Was this left out on purpose? If not, maybe with some detail on what would be needed I might be able to help, at least with the not-so-low-level infrastructure...

Version

Spring Security 5.1.0.BUILD-SNAPSHOT

rwinch commented 6 years ago

@danielfernandez Thanks for reaching out. There is not SpEL expression support for request mappings within WebFlux applications. This largely has to do with the fact that we can now use method references and lambdas. For example:

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .authorizeExchange()
            .pathMatchers("/users").access(this::isRob)
            .anyExchange().authenticated()
            .and()
        .httpBasic().and()
        .formLogin()
            .loginPage("/login");
    return http.build();
}

private Mono<AuthorizationDecision> isRob(Mono<Authentication> authentication,
        AuthorizationContext authorizationContext) {
    return authentication
            .map(Authentication::getName)
            .map(username -> username.startsWith("rob@"))
            .map(AuthorizationDecision::new);
}

The reactive bits have changed the authorization model slightly as well. Now we use ReactiveAuthorizationManager which inverts the model a bit. Instead of having config attributes that must be discovered the ReactiveAuthorizationManager could perform the look up of the config attributes based on the object being authorized. This gives more flexibility for dynamic looks. The new API also allows returning a rich object or throwing an exception. This allows for custom error messages in situations like OAuth and missing scopes.

Eventually we should provide a way for APIs like Thymeleaf, but at the moment it does not exist (eventually we would just need to expose the ReactiveAuthorizationManager that is being used by the HTTP based security and Thymeleaf would invoke that.

danielfernandez commented 6 years ago

Thanks a lot for the quick answer @rwinch. So it seems for that in WebFlux apps Thymeleaf should disable the part of its sec:* processors that are meant to do authorization checks. For now only checking the Authentication object fields will be available from sec:* processors.

rwinch commented 6 years ago

That makes sense to not support authorization at this point.

I'm curious how you are able to interact with the authentication since it is available as a Mono<Authentication>?

danielfernandez commented 6 years ago

I'm curious how you are able to interact with the authentication since it is available as a Mono?

Short answer: with quite a lot of pain :)

Long answer:

The key restriction is that Thymeleaf cannot resolve any reactive variables on-the-fly during template execution (i.e. except for the data driver in data-driven mode, but that's not the case), so any possible solutions had to involve the resolution of Mono<SecurityContext> before template processing starts, this is, to include Mono<SecurityContext> into the Spring model just before AbstractView#render(...) made its magic of resolving all async variables. And yes, this means every rendered template will have to have its SecurityContext resolved for execution, and apparently from your comment in spring-projects/spring-security#4762 the same will happen for the CsrfToken.

The key was, controller advices weren't an option in this case because I wanted this to be transparent for the user, to work out-of-the-box just by adding the Spring Security dialect. At the same time I was very limited in how deep into Thymeleaf I could insert code for this because: 1. The thymeleaf core should remain Spring-dependency-free, 2. The thymeleaf-spring5 integration module should have no hard dependencies on WebFlux as it needs to also work with MVC, and 3. The thymeleaf-extras-springsecurity5 integration module should be as decoupled as possible from thymeleaf-spring5 –though a complete decoupling is impossible– because it should be possible to make it work with e.g. a future thymeleaf-spring6 if a Spring Security 5 + Spring 6 version combination ever becomes possible.

So in the end what I did was to build on the already existing mechanism of Thymeleaf dialects providing execution attributes (IExecutionAttributeDialect), a series of arbitrary objects that would be available to the engine throughout any template execution for which those dialects would be configured, and establish that in Spring-enabled applications all execution attributes which name started with a specific prefix would be added to the Spring model before async resolution and therefore available as resolved objects to templates.

Extending this support a bit, these execution attribute objects can not only be objects, but also Supplier<?> so that their creation can be deferred to template rendering time (and the resulting objects scoped to that template execution), and also Function<ServerWebExchange,?> so that the creation of those objects could depend on the ServerWebExchange, as is the case for Mono<SecurityContext> and Mono<Csrf>.

You can see the relevant code for this here: https://github.com/thymeleaf/thymeleaf-spring/blob/19276997cf349ed08c5b79cee227f36c5db751ab/thymeleaf-spring5/src/main/java/org/thymeleaf/spring5/view/reactive/ThymeleafReactiveView.java#L338-L365

So thanks to this mechanism, now the SpringSecurityDialect in thymeleaf-extras-springsecurity5 adds a new Function<ServerWebExchange,Mono<SecurityContext>> that gets executed in ReactiveThymeleafView#render(...), that Mono<SecurityContext> is set into the Spring model, Spring's infrastructure takes care of resolving it before returning control to Thymeleaf for actual view rendering, and from then on every processor that needs the SecurityContext (or its included Authentication) has it available and resolved at the model.

This execution attribute specified by SpringSecurityDialect looks like this:

final Function<ServerWebExchange, Mono<SecurityContext>> secCtxInitializer =
                        (exchange) -> ReactiveSecurityContextHolder.getContext();

So, again, limitation: that SecurityContext and Csrf will need to be always resolved, even if the template doesn't really need them. But right now determining otherwise in Thymeleaf could send complexity through the roof. Future versions of Thymeleaf, I hope, will be able to fully-resolve async variables on-the-fly... but that's not the case yet :(

Unfortunately all of this took me multiple attempts, a couple of failed implementations and several months of Thymeleaf work (very intermittent, of course — we even had a new baby in the meantime and I sort of got distracted 😄), but finally it seems to be all starting to come together and I should be able to release this soon. This all made me delay a lot the release of Thymeleaf 3.0.10 in fact, because I needed support for this scenario from the core. In fact, 3.0.10 is not yet released because I first want to be really sure that what was made does work for the entire Spring Security 5 + WebFlux scenario…

danielfernandez commented 6 years ago

That makes sense to not support authorization at this point.

By the way, I finally went for only allowing a minimal set of authorization expressions in sec:authorize attributes in WebFlux applications: isAuthenticated(), isFullyAuthenticated(), isAnonymous() and isRememberMe().

These expressions are too useful to lose them (esp. isAuthenticated()) and they are based on an AuthenticationTrustResolver, so that was easy to do using a mere AuthenticationTrustResolverImpl.

rwinch commented 6 years ago

Thanks for the response! I'm looking forward to the automatic support of CSRF and authentication!

RobMaskell commented 4 years ago

@rwinch I'm happy to have a go but if you have any more pointers that would helpful? I'll look at basic SpEL but I like the AuthorizationDecision being declarative, being able define those up front and use them from SpEL in a Thymeleaf template sounds interesting if it is even possible?

rwinch commented 4 years ago

@RobMaskell I think that is the problem. We don't know the expressions ahead of time and they would need to be evaluated eagerly.

I think from the security side, the first thing we would need to do is support a SpEL implementation of ReactiveAuthorizationManager. This could probably help on the method security side as well.

This would at least empower Thymeleaf to be able to enhance things on their side to support authorization. Perhaps @danielfernandez has some ideas as to how that would be done.