What is Authorization?

Authorization is the mechanism for controlling who can do what in an application. It's how you make sure users have access to their own data, and aren't allowed to see data that isn't theirs. There are a common set of architecture patterns for authorization that suit any application architecture — knowing those patterns makes writing authorization code much easier. This guide will teach you those patterns.

Authorization is a critical element of every application, but it's nearly invisible to users. It's also usually unrelated to the core features or business logic that you're working on.

1. Authentication vs. Authorization

Many folks often use the term “auth” to refer to both authentication and authorization, which are related and somewhat overlapping mechanisms in application security. But they aren’t the same thing.

Authentication is the mechanism for verifying who a user is. It’s the front door to the application. For example, a username and password together make up an identity (username) and a verification method (do you know the password). Other forms of authentication include OAuth and OpenID Connect (OIDC), which is often used for adding features like “Login with Google” or “Login with Facebook”; and SAML, which is a standard that enterprises use for giving employees a single login across multiple apps.

Authorization is the mechanism of controlling what the user can do. If authentication is the front door, authorization controls what doors you can open once you’re inside.

Authorization often builds on authentication, and the two overlap most closely when information about who the user is becomes an input to determining what they can do. For instance, once a user is authenticated, we might use her username to look up her permissions; or we might be able to infer her permissions based on some other attribute that we can look up.

This course focuses on authorization, and will only touch on authentication when relevant. We assume you have an authentication system in place, or can set one up by following guidance readily available elsewhere.

2. What Authorization Looks Like in an App

We'd like to show you examples of authorization in practice, not just in theory. To do that, we've built an example app where we can demonstrate each concept.

Our application is GitClub, a website for source code hosting, collaboration, and version control. It's very similar to real-life git hosts GitHub and GitLab. GitClub offers a pure example of what motivates authorization in the first place — securing access to resources. One resource in GitClub is a repository, and repositories are access-restricted. Users may or may not be able to read or make changes to a repository. That means we need authorization!

Website Architecture

We'll start with a reasonable architecture for our service, based on the fantastic documentation provided by the folks at GitLab on its production architecture. If this looks complex – fear not! As we talk through where authorization fits into this architecture, we'll gradually peel away layers of complexity.

GitClub Architecture

The above diagram contains the main pieces of our architecture.

gitclub.dev handles three different kinds of traffic:

  • Visiting the website. This will return HTML for your browser, like the contents of the front page of gitclub.com.
  • API requests. This handles requests for data from users or third-party integrations and will return JSON or another structured data format.
  • Git connections, like when you git clone <https://gitclub.dev/oso/authzacademy>. These can be SSH or HTTP connections.

All the requests connect to one point: the Proxy, so named because it stands in for ("proxies") a web server. The proxy will then redirect connections to the right location. In a full system, a proxy can be made up of many parts, like load balancers and authenticating proxies.

The website and the API can access the database (DB), where information like user account data is kept.

The API and Git connections will access the Filesystem (FS), where the Git repositories are stored.

For authentication (as a reminder: see the introduction for authentication vs authorization), our web application will handle authenticating the user using a simple username and password mechanism. After the initial authentication, the Web application returns a token to the user so that on subsequent requests they don’t need to resubmit their password. The Web application also lets users create API tokens for interacting with the API application. These tokens are known as “bearer tokens” - a lightweight token that grants the bearer of the token access.

The Git service performs authentication using the same username and password pair as the Web application.

An aside: monoliths vs. microservices

Our example architecture here is fairly monolithic. We only have a few applications running. For microservices, there are a few differences that we will call out as we encounter them, and we talk about in more depth in Chapter VI: Authorization in Microservices.

The Data Model

In this system, we have many Git Repositories.

Organizations allow group access to repositories. An Organization (like a company, or an open-source group) can have many Repositories. Each Organization can have many Users.

A single person is represented by a User. A User might be part of many Organizations.

Our Authorization Goal

Across our system, we want to enforce one authorization rule: a User can access a Repository only if they’re part of the Organization that owns that Repository.

We'll add more rules later!

3. Where To Put Our Authorization Logic

Now that we have our architecture outlined, let's think about where authorization could take place by following a request as it travels through our application components and see what happens.

