How to check security access before validation (#Valid) in Controller? - java

I am creating a Restful API with Spring Boot 2.5 and would like to know the right way to implement validation while checking roles for some routes. Also, for some routes I need to make sure that only admins can modify the resource or its owner.
#PreAuthorize seems to be the solution, but #Valid seems to be processed before an actual method call, otherwise known as executed before #PreAuthorize.
See : How to check security acess (#Secured or #PreAuthorize) before validation (#Valid) in my Controller?
Is this really the only available and clean solution to make a Restful API with both validation and roles with Spring Boot & Spring Security?

I'm afraid that that is the cleanest solution.
To check roles for some routes, you can configure your HttpSecurity to check roles before even getting to the controller, like so:
#Bean
SecurityFilterChain app(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests
.antMatchers("/route1").hasAnyRole("ADMIN", "USER")
)
return http.build();
}
So, with this configuration, you are making sure that only ROLE_USER or ROLE_ADMIN are allowed to request /route1.
But now, the ROLE_USER is allowed only if they are the resource owners. For this, you must have to resolve the method parameters to know which resource you are requesting. And then, in the #PreAuthorize, you can do something like this:
#PreAuthorize("#myBean.isResourceOwner(resourceId, authentication)")
#PutMapping("/{resourceId}")
public void update(#PathVariable Long resourceId) {
...
}

Related

spring security manual login best practice

I'm using spring security to implement a programmatic, manual user login. I have a scenario where I have positively established the user's identity, and wish to log them in. I don't know their password, and so can't use the regular login code path where you submit a form to a url, which spring intercepts via a servlet Filter, doing all of it's auth+session magic.
I've searched, and it seems most people create their own Authentication object, and then tell spring about via:
PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(user, "", user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
Indeed, this works. Spring even puts it into the session for me, making subsequent http requests maintain their auth status.
However, I feel like this is a dirty hack. I'll present some details that I hope will give concrete examples of the problems associated with using setAuthentication() inside a controller to achieve a manual login:
To give an idea, my config is:
httpSecurity
.authorizeRequests()
.antMatchers("/test/**").permitAll()
.antMatchers("/admin/**", "/api/admin/**").hasRole("USER_SUPER_ADMIN")
.and()
.formLogin()
.loginPage("/sign-in?sp")
.loginProcessingUrl("/api/auth/sign-in")
.successHandler(createLoginSuccessHandler())
.failureHandler(createLoginFailureHandler())
.permitAll()
.and()
.logout()
.logoutUrl("/api/auth/sign-out")
.logoutSuccessHandler(createLogoutSuccessHandler())
.and()
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry)
;
Key points in the above config:
I use custom success and failure handlers for the form login
I want to config behavior for max concurrent sessions per user
I want to maintain spring's default session fixation protection (changing session id upon login).
I want to use a session registry
... more of these session/login functionalities, had I chosen to config it.
I stepped through the code to see how spring processes a form login. As expected, Spring does all the session/login functionalities that my HttpSecurity config told it to do when I use the form login. But, when I do my own custom/manual login via SecurityContextHolder.getContext().setAuthentication(), it does NONE of those functionalities. This is because spring does all of it's session/login functionalities stuff inside of a servlet Filter, and my programmatic code can't really call a Filter. Now, I can attempt to add the missing functionalities myself, duplicating their code: I see that the spring Filter uses: ConcurrentSessionControlAuthenticationStrategy, ChangeSessionIdAuthenticationStrategy, and RegisterSessionAuthenticationStrategy. I can create these objects myself, configure them, and call them after my custom login. But, that's really lame to duplicate all that spring code. Furthermore, there's still other behaviors I'm missing - I noticed that when using the form login code path, that spring triggers some login events which don't get triggered when I do my custom login. And there's probably other stuff that I'm missing or don't understand. The whole process is pretty complicated, and I feel like there's a high chance of introducing bugs if this isn't done right, not to mention that library updates would be a pain if I started duplicating spring code.
So, I feel like I'm approaching this from the wrong way. Should I be using a different strategy, so that I'm not bypassing so much of the stuff that spring does for me? Maybe I should try to make my own AuthenticationProvider to accomplish this custom login?
*To clarify, my code more or less works. But, I feel like I accomplished it using a poor strategy because I had to write code duplicating a lot of stuff that spring does for me. Further, my code doesn't perfectly replicate what spring does, making me wonder what negative implications might result. There must be a better way to programatically achieve login.
I wanted to elaborate on how I implemented the advice of dur. In my scenario, I only used a custom AuthenticationProvider.
Instead of creating a custom servlet Filter, such as extending AbstractAuthenticationProcessingFilter, which seemed like a lot of work, I choose to instead use the following strategy:
At the point in my code where I was confident that I had identified the user, and wanted them to be "logged in", I stuck a flag in the user's session, marking that they should be logged in on the next request, along with any other identity/bookkeeping info I needed, such as their username.
Then, I told the browser client to make an http post to the loginProcessingUrl (the same one I configured spring security to use for form-based login), telling them to send the standard username and password form params, although they don't need to send real values - dummy values like foo are fine.
When the user makes that post request (eg to /login), spring will invoke my custom AuthenticationProvider, which will look in the user's session to check for the flag, and to gather the username. Then it will create and return an Authentication object, such as PreAuthenticatedAuthenticationToken, which identifies the user.
Spring will handle the rest. The user is now logged in.
By doing it this way, you stay within the "normal" way of doing logins, and so spring will still automatically:
Call any custom success and failure handlers you configured for the form login, which is nice if you use that place to do certain things on login, like query or update a db.
It will respect any max concurrent sessions per user settings that you may be using.
You get to keep spring's default session fixation attack protection (changing session id upon login).
If you set a custom session timeout, eg via server.session.timeout in a properties file, spring will use it. There's probably other session config attributes that are done at this time too.
If you enabled spring's "remember me" functionality, it will work.
It will fire a login event, which is used for other spring components, such as storing the user's session in a SessionRegistry. I think the events are also used by other parts of spring, such as the actuator, and for auditing.
When I first tried just doing the typically recommended SecurityContextHolder.getContext().setAuthentication(authentication) to login my user, instead of the custom AuthenticationProvider, none of the above bullets were done for me, which can utterly break your app... or cause subtle security bugs - neither are good.
Here's some code to help solidify what I said:
Custom AuthenticationProvider
#Component
public class AccountVerificationAuthenticationProvider implements AuthenticationProvider {
#Autowired
private AppAuthenticatedUserService appAuthenticatedUserService;
#Autowired
private AuthService authService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// This will look in the user's session to get their username, and to make sure the flag is set to allow login without password on this request.
UserAccount userAccount = authService.getUserAccountFromRecentAccountVerificationProcess();
if (userAccount == null) {
// Tell spring we can't process this AuthenticationProvider obj.
// Spring will continue, and try another AuthenticationProvider, if it can.
return null;
}
// A service to create a custom UserDetails object for this user.
UserDetails appAuthenticatedUser = appAuthenticatedUserService.create(userAccount.getEmail(), "", true);
PreAuthenticatedAuthenticationToken authenticationToken = new PreAuthenticatedAuthenticationToken(appAuthenticatedUser, "", appAuthenticatedUser.getAuthorities());
authenticationToken.setAuthenticated(true);
return authenticationToken;
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
Config spring security to use the provider
// In your WebSecurityConfigurerAdapter
#Configuration
#EnableWebSecurity
public class AppLoginConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AccountVerificationAuthenticationProvider accountVerificationAuthenticationProvider;
#Autowired
private ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider;
#Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
// Spring will try these auth providers in the order we register them.
// We do the accountVerificationAuthenticationProvider provider first, since it doesn't need to do any slow IO to check,
// so it's very fast. Only if this AuthenticationProvider rejects (which means this http request is not for programmatic login), will spring then try the next AuthenticationProvider in the list.
authenticationManagerBuilder
.authenticationProvider(accountVerificationAuthenticationProvider)
// I'm using ActiveDirectory / LDAP for when a user logs in via entering a user + password via the html form, but whatever you want to use here should work.
.authenticationProvider(activeDirectoryLdapAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
...
}
}
For custom web authentication you should implement combination of a custom authentication filter (for example AbstractAuthenticationProcessingFilter or just GenericFilterBean), a custom authentication provider (AuthenticationProvider) or/and custom authentication token (AbstractAuthenticationToken).
For example, see source of Spring Security Kerberos.
See also:
The AuthenticationManager, ProviderManager and AuthenticationProvider

Spring Security REST API roles based on URL parameters

I have a REST API written in Spring Boot with Spring Security and OAuth2. The resources are secured this way:
#Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/v1/security/**").hasRole("ADMIN");
}
I'd like to introduce a new part of the API where the permissions are fine grained, based on projects. Let's consider a simple endpoint that prints the project configuration.
GET /api/v1/project/{projectId}/config
How would I configure the resource server to only allow access for users who have the role ROLE_PROJECT_{projectId}_ADMIN without having to manually specify all projects?
Also if this mechanism has a specific name, please let me know in comments to I can change the question title.
You can use path values in authorization expressions.
According to Path Variables in Web Security Expressions you should write your custom authorization logic.
public class WebSecurity {
public boolean checkUserHasAccessToProjectId(Authentication authentication, int projectId) {
// here you can check if the user has the correct role
// or implement more complex and custom authorization logic if necessary
}
}
Then in your Java security configuration you can refer to this method and pass it the value of the relevant path fragment.
http.authorizeRequests()
.antMatchers("/api/v1/project/{projectId}/config")
.access("#webSecurity.checkUserHasAccessToProjectId(authentication,#projectId)")
...

