Member post originally published on the Cerbos blog by James Walker

Decoupling authorization from your main application code makes authorization more scalable, easier to maintain, and simpler to integrate with your components. However, these benefits are difficult to realize if you don’t consciously plan for them within your authorization implementation.

Decoupled authorization can add new technical challenges that aren’t always apparent at the start of a project. In this article, you’ll explore some of the problems with decoupled authorization. You’ll also see a few useful strategies for avoiding these pitfalls so you can develop effective authorization solutions.

What is decoupled authorization?

Decoupled authorization refers to separating your service’s authorization routines from your main application code.

In monolithic applications, authorization is handled by functions and classes that live inside your single codebase. Decoupled authorization refers to repositioning these components as a standalone service that your main code interfaces with. The interface normally consists of network calls to an API that the authorization component provides.

Decoupled authorization is often used to integrate external identity providers. You can delegate user account storage and permissions management to a platform that’s purpose-built for the task. Your application can make network requests to the platform whenever it needs to authorize an action.

This model keeps authorization logic separate from your application, making it more testable and easier to iterate upon in isolation. It also centralizes the implementation of authorization policies, ensuring all your services apply the same restrictions. Any new services you develop can reuse the decoupled authorization component without duplicating its logic. This isn’t possible when authorization is tightly coupled to specific codebases.

Technical complexities when implementing decoupled authorization

Decoupled authorization has clear advantages over inflexible authorization logic that’s written directly into application components. Decoupling authorization into its own service can increase overall complexity, though. You have to develop and maintain two services while ensuring they remain compatible with each other. Here are four specific kinds of technical complexity you’ll face.

Keep reading

Integration with the main application or service

Plugging the decoupled authorization layer back into your main application can be harder than you think. Whereas authorization has historically been a synchronous process without side effects, decoupling introduces the potential for system failures when the authorization service can’t be reached or an unknown response is received.

The decoupled authorization component must be carefully integrated to ensure the implementation is reliable. The application will need to retry calls to the authorization layer that fail due to a flaky network, for example. When no response can be obtained, perhaps because the authorization component has failed, the app should deny the user’s request to prevent authorization being inadvertently granted.

Each app you develop will need to integrate the authorization layer before it can be consumed. These integrations should be backed by tests that verify the app correctly handles different authorization outcomes, such as grant, deny, and failure.

Management of user accounts and permissions

Decoupling authorization from application code doesn’t mean you can forget about user accounts and permissions. These still need to be managed by your authorization layer, either directly within your authorization component or with an external identity provider.

The service you use should be flexible enough to store all the user data you require. All but the simplest systems will require a granular permissions model with support for roles and groups. You’ll need a mechanism for setting up and maintaining this data, either via scriptable APIs for automated provisioning or an accessible web UI for human use.

Decoupling authorization without planning how you’ll manage user accounts can cause problems as you reintegrate your components into your application. Apps need a dependable mechanism for establishing the user’s identity, retrieving relevant attributes such as their team and project, and checking which permissions have been assigned. All this info has to be centralized across your services to preserve consistency.

Handling data filtering and pagination

Services that filter data and display results over multiple pages need to be adjusted so users are only shown the items they can interact with. For example, an API request for the first ten invoices in a system could expose a different set of items depending on the user: department leaders might only see invoices approved by their department, while accounting staff have unfiltered access.

To achieve this, you’ll need to run your authorization policies against each item included in resource lists fetched by your application. The policies should verify that the items can be used in the current authorization context defined by the characteristics of the resource and the requesting user.

Unfortunately, performing authorization checks for lists of hundreds or thousands of records is often an inefficient process. It also has knock-on impacts on your pagination routines. When an item gets discarded because authorization is denied, a replacement must be loaded from the database to fill up the correct page size.

To properly address this complexity, plan how you’ll integrate your data queries with your decoupled authorization policies. Select an authorization provider that includes features such as query plans to preemptively retrieve the list of resources that a given user can interact with. Use the plan to filter database queries at the point they’re made, instead of individually running authorization policies against each item in your result sets.

Maintaining security and privacy

