OpenFGA Adoption Patterns
This document outlines key implementation patterns for adopting OpenFGA in your organization.
Starting with coarse-grained access control
When evaluating this solution, many companies start by replicating their existing permissions structure before moving to more granular controls. For example, if you're using Role-Based Access Control (RBAC) in a B2B scenario, you might start with a simple model:
model
schema 1.1
type user
type organization
relations
define admin: [user]
define member: [user]
# .. add additional organization roles
# map permissions to organization roles
define can_add_member: admin
define can_delete_member: admin
define can_view_member: admin or member
define can_add_resource: admin or member
You can define any number of roles for the organization type and then define the permissions based on those roles. You can then check if users have a specific permission at the organization level by calling the Check API on the organization object:
Check(user: "user:anne", relation: "can_add_member", object: "organization:acme")
A better implementation is to define the application's resource types in the model (e.g. documents, projects, insurance policies, bank accounts, etc):
model
schema 1.1
type user
type organization
relations
define admin: [user]
define member: [user]
define can_add_member: admin
define can_delete_member: admin
define can_view_member: admin or member
define can_add_resource: admin or member
type resource
relations
define organization: [organization]
# map resource permissions to organization roles
define can_delete_resource: admin from organization or member from organization
define can_view_resource: admin from organization or member from organization
In this case, you'll need to write tuples that establish the relationship between resource instances and organizations, or use Contextual Tuples to specify them, e.g:
user: organization:acme
relation: organization
object: resource:root
In this case, the Check() call will be at the resource level, for example:
Check(user: "user:anne", relation: "can_view_resource", object: "resource:root")
The main advantage of this approach is that your APIs will be checking permissions at the proper level. If you later want to evolve your authorization model to be more fine grained, you won't need to change your app. For example, you can add fine grained access permissions at the resource level, and your authorization check won't change:
type resource
relations
define organization: [organization]
define owner: [user]
define viewer: [user]
# map resource permissions to organization roles
define can_delete_resource: admin from organization or member from organization or owner
define can_view_resource: admin from organization or member from organization or owner or viewer
Provide request-level data
One of the advantages of the Zanzibar/OpenFGA approach is that all the data you need to make authorization decisions is stored in a centralized database. That greatly simplifies how application implement access control. Applications do not need to retrieve all the required data before invoking an authorization service.
However, writing the data to the centralized store adds implementation complexity. You need to implement a data pipeline that makes sure the data is always up to date.
OpenFGA provides a feature called Contextual Tuples that allows sending the required data as part of each authorization request instead of storing it on the OpenFGA database. Overusing this feature has many drawbacks, as you are now adding additional complexity and latency around collecting the data, and you are not benefiting from using OpenFGA as intended. However, implementing a hybrid approach can make sense in many scenarios and can also be a helpful tool at the start when you are transitioning into a more OpenFGA tailored approach.
When the data is already available to the calling API, sending it as a contextual tuple is very simple. A common use case is you have data in your access tokens (for example, roles/groups claims). Instead of synchronizing groups/roles relations to OpenFGA, you can send those as contextual tuples.
When the data is not already, you will need to retrieve it. This is what you need to do if you are implementing pure Attribute Access Control. You'd retrieve the data and send it to the authorization policy engine. You can do the same with OpenFGA using Contextual Tuples.
You'll need to make the trade-off between writing the data to OpenFGA so it's always available for any authorization request, or requesting it before making an authorization check.
We've seen companies successfully following a hybrid approach, starting by synchronizing the data that's easy first and providing the rest as contextual tuples. As their implementation matures, they implement more synchronization processes and stop sending the contextual tuples.
Use OpenFGA to enrich JWTs
Once you have your authorization model and data set up, you can start making authorization checks from your application. The preferred way is to perform a Check() call.
However, you might have a large set of APIs that are already making authorization checks using JWTs. Changing those applications can be a significant investment. Even if JWTs have several drawbacks compared to making FGA API calls, it can be reasonable to first start by using OpenFGA to generate the claims that are stored in JWTs, while the applications keep using those claims to make authorization decisions.
Over time, you'll migrate the applications and APIs to use authorization check instead.
Authentication services usually provide a way to enrich access tokens during the authorization flow. You can see an example on how to do it with Auth0 here.
For example, if you want to include in the access token the organizations that a user can log-in to, based on the following model:
type user
type organization
relations
define member: [user]
You can call ListObjects(type:"organization", relation:"member", user: "user:xxx")
and include those.
Promoting Organization-Wide Adoption
To introduce OpenFGA in a large company, it's recommended that you identify a problem where the additional enables quickly delivering business value to customers. It can be a new project, a new module, a new feature. Using OpenFGA for such a project can be an easier decision. Once an implementation is successful, you can try influencing the rest of the organization to adopt it.
However, influencing the decision makers of a large organization can be hard. Each team has their own internal roadmaps and not all of the teams will see value in implementing a new authorization system. Migration can be seen as a tech-debt project instead of a business-value-driven one.
The can take advantage of the following capabilities to simplify adoption by multiple teams:
- Modular Models enable each team to independently evolve their authorization policies without relying on a central team.
- Access Control allows you to issue different credentials for each application, with permissions that ensure that each credential can only write data to the types defined in the Modules they own.
Domain-Specific Authorization Server
Some companies decide to wrap OpenFGA with their own authorization service. They decide to do this for multiple reasons:
- Sometimes they already have a centralized service, and it's easy to replace it with another without changing the calling applications.
- It can simplify internal adoption by providing domain-specific APIs. Instead of calling
write
orcheck
, applications can call a/share-document
endpoint or a/can-view-document
one. Each team does not need to learn the OpenFGA API. - If they are using Contextual Tuples, they can keep the logic to retrieve additional data to send to OpenFGA in a single service.
- They only need to provide OpenFGA configuration data like Store ID and Model ID in a single service.
On the other hand, adding another service increases latency, adds additional complexity and would make the teams less likely to find help from existing public OpenFGA documentation and resources.
Shadowing the OpenFGA API
When migrating from an existing authorization system to OpenFGA, it's recommended to first run both systems in parallel, with OpenFGA in "shadow mode". This means that while the existing system continues to make the actual authorization decisions, you also make calls to OpenFGA asynchornously and compare the results.
This approach has several benefits:
- You can validate that your authorization model and relationship tuples are correctly configured before switching to OpenFGA.
- You can measure the performance impact of adding OpenFGA calls to your application.
- You can identify edge cases where the OpenFGA results differ from your existing system.
- You can gradually build confidence in the OpenFGA implementation.
To implement shadow mode:
- Configure your application to make authorization checks against both systems
- Log any discrepancies between the two systems
- Analyze the logs to identify and fix any issues
- Once confident in the results, switch to using OpenFGA as the source of truth. The same approach of shallow checks when migrating between models.
This pattern is particularly useful for critical systems where authorization errors could have significant impact.