Spring Security blocks anonymous requests even with #PreAuthorize("permitAll()")

I have a lot of API endpoints that need authenticated requests, and a few that are allowed for any request. I would like Spring Security to block anonymous requests by default, but to let me overwrite it:
aHttpSecurity.authorizeRequests().anyRequest().authenticated()
...
#PreAuthorize("permitAll()")
#RequestMapping("/foobar")
public ResponseEntity<FooBar> get() {
...
}
Unfortunately, this does not work: /foobar outputs 401. Any idea how to do?
#PreAuthorize is just an annotation which wraps method to check if user can execute annotated method. It works not just with controllers.
When you have http requests, firstly requests go through some spring security filters and when you write .anyRequest().authenticated() you just do not go to some wrapped controller endpoint.
So, If you have a few endpoints you can exlude it
aHttpSecurity.authorizeRequests()
.antMatchers(HttpMethod.GET, "/foobar/**").permitAll()
.antMatchers("/**").authenticated()
and delete #PreAuthorize("permitAll()")
Spring Security has two levels of Security.
Filter Level and Method Level.
Filter Level would work with URL and If a URL is not configured to be accessed it will be denied access with 401 or 403 accordingly. This is handled by FilterSecurityInterceptor
Method Level is often used as a second level of defense to Authorize who can access a method and what operations or objects he/she can manipulate. This is handled by MethodSecurityInterceptor

