A Case Study on Separating IAM from Business Logic with Ory

Flock. Community Blogs
Flock. Community
Published in
10 min readJan 18, 2024

Written by Julius van Dis and Willem Veelenturf

Looking at web applications today, there are many concerns to take into account: business logic to solve domain related problems, technical concerns such as routing, hosting, and non-functional performance, and Identity Access Management (IAM) to ensure users interacting with the application are known, and only allowed to perform the operations they are assigned. However, even though those concerns are of separate nature, they tend to come together at various places of the application, creating a fragmented implementation.

We took a closer look at Flock. community’s Workday app flock-community/flock-eco-workday, an application that is used within our community to register contracts and billable hours for clients and assignments, expense tracking, as well as leave / sick days, built with Spring boot and React. It is set up with Google’s OAuth authentication, and uses Role Based Access Control enforced by Spring Security for authorization. Most entities rely on straightforward CRUD interactions, with the exception of some aggregation endpoints.

Another interesting aspect of the Workday app, and by extension a lot of client applications as well, was the lack of permission auditing. There was no audit log present to indicate who got assigned which roles when. But at the same time, having a role assigned doesn’t allow for an overview of resource access. Resource access is managed in the applications itself. This makes it very difficult to test permissions for the app, or be able to state which users have access to a specific resource.

On the topic of IAM

IAM is an umbrella term for the rules and tools organisations use to manage and secure digital access for their users. Within this domain, we can identify two primary facets: authentication and authorization. Authentication revolves around the verification of a user’s identity, ensuring that they are indeed who they claim to be. On the other hand, authorization pertains to defining the actions a user is permitted to undertake within the organisational context.

Authorization can be distinguished into the following (relevant) categories:

RBAC (Role-Based Access Control): Access is organised based on roles, simplifying administration by grouping users with similar responsibilities.

ABAC (Attribute-Based Access Control): Access decisions are made based on attributes like user characteristics and resource properties, offering flexible, fine-grained control.

ReBAC (Relationship-Based Access Control): Access control considers relationships between entities, making decisions based on organisational structures or other relationship criteria.

Goal

Combining our interest in domain Driven Design, as well as our love for separation of concerns, we wanted to separate our IAM and security related logic as much from our business logic. In the workday app — similar to most of the applications we see in our line of work — authentication is delegated to an external component (OAuth implementations, proxy solutions). In case of RBAC solutions, the authenticated identity comes with a list of roles, or these roles are fetched from the database of the web app itself. These roles are then used for authorization, performed by the app itself, and this is where we experience friction. Throughout the codebase, we noticed that all endpoints are annotated with role validations from Spring Security, which mixes the domain logic concern with security concerns. Furthermore, we saw that in multiple cases, the entity first had to be fetched from the database before the application was able to determine whether or not the caller had access to it, e.g. with an EXPENSE.READ role, users can only read their own expenses, not those of others.

Wouldn’t it be nice to extract this authorization logic from the web app itself, and similar to authentication, off load it to a proxy? We wanted to investigate different approaches for authentication and authorization. We had a couple of requirements:

  • The solution should be capable to handle authentication and authorization
  • Business logic, authentication logic and authorization logic should be separated
  • A composable solution which could be tailored to our needs: support CRUD and aggregation endpoints, allowing for implicit authorizations based on hierarchical relations between users (engineers, managers, workspace owners)

Approach

To make this project suitable for a Flock. hack project we sliced it into small steps. We could finish each step during the two hack-days and made the migrations manageable.

We decided to go with the Ory platform. Ory is an open source solution backed by a commercial and cloud hosted offering. Ory exists of three products which can be used independent from each other to compose a tailored authentication and authorization solution:

  • Kratos: authentication and access management, allowing for extensive account management.
  • Oathkeeper: identity and access proxy, manages request authentication and authorization
  • Keto: permissions and relation management for authorization, using a ReBAC model.

Using ReBAC for authorization was our primary choice, as the model focuses exclusively on the relationships, or how resources and identities (aka users) are connected to each other and between themselves. This allows to explicitly create relations between workdays and their respective owners, but also model manager relations.

Another contender we found for ReBAC authorization was OpenFGA, which conceptually brings the same abilities compared to Keto, but is seemingly further developed. To keep focussed we decided to stick with Keto. Our experiences / finding for openFGA can be found in this other blog post.

Step 0 — orchestration with Oathkeeper

To prepare the workday app to take away authentication and authorization logic, we started with placing Ory’s IAM proxy — Oathkeeper between the user and the workday app. This made sure that all traffic aimed towards workday would go through it, allowing us to gradually start adding functionality to it. We created a setup for Oathkeeper in our Google Cloud environment (GCP), ensured workday would only be able to receive internal traffic, and opened Oathkeeper for external traffic.

Step 1 — authentication with Kratos

Step one was to replace the authentication layer of the workday app with Kratos. We created a setup for Kratos in our Google Cloud environment (GCP). We used Google Cloud run and a managed PostgreSQL database. Applying best practices from Ory’s documentation, we configured the flows for registration and login. With surprisingly little effort we managed to connect our social login as well as passwordless login.

Next step was to replace the existing spring security session based authentication layer and use the one from Kratos. We set up the authentication configuration of Oathkeeper to use Kratos’s session based authentication. We used a mutator to append a JWT token to the proxied request towards workday and turned the application into a resource server. We kept the existing authorization layer and relied on role based access of spring security, which was still stored in workday’s database.

