Where should I enforce my authorization policy?

Jul 29th, 2024

Omri Gazitt avatar

Omri Gazitt

OAuth2  |  

Zero trust  |  

Authorization  |  

Security

enforcement points

Introduction

My wife owns a small business, and recently decided to hire a virtual admin. Just before giving the VA various login credentials, she received a strange DM from the VA, asking to borrow $100.

My wife got spooked, and not surprisingly, found out the next day that the VA’s Facebook account got hacked. What can we learn from this story?

  1. Password discipline is important. Don’t hire a VA unless they can demonstrate this!
  2. Account sharing is bad. With B2C SaaS, sometimes you can’t avoid it. Fortunately, with B2B SaaS, each user in an organization has a discrete account with various levels of permissions, so you shouldn’t ever need to do this!
  3. Even with (or perhaps, especially with) B2B SaaS products, identity-related breaches will still happen. Therefore, security principles like zero-trust and defense-in-depth are critical for limiting the blast radius of a compromised identity.

The principle of least privilege

This is where the most important zero-trust principle comes in. Every identity should have the smallest possible permission surface area, and applications should enforce those permissions right before granting or denying access to a protected resource.

Unfortunately, most web applications still struggle with this. OWASP lists Broken Access Control as the #1 web application security issue they track. As engineers, what can we do about it?

Channeling Christopher Walken's SNL skit - we need “more authorization cowbell!”

christopher walken - more cowbell

Christopher Walken - more cowbell!

Defense in depth

Where should your web application or API enforce its authorization policy? Given the growing risks, more and more organizations answer “everywhere we can”.

There are four common enforcement points in the journey of an application request. We’ll go over each one in detail. But first, let’s do a level-set on Authentication (via OpenID Connect) and Authorization (via the PDP architecture).

Authentication: OAuth 2.0 and OpenID Connect

Authentication is the process of having the user prove that they are who they say they are - with a password, passkey, biometric, or multiple factors. This allows them to log in to the app, but outside of B2C scenarios where just logging in means you’re the “superuser” of your account, authentication is just the first stop in the authorization journey.

The ubiquitous authentication protocol these days is OpenID Connect (OIDC). OIDC defines an interaction pattern between a client (browser), a resource owner (user), an authorization server (AS), and a resource server (RS) that serves a protected resource.

oidc flow

In OIDC, the “authorization server” (AS) moniker is somewhat confusing, since all the AS does in the OIDC flow is mint an access token based on the user’s credentials. In other words, all it really does is authentication!

With that said, during authentication, it’s common to have the AS add a set of claims, or scopes, into the access token that it mints on behalf of the user. These claims or scopes represent group memberships, or coarse-grained capabilities that the resource server (RS) can interpret as permissions. So yes, the OIDC authentication ceremony can include a form of authorization after all.

The benefit of this approach is that a resource server can check whether the access token has a certain permission before granting or denying certain kinds of operations, without having to check back with any other system. In other words, the bearer token minted by the IDP carries a set of permissions with it, which are “guaranteed” to be good before the token expires.

But how does the AS know what scopes / claims to add to the access token? This is where an externalized authorization service can come in handy.

Authorization: Policy decision points (PDPs)

So far we’ve stuck with OAuth 2.0 and OIDC terminology, but the authorization world has a different set of terms to describe the actors involved in determining whether a user has a permission on a resource.

OAuth 2.0 (which is the protocol foundation of OIDC) already uses the term “Authorization Server” to describe the service that mints an access token (the result of a successful authentication). To eliminate any ambiguity, we’ll use the term “policy decision point” (PDP) to describe a service that can answer the question “does user U have permission P on resource R?”

The PDP typically operates downstream from the authentication ceremony, and is invoked by a policy enforcement point (PEP). Translating to OAuth terminology, the PEP is typically the resource server (RS).

p*p architecture

Once a user is logged in, the RS (acting as a PEP) can ask a PDP whether the user represented by the access token has a particular permission on a specific resource before granting or denying access to that resource.

