Blogs

Spring Security: Granular security with custom security expressions

Category
Software development
Spring Security: Granular security with custom security expressions

Spring Security is an application security framework from the Spring ecosystem. It supports a number of different authentication and authorization schemes compatible with the latest industry standards regarding this sensitive topic. It is built on a robust architecture that also allows easy customizations and extensibility of the framework’s core functionality.

In this blog post, we will explore several approaches in which Spring Security can be customized, through the means of custom Spring EL security expressions and specialized beans, in order to achieve a more complex and granular security implementation model, in the cases when the simple role-based access control model (RBAC) does not suffice.

Basic Spring Security expressions and how to use them

Spring Security implements some basic authorization expressions by default. If you’ve ever used it, some of these should already be familiar.

  • permitAll, denyAll
  • hasRole, hasAnyRole
  • hasAuthority, hasAnyAuthority
  • isAnonymous, isAuthenticated, isRememberMe, isFullyAuthenticated
  • hasPermission

You can find more information on these default expressions in the official docs: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#el-access.

Now, given a simple spring web security configuration, these could be utilized in the following way.

package com.agency04.blog.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class BasicExpressionsWebSecurityDemoConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("{noop}admin").roles("ADMIN")
                .and()
                .withUser("basic").password("{noop}basic").roles("BASIC");
    }

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .mvcMatchers("/admin-resource").hasRole("ADMIN")
                .mvcMatchers("/basic-resource").hasAnyRole("BASIC", "ADMIN")
                .mvcMatchers("/authenticated-resource").authenticated()
                .mvcMatchers("/**").permitAll()
                .and()
                .httpBasic();
    }
}

Here, in the first configure method is a simple authentication configuration that is just an in-memory store of users. For demonstration purposes, there are 2 users:

  • admin user with ADMIN role and no password encryption rules for simplicity
  • basic user with BASIC role and no password encryption rules for simplicity

Furthermore, in the second configure method httpBasic() is being used, for no special reason rather than its simplicity and convenience for demo purposes, which means that we opted in for HTTP basic authentication and Spring Security will configure everything to support this auth scheme (more on HTTP basic auth https://datatracker.ietf.org/doc/html/rfc7617 ). If we tried to access any of the protected controller endpoints without the authorization header containing the credentials, we would get 401 unauthorized responses.

Now for the main topic of this article, the second configure method also demonstrates the usage of the aforementioned default Spring Security authorization expressions. For example, /admin-resource URI path can be accessed only by a user containing role ADMIN, while /basic-resource may be accessed by either admin or basic user. Moreover, /authenticated-resource assumes that the user must be authenticated, regardless of the role, to access this endpoint.

The code for our simple secured demo controller is provided below.

package com.agency04.blog.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SecuredExampleController {

    @GetMapping("/admin-resource")
    public String getAdminResource() {
        return "Only admin can see this";
    }

    @GetMapping("/basic-resource")
    public String getBasicResource() {
        return "Only basic user can see this";
    }

    @GetMapping("/authenticated-resource")
    public String getAuthenticatedResource() {
        return "Everyone who is authenticated can see this";
    }

    @GetMapping("/public-resource")
    public String getPublicResource() {
        return "This resource is public";
    }
}

Granular authorization rules with global method security

Besides declaring all the route guards in one central place, for more control and better granularity one could use something spring calls method security, which allows access control on a Java method level by using PreAuthorize, PostAuthorize, and Secured annotations and providing them with the security expression.

Below is an example of the same secured demo controller, only this time not relying on central configuration for the authorization definitions, but rather using method security with just mentioned annotations to apply the security expressions.

@RestController
public class SecuredExampleController {

    @GetMapping("/admin-resource")
    @PreAuthorize("hasRole('ADMIN')")
    public String getAdminResource() {
        return "Only admin can see this";
    }

    @GetMapping("/basic-resource")
    @PreAuthorize("hasAnyRole('BASIC', 'ADMIN')")
    public String getBasicResource() {
        return "Only basic user can see this";
    }

    @GetMapping("/authenticated-resource")
    @PreAuthorize("isAuthenticated()")
    public String getAuthenticatedResource() {
        return "Everyone who is authenticated can see this";
    }

    @GetMapping("/public-resource")
    public String getPublicResource() {
        return "This resource is public";
    }
}

Additionally, in order for this to work, security configuration needs to be annotated with @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) so that the Spring is aware PreAuthorize, PostAuthorize, and Secured annotations will be used and that it can set up the proxies behind the scenes which will handle this boilerplate method access control logic.

