mscharhag, Programming and Stuff;

A blog about programming and software development topics, mostly focused on Java technologies including Java EE, Spring and Grails.

Thursday, 15 October, 2020

Spring Security: Delegating authorization checks to bean methods

In this post we will learn how authorization checks can be delegated to bean methods with Spring Security. We will also learn why this can be very useful in many situations and how it improves testability of our application. Before we start, we will quickly look over common Spring Security authorization methods.

Spring Security and authorization

Spring Security provides multiple ways to deal with authorization. Some of them are based on user roles, others are based on more flexible expressions or custom beans. I don't want to go into details here, many articles are already available on this topic. Just to give you a quick overview, here are a few commented examples of common ways to define access rules with Spring Security:

Restricting URL access via a WebSecurityConfigurerAdapter:

public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        
            // restrict url access based on roles
            .antMatchers("/internal/**").hasRole("ADMIN")
            .antMatchers("/projects/**").hasRole("USER")
            
            // restrict url access based on expression
            .antMatchers("/users/{username}/profile")
                .access("principal.username == #username");            
    }
}

Using annotations to restrict access to methods:

@Service
public class SomeService {

    // Using Springs @Secured annotation for role checks
    @Secured("ROLE_ADMIN")
    public void doAdminStuff() { }

    // Using JSR 250 RolesAllowed annotation for role checks
    @RolesAllowed("ROLE_ADMIN")
    public void doOtherAdminStuff() { }

    // Using Springs @PreAuthorize annotation with an expression 
    @PreAuthorize("hasRole('ADMIN') and hasIpAddress('192.168.1.0/24')")
    public void doMoreAdminStuff() { }
    
    // Using an expression to delegate to a PermissionEvaluator bean
    @PreAuthorize("hasPermission(#stuff, 'write')")
    public void doStuff(Stuff stuff) { }
}

What to use when?

If roles are the only thing you need, it is easy. You just need to decide if you prefer defining the required roles based on URLs or based on methods in your Java code. If you prefer the later, just pick one annotation and use it consistently.

In case you need some ACL-like security (e.g. User x has permission y on object z) using @PreAuthorize with hasPermission(..) and a custom PermissionEvaluator is often a good choice. Also, have a look at the Spring Security ACL support.

However, there is a huge field between both approaches where roles are not enough but ACLs might be too fine grained or just the wrong tool. Here are a few example authorization rules that do not fit well into both solutions:

Access to a resource should only be given ..

  • .. to the owner of the resource (e.g. a user can only change his own profile)
  • .. to users with role x from department y
  • .. during standard business times
  • .. to administrators who signed in using two-factor authentication
  • .. to users who connect from specific IP addresses

All those examples can probably be solved by building a security expression and passing it to @PreAuthorize. However, in practice it is often not that simple.

Let us look at the last example (the ip address check). The previously shown code snippet contains a @PreAuthorize example that does exactly this:

@PreAuthorize("hasRole('ADMIN') and hasIpAddress('192.168.1.0/24')")

This looks nice as an example and shows what you can do with security expressions. However, now consider:

  • You possibly need to define more than one IP range. So, you have to combine multiple hasIpAddress(..) checks.
  • You probably do not want to hard-code IP addresses in your code. Instead they should be resolved from configuration properties.
  • It is likely that you need the same access check in different parts of your code. You probably do not want it to duplicate it over and over.

In other cases you might need to do a database look-up or call another external system to decide if a user is allowed to access a resource.

Simple expressions are fine. However, if they get larger and are scattered all over a code base they can become painful to maintain.

Side note: Spring Security implements method security by proxying the target bean. Security checks are then added via the proxy. If you don't know about proxies, you should probably read my post about the Proxy pattern.

Delegating access decisions to beans

Within security expressions we can reference beans using the @beanname syntax. This feature can help us to implement the previously described authentication rules.

Let's look at an example:

@Service
public class ProjectService {

    @PreAuthorize("@projectAccess.canUpdateProjectName(#id)")
    public void updateProjectName(int id, String newName) {
        ...
    }
    
    @PreAuthorize("@projectAccess.canDeleteProject(#id)")
    public void deleteProject(int id) {
        ...
    }
}

Here we define a ProjectService class with two methods, both annotated with @PreAuthorize. Within the security expression we delegate the access check to methods of a bean named projectAccess. Relevant method parameters (here id) are passed to projectAccess methods.

projectAccess looks like this:

@Component("projectAccess")
public class ProjectAccessHandler {

    private final ProjectRepository projectRepository;
    private final AuthenticatedUserService authenticatedUserService;

    public ProjectAccessHandler(ProjectRepository repo, AuthenticatedUserService aus) {
        this.projectRepository = repo;
        this.authenticatedUserService = aus;
    }

    public boolean canUpdateProjectName(int id) {
        return isProjectOwner(id);
    }

    public boolean canDeleteProject(int id) {
        return isProjectOwner(id);
    }

    private boolean isProjectOwner(int id) {
        User user = authenticatedUserService.getAuthenticatedUser();
        Project project = projectRepository.findById(id);
        return (project.getOwner().equals(user.getUsername()));
    }
}

It is a simple bean with two public methods that are called via security expressions. In both cases only the owner of the project is allowed to perform the operation. To determine the project owner we first have to look-up the related project by using a ProjectRepository bean.

The injected AuthenticatedUserService is a simple facade around Spring Security's SecurityContextHolder:

@Service
public class AuthenticatedUserService {
    public User getAuthenticatedUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return (User) authentication.getPrincipal();
    }
}

This cleans up our code a little bit because it removes Spring Security internals (and the type cast) from our access control logic. It also becomes helpful when writing unit tests. This way we do not have to deal with static method calls during tests.

Note we use the standard Spring Security User class for simplicity in this example. Often it is a good idea to create your own customized class as principal. However, this is something for another blog post.

Testing access rules

Another important benefit of this approach is that we can test access rules in simple unit tests. No Spring application context is required to evaluate @PreAuthorize expressions. This speeds up tests a lot.

A simple test for canUpdateProjectName(..) might look like this:

public class ProjectAccessHandlerTest {

    private ProjectRepository repository = mock(ProjectRepository.class);
    private AuthenticatedUserService service = mock(AuthenticatedUserService.class);
    private ProjectAccessHandler accessHandler = new ProjectAccessHandler(repository, service);
    private User john = new User("John", "password", Collections.emptyList());

    @Test
    public void canUpdateProjectName_isOwner() {
        Project project = new Project(1, "John", "John's project");
        when(repository.findById(1)).thenReturn(project);
        when(service.getAuthenticatedUser()).thenReturn(john);
        assertTrue(accessHandler.canUpdateProjectName(1));
    }

    @Test
    public void canUpdateProjectName_isNotOwner() {
        Project project = new Project(1, "Anna", "Anna's project");
        when(repository.findById(1)).thenReturn(project);
        when(service.getAuthenticatedUser()).thenReturn(john);
        assertFalse(accessHandler.canUpdateProjectName(1));
    }
}

Summary

Many authorization requirements cannot be solved by using roles alone and ACLs often do not fit. In those situation it can be a viable solution to create separate beans for handling access checks. With @PreAuthorize we can delegate the authorization check to those beans. This also simplifies writing tests as we do not have to create a Spring application context to test authorization constraints.

You can find the shown example code on GitHub.

Leave a reply