Expected Behavior
I would like to get access to the configured AuthorizationManager to be able to use it for custom authentication in SPAs.
Current Behavior
The manager is instantiated in AuthorizeHttpRequestsConfigurer only and directly injected into the AuthorizationFilter without making it available (e.g. as bean).
Context
Filtering by HTTP requests is not enough for our SPAs and being able to reuse the security configuration for our own filtering would be beneficial. Actually, the same problem exists with the "old" AccessDecisionManager.
Comment From: evgeniycheban
I can take this.
Comment From: paulroemer
I am happy to help and create a PR but would need some guidance by you guys.
My first question is if my suggestion fits the architecture. I guess there is some reason why not to expose the AuthorizationManager/AccessDecisionManager.
Then I wonder if there are other ways to access the configured request authorization. Accessing the manger just felt like the most natural way to me.
Comment From: jzheaux
Hi, @paulroemer. Thanks for reaching out. I'd like to understand your situation better to see what the best way to help is.
Filtering by HTTP requests is not enough for our SPAs
Can you elaborate on what's making filters insufficient?
Comment From: paulroemer
Sure! Our framework supports internal re-routing. That means, developers can decide to reroute to some other page programatically on server side. This omits the whole filter chain approach, unfortunately.
But yes, that was my first idea, too. Intercepting the POST requests from the client, unpack the navigation command and filter accordingly. Would be a very nice implementation but would not cover all paths.
Comment From: jzheaux
Got it, thanks. One thing you can do is publish your own @Bean.
To do a quick comparison, consider the following filter chain definition:
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.antMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
.antMatchers("/resource/**").hasAuthorize("SCOPE_resource")
)
// ...
return http.build();
}
The alternative is to publish an AuthorizationManager bean:
@Bean
AuthorizationManager authz() {
return RequestMatcherDelegatingAuthorizationManager.builder()
.add(new AntPathRequestMatcher("/admin/**"), AuthorityAuthorizationManager.hasAuthority("ROLE_ADMIN"))
.add(new AntPathRequestMatcher("/resource/**"), AuthorityAuthorizationManager.hasAuthority("SCOPE_resource"))
.build();
}
@Bean
SecurityFilterChain web(HttpSecurity http, AuthorizationManager authz) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize.anyRequest().access(authz))
// ...
return http.build();
}
Then, in places where you want to programmatically execute the same authorization decision logic, you can invoke the AuthorizationManager yourself. With the exception of some static prefixes, the two blocks of code are quite similar.
Would an approach like this achieve what you are trying to do?
My first question is if my suggestion fits the architecture
The reason that the above is preferred over publishing a @Bean is that it better encapsulates the DSL. This is similar to Security's preference on avoiding getters. By not exposing an AuthorizationManager, it gives us the flexibility to change how DSL instructions like anyRequest.authenticated() are turned into Java objects without breaking passivity.
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: spring-projects-issues
Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.
Comment From: paulroemer
Hey guys, sorry for the late response but I was on vacation... Would be nice if you can reopen the issue!
@jzheaux, I gave your example a try but there seems to be a type mismatch:
The authz bean is of type RequestMatcherDelegatingAuthorizationManager that is a AuthorizationManager<HttpServletRequest>. On the other hand the access() method in your example expects an AuthorizationManager<RequestAuthorizationContext>. See https://github.com/spring-projects/spring-security/blob/main/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java#L284
Comment From: paulroemer
I worked around the issue by reimplementing the RequestMatcherDelegatingAuthorizationManager to implement AuthorizationManager<RequestAuthorizationContext> to keep on testing the approach.
I noticed that
.and().formLogin().loginPage(LOGIN_URL).permitAll()
and likely others do not work together with that approach. According to the Javadoc of HttpSecurity.authorizeHttpRequests():
Returns:
the HttpSecurity for further customizations
I would expect it to work. Any thoughts?
Comment From: jzheaux
Thanks for keeping me honest, @paulroemer. :) Yes, I think some adjustments to the snippet would help.
The following is what I would recommend:
@Bean
AuthorizationManager<RequestAuthorizationContext> authz() {
AuthorizationManager<HttpServletRequest> authz = RequestMatcherDelegatingAuthorizationManager.builder()
.add(new AntPathRequestMatcher("/admin/**"), AuthorityAuthorizationManager.hasAuthority("ROLE_ADMIN"))
.add(new AntPathRequestMatcher("/resource/**"), AuthorityAuthorizationManager.hasAuthority("SCOPE_resource"))
.build();
return (authentication, context) -> authz.check(authentication, context.getRequest());
}
@Bean
SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> authz) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize.anyRequest().access(authz))
.formLogin((form) -> form.loginPage(LOGIN_URL).permitAll())
// ...
;
return http.build();
}
The .and() form of authorizeHttpRequests is added only as of Security 5.6 M1. Is this what you are referring to when you say the method isn't compatible? If you want to use the lambda and .and() versions together, the following should work:
@Bean
SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> authz) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize.anyRequest().access(authz))
.formLogin().loginPage(LOGIN_URL).permitAll();
// ...
;
return http.build();
}
Does that clear things up?
Comment From: paulroemer
Thanks for the updates. That simplifies my code a lot. What's still not working is the configuration of the login page
http
.authorizeHttpRequests((authorize) -> authorize.anyRequest().access(authz))
.formLogin().loginPage(LOGIN_URL);
The problem is that even with the configuration above the AuthorizationFilter blocks the request to the page. When adding a permitAll() to the form login configuration I am getting the following exception:
java.lang.IllegalStateException: permitAll only works with HttpSecurity.authorizeRequests()
Comment From: jzheaux
@paulroemer, sorry I missed this comment.
This could be an issue, would you please file a ticket with the about details about permitAll?
I believe in the meantime you should be able to add the endpoint to authorize directly as a workaround.
Comment From: tbee
I have a nice twist on this issue: I've implemented a multi factor authentication filter (Spring agnostic) which makes setting up MFA really simple, just one addFilter line in the web configuration. But it needs to know when to do its thing, which basically is when the AuthorizationFilter requires login to access a URL. So I've used the examples above to make the AuthorizationManager available and use that in the MFAFilter.
@Bean
AuthorizationManager<RequestAuthorizationContext> authorizationManager() throws Exception {
AuthorizationManager<RequestAuthorizationContext> permitAll = (a, o) -> new AuthorizationDecision(true);
AuthorizationManager<HttpServletRequest> authorizationManager = RequestMatcherDelegatingAuthorizationManager.builder()
.add(new AntPathRequestMatcher("/favicon.ico"), permitAll)
.add(new AntPathRequestMatcher("/pub/**"), permitAll)
.add(new AntPathRequestMatcher(IMPERSONATE_URL), AuthorityAuthorizationManager.hasRole("ADMINISTRATOR"))
.add(new AntPathRequestMatcher("**"), AuthenticatedAuthorizationManager.authenticated())
.build();
return (authentication, context) -> authorizationManager.check(authentication, context.getRequest());
}
The problem is the last line: "for any other URL, the user needs to be authenticated". Because the MFAFilter needs to be after the AuthorizationFilter (otherwise you don't know which user it is and if he has MFA configured), that last line is always true. So if that line is moved back to the filterChain, like so:
public SecurityFilterChain filterChain(HttpSecurity httpSecurity, AuthorizationManager<RequestAuthorizationContext> authorizationManager, TenantService tenantService) throws Exception {
httpSecurity
.authorizeHttpRequests((authorize) -> authorize.anyRequest().access(authorizationManager))
.authorizeHttpRequests(authorize -> authorize.requestMatchers("**").authenticated())
...
You get a Can't configure mvcMatchers after anyRequest. But if I move that line above the anyRequest, it will not see the permitAlls. Is there any way to use the authorizationManager not using anyRequests()?
I've solved it by marking all permitAll requests, so the MFAFilter knows it can let those pass through:
AuthorizationManager<RequestAuthorizationContext> permitAll = (authenticationSupplier, requestAuthorizationContext) -> {
MFAFilter.passthough(requestAuthorizationContext.getRequest());
return new AuthorizationDecision(true);
};
And then you also don't need to put it in a separate method.
But it is even more easily solvable: if there is no principal present in the security context, then apparently the AuthorizationFilter let the request through, so no MFA is needed either. (Throws out all the complex code and marvels at the simplicity of this solution. 😊)