Lesson 4: A Custom AccessDecisionVoter

1. Goals

The goal of this lesson is to go beyond the standard voters and show how we can set up a custom one to implement an interesting scenario.


2. Lesson Notes

The relevant module you need to import when you're starting with this lesson is: lssc-module9/m9-lesson4

If you want to skip and see the complete implementation, feel free to jump ahead and import: lssc-module9/m9-lesson5

The credentials used in the code of this lesson are:

  • user/pass (in-memory)
  • admin/pass (in-memory) (has the ADMIN role)


2.1. A Stricter Access Decision Manager

Let's start by changing the AccessDecisionManager implementation from AffirmativeBased to the stricter UnanimousBased:

@Bean
public AccessDecisionManager unanimous(){
    List<AccessDecisionVoter<? extends Object>> voters = Lists.newArrayList(
      new RoleVoter(), new AuthenticatedVoter(), new WebExpressionVoter());
    return new UnanimousBased(voters);
}

Now, let's wire it in:

.anyRequest().authenticated().accessDecisionManager(unanimous())


And let's secure /secured with an extra role:

.antMatchers("/secured").access("hasRole('ADMIN')")


We can now try to access /secured and debug through the decision flow to see the stricter voting run.


2.2. The Custom Scenario

We're going to explore the following scenario - we need to be able to lock users out and have that lockout apply in real-time (not after a login).

The need for this kind of real-time lockout is simple - if a user is locked out, then they’re a serious security concern so we need to make sure that their current session cannot be used to do any damage.

Before we start, note that there are multiple ways to implement this kind of scenario - this is just one of them.

Let’s first create the new voter:

public class RealTimeLockVoter implements AccessDecisionVoter<Object> { ...

Now, before implementing the actual voting logic, let’s create the very simplistic cache for users that are locked out:

public final class LockedUsers {
    private static final Set<String> lockedUsersSets = Sets.newHashSet();
    private LockedUsers() {}
    public static final boolean isLocked(final String username) {
        return lockedUsersSets.contains(username);
    }
    public static final void lock(final String username) {
        lockedUsersSets.add(username);
    }
}

Finally - let’s get back to the voting logic:

if (LockedUsers.isLocked(authentication.getName())) {
    return ACCESS_DENIED;
}
return ACCESS_GRANTED;

And we’re done - simple and to the point.

Now if we go through a simple scenario

- login with admin (so that we can access /secured)

- then (with the help of the Display view in Eclipse) we add the user to the locked cache

- and refresh => we're locked out


2.3. Upgrade Notes

As we've already mentioned in the previous lessons, in Spring Boot 2 it is required to encode the passwords. It means that they can not be stored anymore in a plain text format. Therefore, the user accounts should be created with the encoded passwords:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.
        inMemoryAuthentication().passwordEncoder(passwordEncoder())
        .withUser("user").password(passwordEncoder().encode("pass")).authorities("USER").and()
        .withUser("admin").password(passwordEncoder().encode("pass")).authorities("ADMIN");
}

Naturally, the corresponding bean should be added into the context:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

Also, after upgrading to Spring Boot 2.6.0+, both the AccessDecisionManager and PasswordEncoder @Beans, previously defined inside the LssSecurityConfig class, were moved to the LssApp class to avoid circular dependency issues.


Upgrade AccessDecisionVoter and AccessDecisionManager to AuthorizationManager

Since Spring Security 6.0, the AccessDecisionVoter and AccessDecisionManager have been deprecated. In fact, the whole authorization mechanism was updated, now relying solely on AuthorizationManager instances to determine access to the resources.

With this new approach, we replace the RealTimeLockVoter with RealTimeLockAuthorizationManager, which implements AuthorizationManager and move the authorization logic to the check() method:

public class RealTimeLockAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authenticationSupplier,
                RequestAuthorizationContext object) {
        Authentication authentication = authenticationSupplier.get();

        if(authentication == null) {
            return new AuthorizationDecision(false);
        }

        if (LockedUsers.isLocked(authentication.getName())) {
            return new AuthorizationDecision(false);
        }

        return new AuthorizationDecision(true);
    }
}

And instead of building an UnanimousBased AccessDecisionManager with a list of AccessDecisionVoter, we have to define the authorization rules for each request matcher, with the possibility of building composite AuthorizationManagers.

So, in our case, we have:

  • A “base” access rule for all of our endpoints, requiring the request to be authenticated and additionally that the user hasn’t been locked out
  • Particularly for "/secured", the user also needs to have an “ADMIN” role

We can define our base rules to be re-used easily:

private AuthorizationManager<RequestAuthorizationContext> baseAccessRules =
  AuthorizationManagers.allOf(
    AuthenticatedAuthorizationManager.authenticated(),
    new RealTimeLockAuthorizationManager());

Note that we can combine different access restrains with the AuthorizationManagers.allOf method.

Next, in the LssSecurityConfig we can set up the HTTP requests access rules:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
    .authorizeHttpRequests((authorize) -> authorize
        .requestMatchers("/secured").access(AuthorizationManagers.allOf(
            baseAccessRules,
            AuthorityAuthorizationManager.hasRole("ADMIN")
            ))
        .anyRequest().access(baseAccessRules)
    )
    // ...
}


3. Resources

- Custom AccessDecisionVoters in Spring Security

- Custom Voters in the Spring Security Reference

LSS - A Custom AccessDecisionVoter - transcript.pdf
Complete and Continue