antMatchers allow ADMIN all routes while other roles are restricted - spring-boot

Specific user groups should have access to different api-routes. Given the following HttpSecurity we, for example, allow CUSTOMER to access GET /invoices/*. Also, I want to allow ADMIN to access any route /**.
#Override
protected void configure(HttpSecurity http) throws Exception {
http = http
.cors().and()
.csrf().disable(); // REST only
// ### Authentication
// ...
// ### Authorization
// anonymous (and all other roles)
e = e
.antMatchers(HttpMethod.POST,
"/user/account/create",
"/user/account/confirm/*",
"/feedback")
.permitAll()
.antMatchers(HttpMethod.GET,
"/" + StorageController.STORAGE_RELATIVE_PATH, // public files
"/translations/*")
.permitAll();
// role CUSTOMER
e = e
.antMatchers(HttpMethod.GET,
"/invoices",
"/invoices/*")
.hasRole(Role.CUSTOMER.toString())
.antMatchers(HttpMethod.PUT,
"/profile",
"/contracts/billingAddress")
.hasRole(Role.CUSTOMER.toString())
.antMatchers(HttpMethod.POST,
"/contracts",
"/profile/logo")
.hasRole(Role.CUSTOMER.toString());
// only ADMIN
e = e.antMatchers("/**").hasRole(Role.ADMIN.toString());
}
While ADMIN is allowed to use all non-mentioned /admin/.../ routes, the role get's a 403 on, for example, /invoices/* - why? From what I understand the specified configuration depends on the order and hence requires /invoices/* to have CUSTOMER role and hence ADMIN is not enough - correct?
If I add the following (section CUSTOMER or ADMIN), it works, but it is so cumbersome to always list ADMIN role. I just want ADMINs to be able to access everything.
#Override
protected void configure(HttpSecurity http) throws Exception {
// ...
// role CUSTOMER
e = e
// ... as above ...
// CUSTOMER or ADMIN
e = e
.antMatchers(HttpMethod.GET,
"/invoices/*") // AGAIN
.hasAnyRole(Role.CUSTOMER.toString(),
Role.ADMIN.toString());
// only ADMIN
e = e.antMatchers("/**").hasRole(Role.ADMIN.toString());
}
Also, I cannot put the ADMIN rule above the others, because it would exclude the other roles from accessing anything. Is there a way to specify what I want to do in a more elegant/easier way?

As you indicated in your question, this behaviour occurs because each matcher is considered in the order it was declared.
A request to /invoices will reach the below matcher first and since it is a match, it will apply the associated rule.
.antMatchers("/invoices").hasRole("CUSTOMER")
If the user making the request only has the role "ADMIN" then they will be denied access, because an "ADMIN" is not a "CUSTOMER".
Option 1
To get the desired behaviour you can explicitly list out the roles that have access to the endpoint.
As you mentioned, this is verbose, however, the advantage is that it clearly indicates the allowed roles in one place.
.antMatchers("/invoices").hasAnyRole("CUSTOMER", "ADMIN")
Option 2
If an "ADMIN" should be able to do anything a "CUSTOMER" can do, then you can declare a RoleHierarchy that states that any "ADMIN" is also a "CUSTOMER".
#Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_CUSTOMER");
return hierarchy;
}
The > symbol can be thought of as meaning "includes".
Then, any endpoints protected by .hasRole("CUSTOMER") can also be accessed by users with the role "ADMIN", since having the role "ADMIN" implies that they also have the role "CUSTOMER".

Related

401 on .permitAll() request in Spring Security

I have specified .permitAll() on the endpoint "/api/v2/user/login/**" but it still gives 401 when I don't give any authentication details in postman.
In fact, it's showing abnormal behaviour, below are my observations.
Gives 200 for any correct user details (regardless of role).
If I make a request with correct user details, it gives 200. If just after that request I do another request with incorrect password, it still gives 200. But incorrect username isn't tolerated.
Once it gives 401, it will keep giving 401 for all requests until I enter correct credentials.
CSRF is disabled so that shouldn't be an issue. I have tried playing with the order of permitAll request but that hasn't worked yet. Checkout the last antMatchers.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().
authorizeRequests().
antMatchers(HttpMethod.POST, "/api/v2/user/", "/api/v2/user", "/api/v2/user/change-role/**").hasAuthority("ROOT").
antMatchers(HttpMethod.GET, "/api/v2/user/", "/api/v2/user").hasAuthority("ROOT").
antMatchers(HttpMethod.POST, "/api/v1/customers/", "/api/v1/customers").hasAnyAuthority("ADMIN", "ROOT").
antMatchers(HttpMethod.GET, "/api/v1/customers/", "/api/v1/customers").hasAnyAuthority("EMPLOYEE", "ADMIN", "ROOT").
antMatchers(HttpMethod.POST, "/api/v2/user/login/**").permitAll().
anyRequest().
authenticated().
and().
httpBasic();
}
And here's the relevant controller method.
#RequestMapping(value = "/user/login", method = RequestMethod.POST)
public ResponseEntity<Boolean> loginUser(#RequestParam String username, #RequestParam String password){
return myUsersService.loginUser(username, password);
}
Any ideas are appreciated. Thanks!
Put antMatchers with permitAll first in the chain and remove /** to match the actual path you want to permit without auth.

Spring Boot 2 + Oauth2: How to have separate logins for regular users and admins?

So, I am using Spring Boot and Security for a while now. So far I only had one "kind" of user which were simply given roles USER_ROLE or ADMIN_ROLE in order to secure my REST endpoints.
However, I am now at a point where I realize: I only have one login. That is the default /oauth/token endpoint which, eventually, loads a user from my database and adds the authorities accordingly:
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
username = username.trim();
AppUserEntity appUserEntity = this.appUserRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found."));
List<GrantedAuthority> authorities = new ArrayList<>();
Collection<AppUserRoleEntity> roles = appUserEntity.getRoles();
for (AppUserRoleEntity appUserRoleEntity : roles) {
RoleEntity roleEntity = appUserRoleEntity.getRole();
authorities.add(new SimpleGrantedAuthority(roleEntity.getRoleType().toString()));
}
return new AppUserDetails(
appUserEntity.getId(),
appUserEntity.getEmail(),
appUserEntity.getPassword(),
authorities,
appUserEntity.getActivated()
);
}
The problem with this is, that there is no distinction between users. I do not know here which login (on my website) the user was using. A login will always work, even if a normal user uses the admin-login mask.
What I seek is a way to have different registration and login endpoints for admin and regular users. How would I do that?
I have seen this tutorial and also this one but they do not use OAuth2.
What are my options here and/or what is the Spring Boot way to do this?
Authentication and Authorization are two things. With OAuth, you are doing only the authentication part. Meaning, it checks whether the user has a valid username and a password. It is the application's responsibility to allow or deny access to certain areas of the application based on the grant/role assigned to the authenticated user.
You authenticate users with oauth/token endpoint and pass the token to the application with every request. Then in application's security configurations, you restrict admin area to only users who are in ADMIN_ROLE.
Please check the following section of a sample spring security configuration.
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() //User must have a valid token
.antMatchers("/admin/**").hasRole("ADMIN"); //User must be of ADMIN_ROLE
}
So, any user having a valid username and a password cannot access (login is a bit vague word) admin area of your application.
In the above configuration, if a non-admin user try to access /admin/employee after authenticating through oauth/token, it will throw 403 error. In other terms, that user is not allowed to login to that area of the application.

How to hook into Spring Security authentication process?

Currently I have this trivial configuration:
// Kotlin code
override fun configure(http: HttpSecurity) {
http
.formLogin()
.loginPage("/entry")
.loginProcessingUrl("/auth")
.usernameParameter("usr")
.passwordParameter("pwd")
.defaultSuccessUrl("/", true)
.failureHandler { request, response, exception ->
// Can't figure out what to enter here (see below).
}
}
If authentication fails, I have two requirements:
Flash error message into the session (avoiding 'error' param in query string). It seems I can't inject RedirectAttributes into this lambda; is there a workaround?
I want to send back the login (but not the password) that user entered before submitting login form, in order to repopulate the field. How do I do that?
I was able to figure it out.
#Configuration
#EnableWebSecurity
class SecurityConfig: WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http
.formLogin()
.loginPage("/entry")
.loginProcessingUrl("/auth")
.usernameParameter("usr")
.passwordParameter("pwd")
.defaultSuccessUrl("/", true)
.failureHandler { request, response, _ ->
request.session.setAttribute("loginError", "Login Error!")
request.session.setAttribute("failedUsername", request.getParameter("usr"))
response.sendRedirect("/entry")
}
}
}
Then, you have to set up login controller to customize serving of login form:
#Controller
#RequestMapping("/entry")
internal class LoginController {
#GetMapping
fun getLoginForm(session: HttpSession, model: Model): String {
if (session.getAttribute("loginError") != null) {
model.addAttribute("loginError", "Login Error!")
session.removeAttribute("loginError")
model.addAttribute("failedUsername", session.getAttribute("failedUsername"))
session.removeAttribute("failedUsername")
}
return "login"
}
}
Then, you can use loginError and failedUsername model attributes in your templates:
<div th:if="${loginError}">Incorrect login/password</div>
<!-- ... -->
<input type="text" name="usr" th:value="${failedUsername}">
Basically we are emulating "flashing" messages into session. We carry these messages in the session and remove them as soon as they are read and passed on into the model. It’s possible that redirect will go wrong and messages will remain in the session, but they are harmless on their own, plus they will be removed the next time user visits /entry page.
As a result, now there is no ?error in page URL, and the user is not required to retype username.

Spring Security Custom Authentication Filter and Authorization

I've implemented a custom authentication filter, and it works great. I use an external identity provider and redirect to my originally requested URL after setting my session and adding my authentication object to my security context.
Security Config
#EnableWebSecurity(debug = true)
#Configuration
class SecurityConfig extends WebSecurityConfigurerAdapter {
// this is needed to pass the authentication manager into our custom security filter
#Bean
#Override
AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean()
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
//.antMatchers("/admin/test").hasRole("METADATA_CURATORZ")
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new CustomSecurityFilter(authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)
}
}
Filter logic
For now, my custom filter (once identity is confirmed) simply hard codes a role:
SimpleGrantedAuthority myrole = new SimpleGrantedAuthority("METADATA_CURATORZ")
return new PreAuthenticatedAuthenticationToken(securityUser, null, [myrole])
That authentication object (returned above) is then added to my SecurityContext before redirecting to the desired endpoint:
SecurityContextHolder.getContext().setAuthentication(authentication)
Controller Endpoint
#RequestMapping(path = '/admin/test', method = GET, produces = 'text/plain')
String test(HttpServletRequest request) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication()
String roles = auth.getAuthorities()
return "roles: ${roles}"
}
This endpoint then yields a response in the browser of:
"roles: [METADATA_CURATORZ]"
Great. So my authentication and applying a role to my user is working great.
Now, if I uncomment this line from the security config:
//.antMatchers("/admin/test").hasRole("METADATA_CURATORZ")
I can no longer access that resource and get a 403 -- even though we've already proven the role was set.
This seems totally nonsensical and broken to me, but I'm no Spring Security expert.
I'm probably missing something very simple. Any ideas?
Some questions I have:
Does my custom filter need to be placed before a specific built-in filter to ensure the authorization step occurs after that filter is executed?
When in the request cycle is the antMatcher/hasRole check taking place?
Do I need to change the order of what I am calling in my security configure chain, and how should I understand the config as I've currently written it? It's obviously not doing what I think it should be.
Does my custom filter need to be placed before a specific built-in filter to ensure the authorization step occurs after that filter is executed?
Your filter MUST come before FilterSecurityInterceptor, because that is where authorization and authentication take place. This filter is one of the last to be invoked.
Now as to where the best place for your filter might be, that really depends. For example, you really want your filter to come before AnonymousAuthenticationFilter because if not, unauthenticated users will always be "authenticated" with an AnonymousAuthenticationToken by the time your filter is invoked.
You can check out the default order of filters in FilterComparator. The AbstractPreAuthenticatedProcessingFilter pretty much corresponds to what it is you're doing - and its placement in the order of filters gives you an idea of where you could put yours. In any case, there should be no issue with your filter's order.
When in the request cycle is the antMatcher/hasRole check taking place?
All of this happens in FilterSecurityInterceptor, and more precisely, in its parent AbstractSecurityInterceptor:
protected InterceptorStatusToken beforeInvocation(Object object) {
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
...
}
...
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
...
throw accessDeniedException;
}
Extra information:
In essence, the FilterSecurityInterceptor has a ExpressionBasedFilterInvocationSecurityMetadataSource that contains a Map<RequestMatcher, Collection<ConfigAttribute>>. At runtime, your request is checked against the Map to see if any RequestMatcher key is a match. If it is, a Collection<ConfigAttribute> is passed to the AccessDecisionManager, which ultimately either grants or denies access. The default AccessDecisionManager is AffirmativeBased and contains objects (usually a WebExpressionVoter) that process the collection of ConfigAttribute and via reflection invokes the SpelExpression that corresponds to your "hasRole('METADATA_CURATORZ')" against a SecurityExpressionRoot object that was initialized with your Authentication.
Do I need to change the order of what I am calling in my security configure chain, and how should I understand the config as I've currently written it? It's obviously not doing what I think it should be.
No, there shouldn't be any issue with your filters. Just as a side note, in addition to what you have in your configure(HttpSecurity http) methods, the WebSecurityConfigurerAdapter you extend from has some defaults:
http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoginPageConfigurer<>()).and()
.logout();
You can take a look at HttpSecurity if you want to see exactly what these do and what filters they add.
THE PROBLEM
When you do the following:
.authorizeRequests()
.antMatchers("/admin/test").hasRole("METADATA_CURATORZ")
... the role that is searched for is "ROLE_METADATA_CURATORZ". Why?
ExpressionUrlAuthorizationConfigurer's static hasRole(String role) method ends up processing "METADATA_CURATORZ":
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException(
"role should not start with 'ROLE_' since it is automatically inserted. Got '"
+ role + "'");
}
return "hasRole('ROLE_" + role + "')";
}
So your authorization expression becomes "hasRole('ROLE_METADATA_CURATORZ'" and this ends up calling the method hasRole('ROLE_METADATA_CURATORZ') on SecurityExpressionRoot, which in turn searches for the role ROLE_METADATA_CURATORZ in the Authentication's authorities.
THE SOLUTION
Change
SimpleGrantedAuthority myrole = new SimpleGrantedAuthority("METADATA_CURATORZ");
to:
SimpleGrantedAuthority myrole = new SimpleGrantedAuthority("ROLE_METADATA_CURATORZ");

spring 4.1 javaConfg setting to get requestCache working

similar to this:
Spring 3.1: Redirect after login not working
when an authenticated user becomes inauthenticated while deep-linking into a single page web app.
Spring security redirects to logon but:
SavedRequest savedRequest = new HttpSessionRequestCache().getRequest(request, response);
is null
thus i cannot devine the url or params to send re-authenticated user to requested page
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers() //redacted .
.antMatchers("/**").permitAll()
.and()
.formLogin()
.loginPage("/x/y/logon")
.usernameParameter("userLogon") //redacted
.loginProcessingUrl("/x/y/logon") //redacted
.defaultSuccessUrl("/x/", true)
.failureUrl("/x/y/logon?error=true")
.and()
.logout()
.logoutUrl("/x/y/logout")
.logoutSuccessUrl("/x/")
.permitAll();
}
}
-- controller --
#RequestMapping(method=RequestMethod.GET, value="/y/logon")
public ModelAndView logonHandler(HttpServletRequest request, HttpServletResponse response) {
List<Client> clients = //manager call for list of clients to show at logon
SavedRequest savedRequest = new HttpSessionRequestCache().getRequest(request, response);
if (savedRequest != null) {
String reqUrl = savedRequest.getRedirectUrl();
String[] urlParams = reqUrl.split("&");
String prefix = "";
String urlParam = "";
String cid = "";
try {
urlParam = urlParams[1];
} catch(IndexOutOfBoundsException ioob) { }
if (reqUrl.contains("cid=")) { cid = reqUrl.substring(reqUrl.indexOf("cid=")+4, reqUrl.indexOf("&")); }
if (reqUrl.contains("?")) { reqUrl = reqUrl.substring(0, reqUrl.indexOf("?")); }
prefix = reqUrl.substring(reqUrl.indexOf("/x/")+6, reqUrl.indexOf("/x/")+8);
reqUrl = reqUrl.substring(reqUrl.indexOf(prefix)+2);
if (reqUrl.contains("/")) {
reqUrl = reqUrl.substring(0, reqUrl.indexOf("/"));
}
request.setAttribute("requestUrl", prefix+reqUrl);
request.setAttribute("urlParam", urlParam);
request.setAttribute("cid", cid);
}
request.setAttribute("IPAddress", request.getRemoteAddr());
return new ModelAndView("x/logon", "clients", clients);
}
problem is, SavedRequest is null
is this an issue with:
alwaysUseDefaultTargetUrl property?
if yes, how in javaConfig does one set this property?
----- on edit to address comments ------
i'll explain my understanding of ea. .formLogon() settings:
logonPage() will be read by spring and control redirect to logon page when you are not authorized (cookie expire/db record del, etc). There are many ways that a session can not be authorized and spring needs to know what page to send unauth requests to. My manual logon handler only handles requests to the logon url.
usernameParameter() is to change from the default form input name thus obfuscating that one is using spring security.
loginProcessingUrl() this seems to conflict with the custom logonHandler, but i think its req to handle the post and allow for spring to create a secure sesson.
defaultSucessUrl() tells spring where to go after successful logon (the post request).
failureUrl() defines the url for failed logon.
nowhere in my custom logon handler for the get request, are those settings in conflict... i think... but i've read the docs and the Spring Security 3 book and lots of online resources and i still do not feel as though i have a solid understanding of spring security... so i may be way off

Resources