A few more words on the Spring Security concepts

What all authorization schemes in Spring Security have in common is that they rely on a central Spring Security Context object which holds the security-specific information about the user that is authenticated by the system. Such custom logged user representation can be provided by implementing a class that either extends org.springframework.security.core.userdetails.User or implements the org.springframework.security.core.userdetails.UserDetails interface and making the common UserDetailsService, which is used as an entry point for fetching the security-specific user data at the time of authentication, return this object. This is important because the security expressions we are writing about actually rely on this central Spring Security context and so will the custom expressions when we implement them.

Implementing custom method security expressions

To be able to write custom security expressions, the default Spring Security configuration regarding method security needs to be tweaked a little bit. First, write a custom expressions class that is going to implement the custom expression. Expressions are implemented as public class methods.

package com.agency04.blog.expression;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;

import java.util.Optional;

public class CustomSecurityExpression {
    public boolean isUsernameEqualToBasic() {
        final String expectedUsername = "basic";
        final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (isAuthenticated(auth)) {
            final User authUser = (User) auth.getPrincipal();
            return expectedUsername.equals(authUser.getUsername());
        }
        return false;
    }

    private static boolean isAuthenticated(final Authentication authentication) {
        return authentication != null
                && authentication.isAuthenticated()
                && !(authentication instanceof AnonymousAuthenticationToken);
    }
}

Here, a custom expression is implemented which checks if a user’s username is “basic” and later this can be used to limit the access to backend methods where the premise is that only the user with this username may access the resource.

For now, this class has nothing to do with the existing Spring Security infrastructure, so let’s start connecting the dots. Next, an extension of a SecurityExpressionRoot class needs to be implemented which holds the definitions for default Spring Security expressions. This class also needs to implement MethodSecurityExpressionsOperations class, so that it is compatible with the next step and that is creating an extension of the DefaultMethodSecurityExpressionHandler which relies on the SecurityExpressionRoot to expose those default security expressions on a method level for use.

public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
    private Object filterObject;
    private Object returnObject;

    private final CustomSecurityExpression expression = new CustomSecurityExpression();

    public CustomMethodSecurityExpressionRoot(final Authentication authentication) {
        super(authentication);
    }

    public boolean isUsernameEqualToBasic() {
        return expression.isUsernameEqualToBasic();
    }

    @Override
    public Object getFilterObject() {
        return this.filterObject;
    }

    @Override
    public Object getReturnObject() {
        return this.returnObject;
    }

    @Override
    public Object getThis() {
        return this;
    }

    @Override
    public void setFilterObject(final Object obj) {
        this.filterObject = obj;
    }

    @Override
    public void setReturnObject(final Object obj) {
        this.returnObject = obj;
    }
}

Here, the method isUserNameEqualToBasic() is defined as a new expression within the root of the existing expression. For its implementation, it just calls CustomSecurityExpression where we have the actual implementation for the new expressions for better code reuse later. You may also notice the additional filterObject and returnObject properties and the corresponding getters and setters enforced by the MethodSecurityExpressionOperations interface. This code is actually just copied from the default MethodSecurityExpressionRoot class which is Spring Security’s internal implementation that is used by DefaultMethodSecurityExpressionHandler by default. The reason the code was copied and not simply extended was that this class has a package-private modifier and it is simply inaccessible to us, so the goal is to add the custom expressions and still retain the default behavior of the existing Spring Security infrastructure.

Moreover, here is the extension code for the DefaultMethodSecurityExpressionHandler.

package com.agency04.blog.expressionhandler;

import com.agency04.blog.expression.CustomMethodSecurityExpressionRoot;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations;
import org.springframework.security.core.Authentication;