One of our GitClub Users, “alice@acme.org” is accessing the application through the web interface. Suppose they attempt to visit the page of a specific repository, say https://gitclub.dev/acme/anvil. What happens?

They previously authenticated with their password, so the request carries the token as their credential. In this case, our User is allowed to access this repository, because acme is an organization and the User is a member of that organization.

As we trace the request through the infrastructure, we’ll keep coming back to the three main aspects of authorization:

  • Who is making the request?
  • What are they trying to do?
  • What are they doing it to?
Initial connection

First of all, the User's browser makes a connection with our externally-facing Proxy.

Who is making the request?

We don't yet know the identity of the User making the request. Doing so would require pulling the token out of the request. Perhaps we know their IP address.

What are they trying to do?

Establish a TLS connection.

What are they doing it to?

Host: gitclub.dev on port 443.

We're not yet performing application authorization, but it's possible to do some network-level authorization here. Maybe we have an allowlist of IP addresses or even require mutual TLS (although this is somewhat controversial).

At the proxy

The Proxy is configured to direct traffic to the Web, API, and Git servers as necessary. All three of these are ultimately going to need to do some amount of user authentication. As we described earlier, authentication is either the username/password combination or a bearer token.

If we have many downstream services that need to do authentication, there’s more value in adding an authenticating proxy. For example, both the Web Application and the API Application require a token to access most routes. If we handle the token validation in the proxy, then we could perform the following authorization:

Who is making the request?

The proxy validates the token included in the request, which may additionally contain information about the User. For example, we might use JSON-encoded data, much like in a JSON Web Token (JWT).

Suppose our decoded token looks like this:

https://cdn.prod.website-files.com/5f1483105c9a72fd0a3b662a/6051850fd07de863b8b6b99d_carbon (15) 1.png
The structure of a token

Then we know the User is “alice@acme.org”.

What are they trying to do?

By inspecting the HTTP request, we can learn that the User is making a GET request.

What are they doing it to?

Again, from inspecting the HTTP request, the URL is /acme/anvil.

Can we perform authorization based on what we know?

It depends on what kind of authorization we want to apply. Given only the information that exists in the request, we can only do authorization that applies at the route-level. Is the user “alice@acme.org” allowed to make GET requests to /acme/anvil?

The only information we have about the User is their email address. All users can make GET requests to paths of the form /<owner>/<repository> so this is allowed.

But if we want to enforce the full requirement - the User must be in the same organization as the repository - then we don’t have enough information. How can we get that information? We could consider adding more and more information to the token. Or we could configure our Proxy to access the Database. This approach introduces considerable complexity to the Proxy and duplication of Database access logic.

However, the Proxy is a good candidate to apply authorization-adjacent concerns, like rate-limited users, requiring an API key or authentication, and scanning for malicious payloads like you might find in a Web Application Firewall (WAF).

At the website router
GitClub architecture, highlighting the website and database.

We’ve covered the Proxy! Let's move on to the website and database.

Finally, our authenticated request gets to the router of our web application, where the router decides how to handle the request. It’s common at this point to have an authentication middleware convert the identity supplied in the request to a data model fetched from the database.

Who is making the request?

Let’s assume that authentication middleware has converted the identity supplied in the request to a User object, which gives us access to everything we would like to know about the User.

What are they trying to do?

We’re still working with an HTTP Request object, and the HTTP method is a GET.

What are they doing it to?

We still only have the request object and path here: “/acme/anvil”.

Since we now have access to the application data, we could look up relevant information. For example, we can use our existing logic to look up the organization corresponding to /acme/anvil and see if our User belongs to that organization.

However, this is almost exactly what we will need to do in the next step at the controller layer. The controller will take the request, lookup data, perform any necessary data manipulation, and apply business logic.

Therefore, if we try to take this approach of performing the authorization in the middleware, we'll end up duplicating logic and potentially making redundant calls to the database.

There are a few scenarios in which it makes sense to do authorization at this layer:

  1. To apply a defense-in-depth approach to authorization by checking some simple route-level properties. For example, if there is an admins-only area of the website /admin/... and admin permission is stored on the User object, then we could apply this check quickly.
  2. To ensure that authorization was performed somewhere in handling the request, perhaps at a deeper layer of the application. This gets some benefit of ensuring authorization is applied everywhere, without the restriction of only covering coarse route-level concerns.
  3. In applications where request-routing and data accessing are tightly coupled. For example, API-driven applications with a thin REST or GraphQL interface in front of a database. In this case, the Router + Controller layers are effectively compressed into one layer, and authorization can be applied to both at the same time. More on that next.