Decoupled authorization can create security weaknesses and privacy concerns. Any new service increases your threat perimeter and creates an additional target for attackers. Splitting authorization out of your application converts it into a standalone component that might be easier for bad actors to manipulate.

Traditional authorization models are invisible from outside your application. Authorization checks occur within the code, providing no opportunities for attackers to investigate their logic. Decoupled authorization can be more visible if your service isn’t properly protected. Network activity logs reveal the requests being made and the results obtained in response.

Insecure authorization APIs can leak data too. It’s vital to ensure your authorization service only reveals attributes that the user’s permitted to access. Otherwise, a rogue user or attacker could exfiltrate sensitive details by making direct calls to the authorization API.

Ensuring scalability and reliability

Authorization is a critical application component. It’s involved in almost every user interaction, demanding exceptional scalability and reliability. Poorly optimized authorization is a bottleneck that compromises your whole system’s performance.

Splitting authorization into its own service can increase latency as your apps have to wait for authorization checks to complete. Too many pending calls will increase congestion and lead to resource contention. If your authorization layer can’t scale with user activity, people will be left waiting at times of heavy usage.

Failure resilience is equally important. If authorization goes down, users won’t be able to log in or access functions that require permission checks. Authorization services should be deployed as multiple replicated instances to produce a fault tolerant architecture that can withstand individual instance crashes.

Strategies for overcoming decoupled authorization complexity

Decoupled authorization doesn’t have to be burdensome. You can mitigate complexity by sticking to proven strategies that promote an effective implementation. Building upon standard microservices patterns is a good starting point, but the following techniques offer specific best practices for splitting authorization from application code.

Use industry-standard protocols and frameworks

Integrating with industry-standard authorization protocols will increase your system’s interoperability and reduce the risk of oversights occurring. These well-known mechanisms let you centrally manage user identities and permission assignments, then consistently access the data from your services.

Supporting these technologies also gives your users a greater choice of sign-in options. They can bring their own identity from their favored platform, whether it’s Microsoft, Google, Apple, or another alternative. You can easily integrate these SSO options by using the same standards that they’re built upon.

Favor prebuilt authorization platforms and services

Building your own authorization platform is a daunting task. You’re responsible for checking your authorization logic and maintaining security standards. Selecting a dedicated platform such as Cerbos gives you all the benefits of decoupled authorization without the complexity.

These systems sit outside your stack and are integrated using their public APIs. You can register user accounts, handle logins, and set up authorization policies using RBACABAC and PBAC. They remove the complexity of inventing your own mechanisms for storing, evaluating, and querying authorization logic.

Seek specialized expertise and resources when you develop your own solutions

Some systems demand their own authorization layers either because of their sensitivity or due to legacy compatibility requirements. Developing your own authorization solution can be the only option in these circumstances, but you don’t have to do it on your own.

Minimizing features and keeping code paths lean is a good way to lessen security dangers and remove complexity. After distilling your solution to its essential requirements, you can more readily compare it to reference architectures or invite an external review. Seeking an audit and penetration test from a specialized authorization security team can provide confidence that your system’s protected, allowing you to get back to building your business functionality.

Start developing your own solution by clearly listing the vital features it requires. Next, plan out how you’ll deploy your authorization service, protect it from unauthorized access, and scale it to achieve sustained performance. You can then start working on the technical implementation. Try referring to open source authorization platforms if you need more guidance as many of the challenges you’ll face will have already been encountered by others.

Conclusion

Decoupling authorization from the code of individual applications is a best practice that enforces consistent authorization logic across services, simplifies testing, and is more scalable when implemented correctly.

Nonetheless, too many software teams struggle to effectively utilize decoupled authorization because of the extra technical complexity it creates. Poorly planned implementations can be unreliable and difficult to maintain.

Proactively developing strategies to identify and address this complexity will let you build and scale a decoupled authz system for your next project. Involve project managers, developers, and service operators to canvass opinion on potential drawbacks of the approach. Once you’ve identified any problems, you can add relevant mitigations to your development plan.

The measures you choose can be specific technical changes, such as implementing rate limiting to improve security, or more general steps that support your solution’s success. Extensive test suites, adoption of standardized protocols, and the use of expert guidance when needed all strip away the complexity of decoupled authz.