ref

Step 2 — authorization with Keto

With authentication out of the way, it’s time to move on to authorization. With the Ory stack comes Keto, a permission system based on Google’s Zanzibar model. It comes with two essential components: an API to create relationships between entities, and a permission model, which maps relationships and a user context to access. Ory offers Keto as the solution in their ecosystem to solve this challenge. By defining all the relationships between all the objects and subjects in a graph, access rules can be resolved by graph traversal and finding relationships between objects and subjects.

The permission model

According to the documentation, with Ory’s keto you can:

Unify authorization logic in one service that’s the single source of truth for access rights across all of your applications.

Inherently, Zanzibar, and by extension Keto, focusses on Relation Based Access Control (ReBAC). This allows us to make explicit in our authorization system the relation between specific entities and the user trying to access it. When looking at the workday, we have the following relation model:

Conceptually, workday distinguishes between a User (the one logged in, looking around in the application), and a Person (a human to which assignments, workdays and leave days are bound). This model can be written as 4 different relations:

  • User:z is an owner of Person:x
  • Person:x is an owner of Workday:y
  • Person:x is an owner of Leave day:b
  • Person:a is a manager of Person:x

And, to make the permission model complete, there are implicit permissions related to the relations:

  • An owner of a workday can retrieve and make changes to a workday or leave day
  • An owner of a person inherits the rights of that person

For example: if we have the following situation:

This implies that Bob’s workdays can be seen and changed by User Bob, as he is the owner of Person Bob, which in turn owns the two Workdays. User JohnX is also able to see and change the workdays, as Person John is the manager of Person Bob. Conversely, if there would be Workdays owned by Person John, these would only be visible to User JohnX, and not User Bob.

Integrate authorization in workday

To integrate this with Oathkeeper and the workday app, we can create an access rules specifically for workdays to:

  • Authenticate the request with Kratos like we do for all requests
  • Authorize the request with Keto using an API call including the authenticated user’s id from the authentication response, the workday id from the url, to validate the rule `Is user:<user-id> allowed to view Workday:<workday-id>, add a JWT token with the users identity like we do for all requests
- id: workday-view
upstream:
# to allow to use 'local' app rather than docker one
url: http://host.docker.internal:8080
version: v0.40.2
match:
url: http://workday.flock.local:8081/api/workdays/<.*>
methods:
- GET
authenticators:
- handler: cookie_session
authorizer:
handler: remote_json
config:
payload: |
{
"namespace": "Workday",
"object": "{{ printIndex .MatchContext.RegexpCaptureGroups 2 }}",
"relation": "view",
"subject_set" : {
"namespace": "User",
"object": "{{print .Subject}}",
"relation": ""
}
}
mutators:
- handler: header
- handler: id_token
errors:
- handler: redirect

In order to start using Keto as authorisation manager we needed to synchronise all objects to the graph of Keto. Initially we could do this by creating a simple script to sync the database once with the graph. Besides that for every operation we needed a real time sync between the object in the database and the graph. Since we made it the challenge to strictly separate business logic with authorization logic we didn’t want to add this to the application. We decided to create an application filter which intercepts every request and response to update the graph of Keto.

Challenges arise …

While this approach readily solves for the basic CRUD operations, the use case for list endpoints and aggregations makes things a bit more complex. When requesting a list or an endpoint that returns aggregated results for a collection of objects there is no identifier in the access path. For example, ‘GET /api/leaveday-hours-per-month’ returns an overview of the holidays for the persons the subject has a manager relationship with. For this endpoint to work, the workday app needs to know which persons are managed by the caller. For a list call, such as `GET /api/workdays`, the workdays owned by that person are returned. However, the request itself does not contain a person identifier,

To solve this problem we came up with three different solutions. Unfortunately, all have different drawbacks and do not scale when the amount of relationships increase.

Solution 1:

Oathkeeper has the concept of mutators. This can be used to extend the request with additional data. For example extra headers can be added. Keto’s list feature can be used to query all the persons the subject has a manager relationship with to fill the additional header and used by the application to query for the holiday overview. Limitations of this solution are the max size of the request headers, which eventually will become a problem.

Solution 2:

The second solution is that the application will get the list of persons that have a manager relationship directly from Keto and will add this to the query to the database. Downside of this solution is that business logic and authorization logic are mixed and there are still limits in the amount of persons that can be sent to the database with a query.

Solution 3:

The relationships between users remain in the application database. The application itself derives the manages/is managed by relations. With a single query the total amount of holidays per month or workdays for a set of subjects. No limitations in terms of performance. However, application data and authorization data are mixed in the same database and business logic and authorization logic are mixed.

Conclusion

Ory’s Oathkeeper is great for separating authentication and authorization logic from your business logic in your application. It offers composable integrations for authentication and authorization and allows you to offload IAM logic from the business logic layer that proxy. With ReBAC for authorization, all requests by id can be properly authorized before ever reaching the business logic layer.

However, challenges arise when dealing with multiple resources in a single request, be it collections or aggregations. When the relationships filter or aggregate your data they need to be in the same context. There are multiple solutions to this problem all hand into the separation of authentication and authorization logic. There is no silver bullet to the problem as always depends on the domain model and data cardinality.

--

--

Flock. Community Blogs
Flock. Community

Flock.community where everyone has the drive to keep developing themselves. Where we get a little better every day.