In the web application/controller

The Web Router mapped GET /acme/anvil to our controller method. Let’s say this method is view_repository(owner: "acme", name: "anvil").

In this method, we're responsible for collating the data necessary to show the /acme/anvil  webpage and passing that data on to the view. At GitClub, we like sticking with straightforward code, so we're doing some simple server-side rendering of our pages. So we'll get all the data for the UI and render a template for the User.

Without going into too many details, our repository view is probably going to include some of the source code, issues, pull requests, contributors, etc., as well as basic information about the repository itself.

Ultimately, all of the data displayed to the User should be authorized. Can they see the repository in the first place? What about all the additional data on the page?

Who is making the request?

From the previous steps, we can assume the User object was fetched as part of the authentication middleware.

What are they trying to do?

View a repository page and access repository information. Within the controller method, we have the full context of what the User is doing.

What are they doing it to?

The repository acme/anvil, which we will retrieve from the database as part of the method anyway.

Finally, it seems like we have all our ducks in a row. We have all the data we need and we know precisely what the User is trying to achieve. The anvil repository is owned by the acme organization, and the User is a member of that organization, so yes! We should let them read this.

Extra credit

But wait, there's more!

What about all the auxiliary data accessed as part of rendering the view? Repository members, issues, comments, etc. Are all of these visible to the User?

Well, we can continue to make access control decisions as necessary. We have access to all the relevant information and we know what data we’re trying to retrieve on behalf of the User.

We can go even further here. Suppose we know the User doesn't have access to configure the repository settings, since they aren’t an admin of the repository. That means we can return this information to the view renderer so that it can hide that option from the page. We’ll cover this more in a future guide.

If the User was not authorized to view the repository, we're also in the ideal place to render a view that displays helpful information or return a “Forbidden” response.

At the database/database connector

There is one final place we could apply authorization: when fetching data from the database. There are a few places where we could consider authorizing this action: over the SQL query itself (maybe with a middleware or query proxy), or maybe even in the database.

Who is making the request?

We could potentially include information about the User in the query context.

What are they trying to do?

Run a SQL SELECT statement.

What are they doing it to?

The repositories table filtered by name = "acme/anvil".

We’re now in the data access layer, so we need to include any necessary application context with the database connection/request or in the database query.

Remember the logic we want to enforce:

Users can read repositories if they belong to the same organization as the repository.

The data layer could be the ideal place to enforce this rule because it can be easily represented as a SQL query. In this case, when running a SELECT statement on the repositories table, we could join on the organization_members table and only select repositories in the same organization as the User.

For example, if the original query was:

A SELECT statement on the repositories table.

then with authorization applied we would instead have:

A SELECT statement on the repositories table with a join.

The advantage of this approach is that it allows you to do more than just answer yes/no authorization queries. For example, you can list all repositories a User can view on their homepage by applying the same filters.

The challenge here is how to generate the query filters for authorization without duplicating logic for constructing and managing SQL statements, which we might otherwise handle in the application. Due to this, it often makes the most sense to implement authorization filters by using the same mechanisms used elsewhere in the application.


In summary, there are a few candidates for where to apply authorization:

  • At the network layer. This layer has very limited data and only allows for simple network access control measures like allow/deny lists. We shouldn’t focus on authorization here.
  • At the Proxy or Router. This is best for route-level authorization, as more granular access control generally requires an additional call to a service or database to make an authorization decision.
  • In the Application/Controller. Here, all information is available to us, so we can easily apply our authorization requirements. This is a good place to put our authorization logic.
  • In the Database: if the application generates database filters, we can apply our authorization here. This lets us ask more broad questions about access, so when possible, it’s best to enforce authorization here.

It’s easiest to apply authorization as close as possible to the resource we want to protect, since there is the most context about precisely what the user is trying to do, and data available to make a decision.

4. Adding Authorization to an Application