Using security annotation and secure by default

I am using Spring-security in a Spring-boot appplication. By default, all methods are restricted to authenticated users thanks to this configuration :
#Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
Now, I would like to mark some urls as public. I could do it with ant matchers but I would prefer to be able to directly mark the relevant methods with an annotation.
I saw that some #PreAuthorize annotation exist but I could have it work only if I remove those lines from the configuration :
.authorizeRequests()
.anyRequest().authenticated()
This forces me to annotate manually every method that I want to secure with this annotation :
#PreAuthorize("isAuthenticated()")
This is highly dangerous because every forgotten url will be publicly accessible by default. Is there a way to make every url accessible to authenticated users by default and open some urls with
#PreAuthorize("permitAll()")
Also, I saw in another post that the OP was answered :
But it is really a bad idea to use pre-post annotation on a controller, for what could be done directly in HttpSecurity configuration. It forced you to use proxyTargetClass = true.
What is wrong with that? (Also, I did not need to use proxyTargetClass = true)

SecurityContext authorities does not equal ServletRequest roles?

I was trying to replace some manual authority checking with annotations (#Secured and #PreAuthorize). While debugging why it doesn't work I was surprised to find that the second of these two assertions failed at the top of a #RequestMapping controller method.
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
assert(auth.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_READER"))); // passes
assert(request.isUserInRole("ROLE_READER")); // fails
I assume (as I can't get them to authorise anything) #Secured and hasRole() make use of the latter lookup?
Are the roles not supposed to be automatically populated from the SecurityContext authorities?
Was the filter that set the Authentication supposed to add the roles separately?
Edit:
Cut down the spring security config to spring boot's (1.3.0) default, plus the filter that sets the authentication.
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new JwtAuthenticationFilter(), FilterSecurityInterceptor.class);
}
}
I assume (as I can't get them to authorise anything) #Secured and hasRole() make use of the latter lookup?
My assumption was wrong. Both use the granted authorities, but in different ways.
#Secured requires the prefix, so #Secured("ROLE_READER") works.
hasRole does not use the prefix, so #PreAuthorize("hasRole('READER')") works.
Both require the granted authority to be prefixed, so #Secured("READER") will never work, even if there is an authority named READER.
The prefix can be configured with RoleVoter's rolePrefix property.
HttpServletRequest.isUserInRole uses a completely separate system, unrelated to Spring security. It is not populated by default, and does not need to be. I believe adding a SecurityContextHolderAwareRequestFilter to the chain will populate it

Categories