When thinking about implementing an authorization solution, we are faced with the choice of whether to use a library that would be embedded in our application code, or to set up a service to which our application will make authorization calls. In this post, we’ll examine the implications of choosing between the two in the context of authorization.
Let’s consider the most basic difference between a library and a service: A library is a collection of code that you compile into your application. Conversely, a service is an external capability that you access from your application. As a general rule, a library will always outperform a service in terms of speed — since it is baked into the application and no network calls are required to invoke it. But there is a price to pay: we inevitably couple our application to that library. This means that whatever logic is handled by that library is now an inherent part of our application. This might be acceptable in many use cases, but in the case of authorization, there are several considerations that might discourage this approach.
Let’s discuss some of the considerations to keep in mind when choosing between using an authorization library and an authorization service.
An authorization solution has to be able to make decisions based on the identity of a user, the permissions they’ve been given and the resources they’re accessing. Some of the information needed to make an authorization decision is going to be stored in a database, and the authorization solution has to be able to resolve this information in order to make decisions.
Being part of the application, an authorization library competes for the same resources as the application using it: When the library attempts to make a decision, it will query the database and load the results into the same memory allocated to the application. Consequently, the library’s performance limitation is tied to that of the application — which means its performance can only be scaled up and not out.
A service, on the other hand, maintains its own resources and when it makes authorization decisions it will do so without affecting the application’s performance. Since the authorization service is isolated from the application, it can be horizontally scaled as needed.
Consistency of Behavior
When we use an authorization library and embed the authorization logic in the application code, we are bound to have multiple integration points and each could have its own behavior since there’s no central authority to enforce any particular flow. A centralized authorization service, on the other hand, lifts the authorization logic out of the application and will behave consistently wherever it is invoked. As a single point of authority for the authorization logic, an authorization service makes it easier to maintain and modify the authorization logic consistently across all of the consuming services or applications.
When we consider microservice architectures, we could envision multiple services that are written in different languages. In this case, each service could apply an authorization strategy based on the authorization libraries that are available for the language the service is written in. As a result, we can’t expect the same consistency of behavior across services. The use of a centralized service that would govern the authorization logic would ensure consistency across all services.
Reasoning and Validation
A decision engine has to be idempotent and deterministic — it should always give the same result for the same input. This ensures that the decision produces predictable results that we can reason about.
Since an authorization library is baked into the application, it is dependent on the application state to produce its decisions. We can’t examine the results of the library’s decisions without running the application. Once we do run the application, we have no assurances that our inputs are not affected by changes in the application state. This fact makes it much harder to validate a library’s idempotency and determinism — and it makes it harder to reason about its authorization decisions.
A service is stateless and idempotent by design, and is detached from the application. This means that it could be tested and validated in isolation, independent of the application state. With an isolated service, it’s simply easier to reason about the authorization logic: we clearly know what our inputs are and that nothing has altered them, and so we can also know what the expected results would be.
Authorization information flow
The application needs to provide the decision engine with two pieces of information: the identity of the user and the resources that the user is trying to access. There are two options for this information to be relayed and then used by the decision engine:
- The application can provide identifiers for the user and the resource, and the decision engine will resolve the rest of the required information out of band
- The application will provide all the required information to the decision engine (and not just the identifiers)
For the most part, authorization libraries will rely on the latter option which means that they are completely dependent on the data provided to them by the application. When reasoning about the decisions made by an authorization library, we’ll also need to reason about the data it has been provided to understand why an authorization decision was made. In this model, we have to consider the possibility of unintended behavior, bugs or even a malicious actor changing the state of the application in a way that would affect the authorization logic.
Being isolated from the application, an authorization service has to be able resolve the information it needs to make a decision, which makes it less prone to errors that might arise due to problems with the application state.
Consumer and Contributors Personas
As we mentioned, using a library by definition means our authorization logic is embedded in the application code. If the only consumers and maintainers of the authorization logic are developers, this might not be a problem. But in organizations which include non-developers such as security teams, auditors and the like, this presents a challenge, since these teams will have to be able to sift through the codebase to find and understand the authorization logic.
An authorization service, on the other hand, can be consumed without requiring the consumer to understand the application logic at all, and it allows for the authorization logic to be developed, tested and maintained by non-developer members of the organization, outside of the application development cycle.
That said, while there are advantages for being able to separate the responsibility of application development from that of authorization policy development, this separation may come at a cost: if the application evolution requires changes to the authorization policy, more people are going to have to be involved — which may slow down the application development cycle.
The ability to provide audit trails is a key requirement of any authorization solution. Given the fact that a library is embedded in the application and has many points of integration it is significantly harder to audit than a service, since there’s no central place to aggregate decisions made by the library. A centralized authorization service lends itself to centralized auditing: since all authorization requests and authorization decisions are made in the service, we can easily collect and aggregate the information needed for auditing.
In this post we reviewed several questions that should be considered when choosing between using an authorization library and an authorization service. Hopefully we’ve presented the case for delegating authorization to a separate service.