Designing Your Java Policy Enforcement Point (PEP)
To get a more general background in authorization, check out my previous posts about Attribute-Based Access Control and our authorization investigation at Split. This post is specific to implementing the PEP piece of an ABAC system or the equivalent in a Policy-Based Access Control (PBAC) system.
If you’re implementing an authorization system using ABAC or something similar, you will likely find yourself needing to build a Policy Enforcement Point (PEP). The PEP is the place in your application that calls your authorization system (in our case, a separate authorization service).
At a high level, the PEP needs to intercept any or all requests before they’re executed. Once the PEP intercepts the call, it needs to determine if that call requires authorization. If so, it gathers the data needed for making an authorization decision. It then needs to be able to interpret the authorization service response. If authorization failed, it needs to translate the failure into the correct error codes for the original client and prevent continued execution. If it succeeded, execution should continue. In some cases, you may even need to provide more custom experiences, such as the client can continue but needs to click through an NDA screen (obligations in ABAC).
There are a few main options for implementing all of this (discussed in more depth here). To summarize here, you can place your PEP into something like an API Gateway, since the gateway is routing all of your calls anyway. This provides a single choke-point that can intercept the calls and verify authorization before allowing them to continue. The other main option (which I will be discussing here) is to add a check into the call stack of every single endpoint on each of your services.
It may sound like a lot to add something to the call stack for every endpoint, but it can be much easier than it sounds. At both Box and Split, we implemented our own custom libraries leveraging custom Java annotations and the Jersey Filter Chain. It’s worth noting that there are likely other technologies that could be leveraged instead for part or all of this, but Box and Split independently came to very similar solutions, so I wanted to share what we did.
At both Box and Split, we wrote our PEP as a new library. At Box, this was the obvious choice because authorization was owned by an independent team, and several existing microservices could use this library almost immediately. At Split, however, all our initial use-cases were within our existing monolith, so we could have easily added the code there. We implemented it as a library anyway because we are starting our journey towards microservices and this authorization service is one of our first steps. Given that, we hope to soon have several services that need to leverage the PEP. Perhaps the most challenging part about implementing it as a library is cleanly separating the logic that we know all services will want from the logic specific to how our current service is implemented. For example, do you include the actual filter in the library since every service will need some sort of filter? Or do you leave that to the service since the exact priority of the filter may be service-dependent, and some services may use a Jersey filter alternative?
Jersey Filter Chain
The first piece of our solution involves leveraging the Jersey filter chain. The Jersey filter chain provides the ability to apply filters on each request to your service.
There are several types of filters called at different points in the call stack — before the request goes to the endpoint and after the endpoint, right before it returns to the caller. There is even nuance about where you put the filter within these stacks. A filter before your code is most often used for things like authentication, authorization, and logging. A filter after the code is most often used for things like adding custom headers, providing consistent error responses, and logging. In this case, we added a
ContainerRequestFilter with priority
Priorities.AUTHORIZATION. This puts it before the code is executed, after authentication, but before many other things. It’s important to remember that when you add a filter to the filter chain, it is run on every single request, no matter what. This increases the importance of exiting early, handling failures gracefully, and above all, performance. Creating the filter involves implementing the
ContainerRequestFilter interface and then registering the filter — you can read more about the specifics here.
Now that we have a filter, we have a point where we can check authorization in the call stack before every request. But how do we know what permissions are required for the specific endpoint being called? It may be the case that some endpoints require the user to be an admin, while for others, the user doesn’t even need to be logged in. To accomplish this, we want to add a small amount of metadata to the endpoint itself. This is where Custom Java Annotations come in.
In Java, annotations add a way to add metadata to your code. This metadata can be used to specify information about a particular variable or method that can be read and used during runtime through reflection. This means that code in a filter can pull this annotation metadata from the targeted method. There are annotations included in Java, but you can also create custom method-level or class-level annotations. In this case, we want to create a method-level annotation. We named our filter
@SplitAuthorization, but the name is totally up to you. Then within that annotation, you can add additional data that you need. For example, we also added an operations field, where the developer can specify all of the permissions required to access that particular endpoint. We also wanted to fetch the
organizationId before calling the authorization service (to avoid network calls), so we include data about how to fetch that ID in the annotation. Our resulting annotated endpoint looks something like this:
Putting It All Together
Now that we have the annotation, we need to actually use the annotation to enforce authorization. Back in our filter, we need to check for the annotation. To do this, we leveraged
ResourceInfo, injected using
@Context. Once we have the
ResourceInfo, we can get the method that the request is targeting, which allows us to get the annotations:
Annotationannotations = resourceInfo.getResourceMethod().getAnnotations(); Then we can check if one of them is our
@SplitAuthorization annotation by using
annotation.annotationType().equals(SplitAuthorization.class). Once we find our annotation, we can get the specific metadata set on it out as well. Note that if we want to get the
ResourceInfo, our filter can not be a
@PreMatching filter. That was pretty dense, so all together, that would look like:
Wrapping it up
Likely I haven’t been able to find any open source libraries for this because many of the details will be custom for your implementation. How exactly you call your authorization will depend on if it’s a separate service or not, how that call is structured, and more.
Additionally, if you want to send additional information with your request (such as the recource’s
organizationId like we do), fetching that information is specific to your system. All of that said, while there is a lot of customization in the PEP, a lot of the basic structure can be the same, no matter your details. At both Split and Box, we built a custom PEP library using the Jersey filter chain and Custom Java annotations.
- I’ve written several blog posts giving more background on authorization more generally in this one about Attribute-Based Access Control and this one about our authorization investigation at Split
- More information about writing your own Jersey filter (and the filter chain in general).
- This is a good resource for more about writing custom annotations in Java.