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: m9-lesson4

If you want to skip and see the complete implementation, feel free to jump ahead and import: 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 more stricter UnanimousBased:

@Bean
public AccessDecisionManager unnanimous(){
    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(unnanimous())


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, not 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();
}

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