We've spoken about where in our architecture we want to apply authorization. We saw that in many cases we want to apply the authorization in the application so that we have full access to the application context.

But how do we design, implement, and perform the authorization?

Naïve Approach

When many people start, they don't "implement authorization" as a separate development step. They simply add whatever checks seem necessary to their application code as they go. The reason this is so common is that the logic starts out feeling simple. You may not even notice it’s there! For instance, you might filter all repositories by the User's ID or organization ID as part of the database query. Since the application context is right there, it’s easy to implement it this way.

That quickly gets difficult. As the number of places where you need to apply authorization increases, you end up duplicating the same logic. Making any change then requires us to remember every place our logic is duplicated.

In our previous example, we enforced that the User could only see the page /acme/anvil if they were a member of the Acme organization. The same logic applies to other sub-pages such as /acme/anvil/issues and /acme/anvil/members and so on. Each method that handles these pages needs to repeat the same logic.

Now suppose that we add a feature that permits inviting users outside an organization to collaborate on the specific repository. We now need to add this check to our authorization handler, which in the naïve case means every method that handles repository pages.

Separating our authorization from our application is difficult

Much like with any other piece of application logic, we’d like to apply “separation of concerns”, and write our authorization independently of our application logic. This turns out to be surprisingly difficult!  At first, maybe we refactor all repository views to first load and check permissions on the repository. But then we realize that our auth rules aren’t universal: only organization admins are allowed to access /acme/anvil/settings, and so we need to modify our repository abstraction to also include information about organization admins. Authorization is so deeply interwoven with the application that it can be hard to come up with a clean interface to split authorization from business logic.

How can we design something better?

Formalizing Our Authorization Model

We saw in the Example section above that we can often frame an authorization decision as:

  • Who is making the request
  • What are they trying to do
  • What are they doing it to

In writing about authorization, there are formal terms for each of these.

The "who" is called the actor. In many cases, this actor is just a user of the application.

"What they are trying to do" often comes down to a simple verb. E.g. create, read, update, delete (CRUD) as is common in APIs. We call these actions.

“What they are doing it to” is the resource*.* This might be a specific object in the application - in the case of GitLab, a repository or an organization.

This triple (under a variety of names) has been frequently used in authorization systems. For instance, the literature on Microsoft Azure uses “Security Principal”, “Action”, and “Resource”.

The benefit of introducing this structure into your application is twofold. First, you get consistent language to talk about authorization to cover both simple and complex use cases. An actor can be a simple user, but it could also be a third-party application acting on behalf of a user who has delegated permission to another user.

Second, it provides the beginnings of a clean interface. A simple authorization interface takes in the triple (actor, action, resource) and returns an allow or deny decision for the inputs.

What Interface To Use For Our Authorization API

Up to this point, we’ve mostly focused on where we can “apply” authorization, which referred to the entire process of evaluating the input request, extracting relevant information, combining this with additional data lookups, implementing the rules and checks, and even filtering data by permissions.

There are two important parts to this: the enforcement and the decision. The authorization interface is the boundary between these two things.