What’s the benefit of externalizing authorization logic out of the RS and into the PDP? It’s basically the same benefit as externalizing authentication to an IDP. Your app can rely on a purpose-built architectural component to perform authorization consistently. You can also do it in a standards-compliant manner - just like the OpenID Foundation defined the OpenID Connect standard for authentication, it is now working on the AuthZEN standard for authorization.

PDPs can also search

In addition to answering the permission question, many PDPs can also answer questions like “what resources of a certain type does a user have access to?” A common example is to enumerate the groups that a user is a member of. More on this soon…

PDPs can be invoked from multiple enforcement points

One of the most powerful aspects of externalizing authorization to a PDP is that you can use the same PDP with any number of policy enforcement points (PEPs).

Now that we have all the terminology defined, we can get specific about what those enforcement points are.

#1: Enforcement during the authentication ceremony

The first enforcement point to consider is the authorization server (AS) from the OIDC flow. For example, the AS can make a call to a PDP to determine which groups the user belongs to, and add those groups as claims in the access token that it mints on behalf of the user.

So far, so good. We can enrich an access token with additional information, such as roles and group membership, that helps the RS determine whether a user can perform an operation.

But what about enforcing access to specific resources? When you log in to Google Drive, the Google AS doesn’t figure out which documents you have access to, and stick those in the access token. This is because of three reasons:

  • You could have access to many documents. Figuring out which may take a long time!
  • Access tokens have size limits. Even if you could find out which documents you have access to in a reasonable amount of time, chances are you can’t express all of them as scopes in an access token.
  • As soon as you minted the access token, it would contain stale information, since fine-grained permissions in Google Drive can change rapidly, certainly faster than the typical lifetime of an access token.

In other words, the collaboration model for Google Drive is not a good fit for the authorization model supported by scopes or claims embedded in an access token.

Fine-grained authorization

When the granularity of an authorization decision extends down to individual resources, we say that it is a fine-grained access control decision. But our PDP is purpose-built to make these types of decisions. After all, its bread and butter task is to answer the question “does user U have permission P on resource R?”

The most obvious place to perform fine-grained authorization is the resource server (RS) itself, which in plain english means the back-end service / API that performs actions on resources.

#2: Enforcement by the resource server

So, the second enforcement point in a defense-in-depth strategy is the resource server.

As a reminder, the PDP needs three pieces of information:

  • The user context (“who is operating on the resource?”)
  • The resource (“what resource is being operated on?”)
  • The permission (“how is the resource being operated on?”)

The benefit of making fine-grained enforcement decisions by the resource server is that the RS can package up all the information necessary to make that decision and send it to the PDP.

  • The user can be extracted out of the access token (e.g. the subject claim in a JWT)
  • The resource, and any information about it (e.g. its owner, or access control list), can be retrieved by the service before calling the PDP
  • The permission is usually determined by the action that the API is trying to perform (e.g. create, read, update, or delete the resource).

This allows the PDP to remain stateless. It relies on the RS to package up all this context so that the PDP can simply run its authorization policy over the incoming data, and make an access decision.

However, there are drawbacks to this approach. We’re placing considerable responsibility on the RS to “get it right”. It needs to gather all the information required by the authorization policy, often needing to query its back-end database. The value of externalizing the authorization decision to another component is diminished relative to just writing some code inside the service.

Externalizing authorization state

So far, we’ve externalized authorization logic (the “authorization policy”) to the PDP. What if we could also externalize the authorization state?

This is exactly what stateful PDPs do. By telling the PDP about your resources and their relationships to users and groups (e.g. “resource R is owned by user U”, “resource R can be viewed by user X”), you can completely abstract the authorization responsibility out of the resource server.

This works great when your enforcement point is the RS, but places a different burden on your application: it must inform the stateful PDP when a relationship between a user and a resource gets added or removed.

This is exactly how Google Drive works. Google Drive (and many other Google applications) use a stateful authorization system internally codenamed Zanzibar as their PDP. When a user shares a document or folder in Google Drive, that application informs Zanzibar of this new relationship. This is why Zanzibar is known as a “relationship-based access control” (or ReBAC) system. It performs access checks by walking the graph of relationships between a resource and a user, allowing access only if there’s a path that carries the permission in question.