public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {
    @Override
    protected MethodSecurityExpressionOperations createSecurityExpressionRoot(
        final Authentication authentication,
        final MethodInvocation invocation
    ) {
        final CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot(authentication);
        root.setPermissionEvaluator(getPermissionEvaluator());
        root.setTrustResolver(getTrustResolver());
        root.setRoleHierarchy(getRoleHierarchy());
        return root;
    }
}

This extension is used to override the creation of the security expression root to be used when evaluating the security expressions. It uses the CustomMethodSecurityExpressionRoot class where the new expression is defined.

To wrap this method security configuration up, there is one more step and that is providing an additional Spring configuration class which extends GlobalMethodSecurityConfiguration to be able to register the new expression handler with the existing Spring Security infrastructure.

package com.agency04.blog.config;

import com.agency04.blog.expressionhandler.CustomMethodSecurityExpressionHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.DenyAllPermissionEvaluator;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class CustomSecurityExpressionsAuthorizationConfig extends GlobalMethodSecurityConfiguration {
    private final ApplicationContext applicationContext;

    public CustomSecurityExpressionsAuthorizationConfig(final ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        final PermissionEvaluator permissionEvaluator = new DenyAllPermissionEvaluator();
        final CustomMethodSecurityExpressionHandler expressionHandler = new CustomMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(permissionEvaluator);
        expressionHandler.setApplicationContext(applicationContext);
        return expressionHandler;
    }
}

Here, @EnableWebSecurity annotation is used so that there are no conflicting bean definitions with default Spring Security autoconfiguration. We also instantiated permissionEvaluator as DenyAllPermissionEvaluator which is also the default implementation used in standard Spring Security configuration.

Now it is time to include and test one additional method in the endpoints controller which will be using the new security expression.

@GetMapping("/basic-username-allowed-resource")
@PreAuthorize("isUsernameEqualToBasic()")
public String getBasicUsernameResource() {
    return "Only user that is authenticated and has username equal to 'basic' can see this";
}

If you have tried it out, it should be working now and allowing access for the basic user and not for the admin user because it strictly expects the username to match the value “basic”.

Using custom security expressions in the server side rendered views

If you have ever worked with a server-side rendering engine, thymeleaf, which is very popular in the Spring ecosystem, you may have also stumbled upon its security dialect which allows you to use the default Spring Security expressions in thymeleaf HTML code. One of such usages may look something like this. <div sec:authorize="hasRole('ADMIN')"> This content will be visible only if user has role admin. </div>

What if we want to use the custom method security expressions like this, in order to have a common library of expressions that can be used on the frontend as well as in the backend? If using a server-side rendering technology, it would be really nice to have.

To achieve this, the Spring Security configuration needs to be expanded a bit more further, but nothing too much. In a similar manner to how method security expressions are defined, the same can be achieved for web security expressions. These are the terminology differences that Spring uses.

The same way the CustomMethodSecurityExpressionRoot is defined, a CustomWebSecurityExpressionRoot needs to be defined which extends Spring’s WebSecurityExpressionRoot that also relies on the base SecurityExpressionRoot that was seen earlier.

package com.agency04.blog.expression;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.WebSecurityExpressionRoot;

public class CustomWebSecurityExpressionRoot extends WebSecurityExpressionRoot {
    private final CustomSecurityExpression expression = new CustomSecurityExpression();

    public CustomWebSecurityExpressionRoot(final Authentication a, final FilterInvocation fi) {
        super(a, fi);
    }

    public boolean isUsernameEqualToBasic() {
        return expression.isUsernameEqualToBasic();
    }
}

Next, an extension of DefaultWebSecurityExpressionHandler needs to be defined, similarly as was done earlier for the DefaultMethodSecurityExpressionHandler.

package com.agency04.blog.expressionhandler;

import com.agency04.blog.expression.CustomWebSecurityExpressionRoot;
import org.springframework.security.access.expression.SecurityExpressionOperations;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.access.expression.WebSecurityExpressionRoot;

public class CustomWebSecurityExpressionHandler extends DefaultWebSecurityExpressionHandler {
    private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

