A way to unify two authorization methods - Part I
Time to talk about security today. Security is one of the most important aspects of a system, and probably one of the most boring to implement.
It is usually divided in two steps:
authentication and authorization. In short words, authentication gives access to a system. It is a yes/no question: do I know you? On the other hand, authorization controls permissions (what parts of the system you can access). In this post, I am going to talk about the latter.
As I said in this very blog, Playtomic evolves so fast (do you remember our microservices architecture?). So fast that we ended with two authorization systems:
one based on roles and another one based on authorities.
The main difference is that authorities define fine-grained permissions, while roles usually define sets of permissions based on the type of the user.
For instance, an authority could be “read_users” while a role could be USER_
ADMINISTRATOR.
We like the authorities because each service is responsible of defining their own authorities. On the other hand, roles are useful because define types of users of the system. They are not defined by the services themselves, but by the whole system.
We could argue why we got there in the first time. However, I learned from two years in a startup that we don’t discuss guilt but solutions. And solutions must be fast to implement and don’t have to be perfect but nearly :)
So, this is our problem:
- Two authorization schemes that we want to merge, or make compatible.
- Authorization based on authorities simplifies the definition of the access rules to your resources, authorization based on roles eases the access configuration of your users.
- There are several microservices already running in production, so that, the fewer the changes, the better. Some use roles, some authorities.
- We use JSON Web Tokens (JWT) to move the authentication/authorization through the services. It can contain roles or authorities. So that, at this point we can access some services but not others depending on how the jwt was generated.
Our purpose is to define a “framework” that allows us to write permission rules in a simple way. It is common that your security logic ends scattered all over your code. By writing permissions using authorities in our controllers and services, we aim to separate the type (role) of the user that access from the actual permission it has. By contrast, we wanted to be able to assign roles to the users, because it is easier to understand what type of operations that user should be able to do.
Extra:
changes have to be as minimal as possible. It would be preferable if services are independent. That is, they don’t have to call to another one to get the transformation from roles to authorities.
## How authorization was working at this point: Spring security.
- When the user is logged, a jwt is generated. All requests will include that jwt as their credentials.
- A security filter intercepts all requests, reads the jwt and gets the roles/authorities.
- Controllers or services are protected via annotation (@PreAuthorize), via config (HttpSecurity in WebSecurityConfigurerAdapter).
- Finer-grained permission (i.e can this user modify this precise resource?) where handled programmatically accessing the Authorization.
Example with authorities and PreAuthorize:
@PreAuthorize("hasAnyAuthority('admin_venues', 'read_venues')")
@RequestMapping(path = "/venue/{venue_id}", method = RequestMethod.GET)
public Venue getVenue(@Nonnull String venueId) {
return venueService.getVenue(venueId);
}
Example with roles and HttpSecurity:
@Configuration
public class CustomWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/venue/**").hasAnyRole("VENUE_ADMINISTRATOR")
// ...
// see JwtAuthorizationFilter below
.addFilterBefore(jwtAuthorizationFilter, AnonymousAuthenticationFilter.class)
// ...
}
## How authorization is going to work from this point:
Each service defines:
- A mapper from roles to authorities. Good
:
independent a easy to implement. Bad:
if a new role is added, you have to modify this piece. On the other hand, we could add a service that
knows the mapping, but then we have to add authorities to a role each time a new service appears. Trade-off. Choose your one.
- A checker that verifies your access.
/**
* AbstractJwtAuthorizationFilter reads the jwt and handles the errors when
* it cannot be verified or misses some expected data.
*/
@Component
@Slf4j
public class JwtAuthorizationFilter extends AbstractJwtAuthorizationFilter {
@Nonnull
private RolesToAuthorities authoritiesProvider;
public JwtAuthorizationFilter(@Nonnull ObjectProvider<RolesToAuthorities> authoritiesProvider) {
this.authoritiesProvider = authoritiesProvider.getIfAvailable(() -> new DefaultAnemoneRolesToAuthorities());
}
/**
* User contains user data from the jwt (such as, id) and roles/authorities.
*/
@Override
protected boolean handleInternalAuthorization(@Nonnull HttpServletResponse httpResponse, @Nullable User user, @Nonnull String bearer) {
if (userContext == null) {
httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
log.info("userContext == null, unauthorized.");
return false;
}
// CurentUserDetails is our implementation of UserDetails
// By using authoriesProvider, maps the roles/authorities on User to the final authorities.
CurrentUserDetails currentUser = new CurrentUserDetails(userContext, authoritiesProvider);
SessionAuthenticationToken token = new SessionAuthenticationToken(currentUser, bearer);
SecurityContextHolder.getContext().setAuthentication(token);
return true;
}
}
public interface RolesToAuthorities {
@Nonnull
Collection<GrantedAuthority> toAuthorities(@Nonnull String role);
}
We have abstracted this checker into:
- A class that gets the owner of the object being accessed (dependant of the object your are accessing).
- A class that checks that your are the owner of the object (generic).
public interface PermissionChecker<T> {
boolean isAllowed(@Nonnull String permission, @Nonnull Authentication auth, @Nullable T object);
}
@Getter
@AllArgsConstructor
public class ByResourceOwnershipPermissionChecker<T> implements PermissionChecker<T> {
@Nonnull
private String resourceType;
@Nonnull
private GetOwner<T> getOwner;
protected boolean isAdmin(@Nonnull Authentication auth) {
final GrantedAuthority adminAuthority = new SimpleGrantedAuthority("admin_" + resourceType);
return auth.getAuthorities().contains(adminAuthority);
}
protected boolean canOperateOnOwned(@Nonnull String permission, @Nonnull Authentication auth) {
final GrantedAuthority ownsAuthority = new SimpleGrantedAuthority(permission + "_own_" + resourceType);
return auth.getAuthorities().contains(ownsAuthority);
}
final public boolean isAllowed(@Nonnull String permission, @Nonnull Authentication auth, @Nullable T object) {
if (isAdmin(auth)) {
return true;
}
if (!canOperateOnOwned(permission, auth)) {
return false;
}
return ownedBy(object, auth);
}
boolean ownedBy(@Nullable T object, Authentication auth) {
if (object == null) {
return false;
}
Object p = auth.getPrincipal();
if (p == null || !(p instanceof UserDetails)) {
return false;
}
Object ownerId = getOwner.apply(object);
if (ownerId == null) {
return false;
}
String id = ((UserDetails)p).getUsername();
// The anonymous case
if (id == null) {
return false;
}
return ownerId.equals(id);
}
public interface GetOwner<C> {
@Nullable Object apply(@Nonnull C o);
}
}
PermissionChecker checker =
new ByResourceOwnershipPermissionChecker<TestObject>("objects", (t) -> t.getOwnerId());
@Component
public class VenueRolesToAuthorities extends DefaultAnemoneRolesToAuthorities {
@Override
@Nonnull
public Collection<GrantedAuthority> toAuthorities(@Nonnull String role) {
if ("VENUE_ADMINISTRATOR".equalsIgnoreCase(role)) {
return Arrays.asList(ga("admin_venues"), ga("read_venues"));
}
return Arrays.asList(ga(role));
}
@Nonnull
public static final GrantedAuthority ga(@Nonnull String authority) {
return new SimpleGrantedAuthority(authority);
}
}
I am going to stop here as this post is already pretty long. We have seen the skeleton of an authorization system that works with roles and authorities (actually, it works on authorities but allows defining permissions in term of roles). Following posts will show a real example with uses of ByResourceOwnershipPermissionChecker.
Nice to have:
- Use custom claims in your jwt to specify what authorities/roles apply to what resources.