Suppose our interface is the method is_allowed(actor, action, resource), which would be invoked with the inputs we spoke about previously, for example, is_allowed(current_user, “read”, Repository(name: “acme/anvil”).

Enforcement is how we decide what to do with an authorization decision. This means extracting the actor, action, and resource from the request as we saw in the previous examples, and calling the is_allowed method.

In GitClub, if the user doesn’t have permission to read the /acme/anvil repository, we can either respond with an HTTP 403 Forbidden response or redirect the user to a different page. Other examples of enforcement include using database filters to restrict access to an entire collection of data.

The decision is how we implement the authorization interface: given the triple of inputs (actor, action, resource), we return a result. In our previous example, the decision was: the User is allowed to access this repository, because acme/anvil is in the acme organization, and the User is a member of that organization.

Decisions don’t necessarily need to be a binary yes/no, but might be further dependent on other events or checks, or have additional effects, like emit warnings.

Enforcement and decision architecture

When we spoke about needing to apply authorization in the application or database layers, this was primarily referring to the enforcement. We will cover options around enforcement in a future guide. However, the decision is a separate piece and can be implemented in different places.

Options for Implementing Authorization Decisions

Making an authorization decision requires two pieces of information, data and logic:

  • Authorization data is a subset of application data used for access control. E.g. Alice is a member of the Acme organization, and the acme/anvil repository belongs to the acme organization.
  • Authorization logic describes the abstract rules expressed over data and used to determine whether a user is allowed to perform an action on a resource. E.g. members of an organization are allowed to access repositories that belong to that organization.

There are two distinct approaches to making authorization decisions: centralized and decentralized. In the centralized approach, authorization decisions are delegated to a central authority which is provided with access to the necessary data and takes authorization logic as an input. In the decentralized approach, the application makes authorization decisions itself using the data it already has access to.

There is a third option, the hybrid approach, which takes a decentralized approach but applied to a system with multiple services and applications.

We'll cover the three alternatives here, along with concrete examples of these in the wild.

Decentralized authorization: keeping authorization in the application

The natural progression from the naive, ad hoc implementation is to move to an authorization system built into the application. This might look like refactoring the existing code so that enforcement and decision are separated by the interface we proposed earlier.

This might be a purpose-built do-it-yourself implementation, a native part of the framework you’re using, or a dedicated authorization library. For example, GitLab’s Declarative Policy framework, Pundit for Ruby/Rails, django-guardian for Python/Django, or (our favorite, because we wrote it) Oso for cross-language. Whatever approach is taken, there are a few pros and cons to this approach.

First of all, keeping the authorization logic in the application, rather than as a separate service, simplifies the development experience. Adding to your application just means including the code or library. Making any changes to permissions is the same as making changes to the application, and goes through the same process. This extends to other areas like testing and debugging - these all happen as they do for application code.

However, probably the most significant advantage of authorization in the application is access to data for decisions. From our example, making a decision requires checking whether the user belongs to a particular organization. If the application already uses this data – for example, our Web Application already knows how to list a user’s organizations – then we already have the mechanism to retrieve this information for an authorization decision.

A decision requires checking authorization logic and authorization data

With decentralised authorization, all applications need to have the necessary logic and data to make an authorization decision

On the other hand, when we have multiple applications – like our Web, API, and Git applications – then we may end up duplicating the same authorization logic across all applications. Even worse, we might end up duplicating the logic to fetch data. Our Git application has no need to know about organizations other than for authorization decisions.

Centralized authorization: adding a service

If we have many applications or services that deal with the same actors, actions, and resources, we shouldn’t duplicate our authorization code between them. We can decouple our authorization logic by adding a separate service that our applications call into.

Authorization data flow

In the centralised model, applications manage their own data and logic is decoupled from the applications, but the central service still needs to access all data.

There are a few distinct options for the central service, which mostly relate to how they access application data.

First of all, the central service could be built as a regular service that fetches data by either making requests to the other existing services, or by directly accessing the database. The downside of this approach is it creates a coupling between all services and the central service, either directly or indirectly. This becomes a problem, for instance, if we ever need to change the API of the central service – all other services will need to change.

Instead, we might make the service the owner of the data. Some production authorization systems do this, like Google's Zanzibar system. In this case, the service becomes the central source of truth for any data related to authorization. For example, what role a user has in an organization, or what organization a repository belongs to.

The benefit of this approach is that this service can be optimised for answering authorization questions. The downside is that it now becomes a dependency of all applications. Instead of the simple interface we proposed before this service needs to additionally serve whatever data the other applications need.

The final approach is to include the relevant application data as part of a request to the authorization service. Determining what data is "relevant" is about as hard as the authorization decision in the first place. In our previous example, the relevant data was all organizations and repositories the user belonged to.

All approaches share some additional disadvantages around the complexity of managing a separate service.

Hybrid approach

There is a third approach that combines some of the best elements of the above two.

In the hybrid model, each individual application or service manages its own data and authorization over that data. However, applications rely on other services to support authorization decisions.

Two services querying each other's authorization

In the hybrid model, each application only manages its own data and related authorization logic. Decisions delegate to other applications when necessary.

This is a model that can often be found when a monolith application has several smaller auxiliary services. The central monolith controls all data and exposes a simple endpoint to the other services. But this same pattern can be applied across multiple services too.

Take our GitClub example. Perhaps the API service manages organizations, repositories and user data. But the Git service needs to know whether a given user is allowed to read the repository source code. So it delegates the authorization decision to the API service.

To make this work requires extending our simple authorization interface to return more information. For example, if the API service can return as part of a request to /org/1/repo/2 what permissions the current user has on that repository, then the Git service can use these to make a trivial authorization decision.

We can distribute more of the authorization than this. Perhaps we have an “organizations” service to handle org data, and this can give us a list of organizations and permissions a user has.

The power of this approach is that it naturally follows the shape of your existing architecture. That is, services manage whatever data you had designed them to manage in the first place, and authorization becomes an extension of that.

A Quick Guide To Authorization Data Flow

In summary, there are a few different approaches to making authorization decisions:

  • The decentralized approach is the simplest to implement, since all applications manage their own authorization. This is the best approach for a small number of applications, or where decisions rely on data which is already managed by the application.
  • A centralized service can help keep decision logic consistent across multiple applications, and also make policy changes decoupled. However, the downside of this is that you’ll need to centralize a lot of authorization data to make decisions. This approach works well when many services need to make authorization decisions of the same set of data.
  • The hybrid approach leaves decision-making to individual applications, but makes those decisions accessible to other applications when necessary. This is the best approach for balancing the decoupling of logic and data across applications, but requires a consistent way to implement.

5. Putting Everything Together

Implementing authorization requires the following components:

  • An authentication system to identify who is making the request (the actor).
  • Enforcement, which takes in the request, translates it into (actor, action, resource) and passes these to the authorization decision process.
  • Authorization logic, which specifies how a decision is made, expressed over authorization data.
  • The decision implementation, which takes in the (actor, action, resource) as input, along with the authorization logic and data and returns a decision.
  • The resulting decision is used by the enforcement to return a response back to the actor.

While there are many permutations and combinations of the above, for most applications we typically recommend the following setup:

  • Use an identity provider to handle authentication.
  • Enforce authorization in the application itself.
  • Keep authorization logic separate from application code by adding an authorization interface.
  • Keep authorization data in the application. This means using a simple decentralized model for implementing decisions when your application has a small number of services or using a hybrid approach for multiple services.

Of course, every organization should assess its use case and trade-offs individually. If this is an area you’re exploring, we encourage you to join the community of developers in the Oso Slack! Our core engineering team is also in Slack and is happy to engage and answer your questions. If you want to kickstart the process of building authorization into your application, you can use Oso and learn more about it in the Oso documentation.

6. What's Next

Up to this point, we've covered different architectures for implementing authorization enforcement and decisions. However, the fun doesn't stop there.

Once you start building your authorization system as part of the overall application, other related areas start cropping up.

For example, what if we want to convey authorization information to the end-user? It's not an ideal user experience to be constantly told "this action was forbidden"; instead the user interface itself should indicate what the user can or cannot do. In order to do this, we need the backend to return this information to the user: if the user doesn't have write permission, then grey out the option to make changes.

Similarly, we might want to provide an admin interface that displays what permissions organization members have in the application. This could include listing what roles exist in the application, who has access to a specific resource, or what resources a specific user can access.

All of this involves thinking about authorization as a component of the overall application. We'll come back to the specifics of these use cases in future chapters.

Going beyond the simple example we used in this guide, repositories may also be created, destroyed, read, or edited in any number of ways, and have child resources (issues, wikis, branches, etc.) whose access control is related to that of their parent resource. Further, permissions hierarchies aren’t limited to resources. Users can be grouped into organizations, and further grouped into teams and sub-teams, which complicates the business of controlling access even more.

In our next entry, we'll show how to apply common authorization patterns to these kinds of use cases. We'll use GitClub as an example, but the patterns we cover, including ownership, hierarchical permissions, and nested resources, are common to many user-facing applications.

Next chapter:

Role-Based Access Control (RBAC)
Introducing authorization models – ways to structure authorization code and guide implementation. These models start simply, but can grow with changing requirements. In this section we cover four models of role-based access control and show how to implement each one.

Previous chapter:

Authorization Academy Introduction
Authorization is a critical element of every application, but the problem is: there’s limited concrete material available for developers on how to build authorization into your app. To help developers build these systems and features, we built Authorization Academy.

The best way to learn is to get your hands dirty.