#3: Enforcement by the API gateway

Once we’ve made the architectural leap to manage relationships in a stateful PDP, this opens up new possibilities for fine-grained access control. In a microservices architecture, each of them is a resource server. Instead of having each RS have to call the PDP with the right user, permission, and resource, you could do this from the API gateway before you even dispatch the request to the RS.

This technique works well if each API request contains all the information to identify the user, permission, and resource. Typically the user is found in the JWT bearer token in the Authorization header. And for REST requests, the permission can be inferred from the HTTP method and endpoint.

That leaves the resource. Often, the resource type and ID can be deduced from a combination of the path/endpoint, path parameters, query parameters, or the request body.

But API gateways are meant to be lightweight. The more processing an API gateway needs to do in order to find these pieces of data, the less attractive gateway-enforced authorization becomes. If the gateway needs to contact another system to perform some kind of mapping between an ID found in a query parameter and a resource ID that the stateful PDP knows about, you’ve definitely crossed into territory where you should rethink the gateway as a viable enforcement point.

Medium-grained authorization

So the API gateway may not be the best place to perform fine-grained (resource-level) authorization, unless the request contains enough information to identify the resource in question, and you’re employing a stateful PDP - e.g. one that is modeled after Google’s Zanzibar.

But the API gateway can still be quite useful in performing another defense-in-depth task - making sure that the user has the permission to invoke a particular API endpoint.

This is known as medium-grained authorization, because the resource in question isn’t the fine-grained resource (e.g. the document in the Google Drive example). Rather, it is the API endpoint itself.

This type of authorization can catch many access issues before dispatching a request to the RS, which can perform fine-grained authorization on the resource itself. Hence, defense in depth.

Once again, this pattern can be accomplished using the same type of stateful PDP that we’re already familiar with. The gateway logic becomes incredibly simple - “can the user identified by this JWT invoke the resource of type ‘endpoint’ identified by the signature of the endpoint that is being invoked?”

For a deeper dive into this scenario, known as API Authorization, check out this article.

#4: Service-to-service enforcement

Last but not least, in a microservices architecture, the subject (or “principal”) that you’re authorizing is often the end-user that invoked the request. But as your architecture gets more complex, there are often back-end services that don’t really care about the identity of the user.

For example, an email notification service can be invoked by a commerce service to notify a user that an order has been placed. In this example, the end-user’s identity isn’t relevant. What’s more interesting is ensuring that only certain services are allowed to invoke the commerce service.

In this scenario, the identity of the caller is a machine identity or “workload identity”. The authorization question becomes “can service A invoke endpoint B on service C?”

How do we implement this kind of authorization? You guessed it, using a stateful PDP!

Much like the API gateway-enforced authorization scenario, this is medium-grained authorization. The user context is a workload identity (which can also be expressed using a JWT), the permission to evaluate is “can invoke”, the resource type is an “endpoint”, and the resource ID is the specific API signature of the endpoint being invoked.

This scenario is tailor-made for enforcement by an API gateway, but can also be done by the resource server if the back-end services don’t use a gateway layer.

Summary

The answer to “where should I enforce my authorization policy?” is “more cowbell!”  In other words, you should employ a defense-in-depth strategy and perform authorization checks in a few places:

  • Authentication ceremony (a.k.a. OIDC Authorization server) - to ensure the user has the right basic roles
  • Resource server - to perform fine-grained access control
  • API gateway - for medium-grained access control to API endpoints and for service-to-service interactions

The good news is that a stateful PDP can be used equally effectively from all these enforcement points.

Topaz is such a stateful PDP. It supports the relationship-based access control (ReBAC) model described in Google’s Zanzibar paper, and is compliant with the AuthZEN specification.

To learn more about Topaz, you can brew install it, fork the OSS project, or join the community slack!

Omri Gazitt avatar

Omri Gazitt

CEO, Aserto