    @Override
    protected SecurityExpressionOperations createSecurityExpressionRoot(
            final Authentication authentication,
            final FilterInvocation fi) {
        WebSecurityExpressionRoot webSecurityExpressionRoot = new CustomWebSecurityExpressionRoot(authentication, fi);
        webSecurityExpressionRoot.setPermissionEvaluator(getPermissionEvaluator());
        webSecurityExpressionRoot.setRoleHierarchy(getRoleHierarchy());
        webSecurityExpressionRoot.setTrustResolver(this.trustResolver);
        return webSecurityExpressionRoot;
    }
}

Finally, this custom expression handler needs to be declared as a Spring bean and included in the existing WebSecurityConfigurerAdapter configuration by using expressionHandler property in the HttpSecurity builder.

package com.agency04.blog.config;

import com.agency04.blog.expressionhandler.CustomWebSecurityExpressionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class BasicExpressionsWebSecurityDemoConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("{noop}admin").roles("ADMIN")
                .and()
                .withUser("basic").password("{noop}basic").roles("BASIC");
    }

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .mvcMatchers("/**").permitAll() // authorization is achieved on a controller method level
                .expressionHandler(customWebSecurityExpressionHandler())
                .and()
                .formLogin();
    }

    @Bean
    public CustomWebSecurityExpressionHandler customWebSecurityExpressionHandler() {
        return new CustomWebSecurityExpressionHandler();
    }
}

Notice that now formLogin() is being used as the authentication scheme instead of httpBasic() since HTTP basic is more convenient for web services and APIs, and form login is most typically used when working with server-side rendered client views.

Achieving the similar functionality using custom spring beans

We had a look into how to create a custom Spring Security extension to allow for new expression implementation which could then be used for granular backend method security and on the server-side rendered web views.

In this part, we would like to provide an example of how similar functionality could be provided by using plain Spring beans and in most cases, it is much simpler and maybe just enough for most of the use cases. The configuration extensions seen above are very useful when there are complicated Spring Security Context principal objects that may need some additional rules to perform authorization, but the advantage of the Spring beans way is that it avoids relatively complicated configuration and that security expressions behave and work on the same principles as all other Spring beans in the application.

The idea relies on a simple principle that expressions that are fed into the @PreAuthorize annotation, for example, could also call Spring beans directly by using @beanName.methodName() syntax. Here’s a simple example to achieve the same thing as in the above custom security expression example.

package com.agency04.blog.component;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class CustomSecurityExpression {
    public boolean isUsernameEqualToBasic() {
        final String expectedUsername = "basic";
        final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (isAuthenticated(auth)) {
            final User authUser = (User) auth.getPrincipal();
            return expectedUsername.equals(authUser.getUsername());
        }
        return false;
    }

    private static boolean isAuthenticated(final Authentication authentication) {
        return authentication != null
                && authentication.isAuthenticated()
                && !(authentication instanceof AnonymousAuthenticationToken);
    }
}

And then the controller method looks something like this.

@GetMapping("/basic-username-allowed-resource-provided-through-spring-bean")
@PreAuthorize("@customSecurityExpression.isUsernameEqualToBasic()")
public String getBasicUsernameResourceProvidedBySpringBean() {
    return "Only user that is authenticated and has username equal to 'basic' can see this";
}

Conclusion

In this blog post, we had a quick peek into how we can leverage the modularity of the Spring Security architecture to enhance the default functionality and tailor it to our specific needs. Also, towards the end of the blog post, an example was provided on how to achieve similar functionality by relying on existing powerful concepts that Spring offers. The examples were pretty simple for the sake of the demonstration, but the concept holds regardless of how complex the security logic may be.

Hope you enjoyed it and learned something new. Until next time, cheers!

Next

Blog

Too Summer for School?

Company

Our people really love it here

How it all started

Est. in 2014., gathering eight employees with eyes set on the future. No matter how set they were, they couldn’t predict the success and extent of growth that would ensue. Today there are more than 100 of us, and people are here to stay.

Stability in unstable times

The turmoil of 2020 caused great inconvenience for people all over the world. However, this did not affect our business. Quite the opposite — we not only kept all jobs and salaries intact, but we also grew in size. And we keep expanding. 

Contact

We’d love to hear from you