Starting from here?
If you want to start this tutorial from the beginning, start from this blog post.
Otherwise, to continue from where we left off, clone the application repo and check out the
add-abac
branch:git clone -b add-abac https://github.com/auth0-blog/rails-api-authorization
Make the project folder your current directory:
cd rails-api-authorization
Then, install the project dependencies:
bundle install
Then, setup the database
rails db:setup
Finally, create a
.env
hidden file:touch .env
Populate
.env
using the .env.example
file:cp .env.example .env
Add your Auth0 configuration variables to the
.env
file and run the project by executing the following command:rails s
Previously, it was on your access control series (read with TV show presenter voice). You learned about Attribute-Based Access Control and integrating it into your Rails API. In this blog post, you’ll continue learning about different authorization systems, this time about Relationship-Based Access Control and how to iterate from ABAC to ReBAC. You’ll also learn about FGA and how it fits into the picture.
What Is Relationship-Based Access Control (ReBAC)?
Relationship-Based Access Control (ReBAC) is an authorization model in which a subject’s access to a certain resource is defined by the relationships between the subject and the resource. Think, for example, about any social network. Usually, users can connect to each other by following each other or sending friend requests, and you can limit what each user can see based on these relationships.
Note that you can use ReBAC along with RBAC and ABAC. Roles and attributes can coexist with relationships in this case; in fact, roles or attributes can be relations 🤯, but it’s up to your business case to define whether it makes sense to have them all or simply keep one of the other.
ReBAC and Fine-Grained Access Control
Fine-grained access Control or Fine-Grained Authorization refers to the ability to grant specific users permission to perform certain actions on specific resources. These types of authorization systems allow you to scale to millions of objects and users, where permissions can change very rapidly.
Both ReBAC and ABAC can be fine-grained but ReBAC allows you to make decisions on a database that has up-to-date data. ABAC can do this too but in most traditional implementations there's no database. This helps in scenarios where the data that influences the decision changes often.
A Wild OpenFGA Appears!
There are many ways you can implement a Fine-Grained Authorization system; OpenFGA is one of those. It’s open source, and it’s inspired by Google’s Zanzibar, Google’s internal authorization system. In a nutshell, the OpenFGA service answers authorization checks by determining whether a relationship exists between an object and a user.
OpenFGA relies on ReBAC. Not only can you define relationships between subjects and objects, but it also facilitates the implementation of RBAC and even ABAC!
ReBAC Implementation with OpenFGA in Your Rails API
Once the concepts are clear, let’s go back to the last implementation of our expense management system. The last thing you heard from stakeholders was:
Users who are managers can see the reports of their directs, plus the reports of their directs’ directs, and so on 😵💫
At the moment, the way to implement something like this would be to add a new attribute to the user’s table
manager_id
and recursively check for managers of managers. This is not impossible, but since you’ve learned about OpenFGA, you decide it’s worth a try since they happen to have a similar case in their examples of an expenses management app. What You Have Implemented So Far
- An
role that can see everything.admin
- A submitter user who can only see their reports or where their ID matches the attribute
of the report.submitter_id
- An approver user who can only see the reports where their ID matches the attribute
of the report.approver_id
What You’re Changing
Since you’re going to introduce a new requirement, you make the following decisions 🤠:
- Rely on OpenFGA to dictate access rights, meaning every time you need to check for access, you’ll use OpenFGA and not the local relationships in your database; these can be used for querying or other tasks.
- Remove the
role. If you remember from the initial blog post, this role was created to allow users to approve expenses, but having more granular control allows you to get rid of the role and rely on relationships.admin
- You are going to keep the attributes
andsubmitter_id
for local checks and querying. If you see it’s necessary, you can implement a task that assures that the relations you have in your database match the ones that live in OpenFGA (this is out of the scope of this blog post).approver_id
- You are going to add a
to themanager_id
table to represent the manager relation so your app knows about this relationusers
Sounds like a lot of work, right? You might have to make some changes in the app, but you’ll end up with a better and more robust authorization system.
Defining Relations
You’ve already mentioned some of the relationships you’ll have, but let’s summarize them. First, you will have two types:
user
and report
. Remember, OpenFGA works with users and objects in your case; users will be users (duh), and objects will be reports. You can define an authorization model as a YAML file, such as: model schema 1.1 type user type report
...which is incomplete, so you need to define relations for each type. For example, you know that a user can be a manager of another user, so the relation
manager
is a good candidate. Let’s not forget about the recursiveness we discussed earlier. Luckily for us, it’s easier to define in OpenFGA, and you’ll do it with an implied relationship that you’ll call can_manage
. This relationship implies that a user can manage another user when they are their manager, or they can manage the manager (and so on recursively). Let’s write it down:model schema 1.1 type user relations define can_manage: manager or can_manage from manager define manager: [user] type report
Now, for the report, you’re interested in knowing who the
submitter
and the approver
are, so these are the two relations you’ll create. The submitter
relation is straightforward, but the approver
relation is also implied because if a user is a manager of the submitter, then they can approve it. So let’s model that:model schema 1.1 type user relations define can_manage: manager or can_manage from manager define manager: [user] type report relations define approver: can_manage from submitter define submitter: [user]
Great! So what you just did here was to define your authorization model, which, as you can see, is where you define the relationships between your users and objects to later on make checks and determine access rights.
Next, let's see what to do with this YAML file and where to put it to have a proper authorization model integrated with OpenFGA.
Integrate OpenFGA into a Rails API
The first thing you need to do is to integrate OpenFGA into your Ruby on Rails API. The fastest way to start testing OpenFGA and play around is using Docker. After pulling the Docker image, you can run it with the following:
docker run -p 8080:8080 -p 8081:8081 -p 3000:3000 openfga/openfga run
The playground will run in port
3000
and the API is available in port 8080
and voila! You have your own OpenFGA server running on your local machine. Now you can copy the authorization model you created in the previous section; you can paste it into the playground's editor to see it there. You can also transform it to JSON format using FGA CLI like so:
fga model transform --file openfga/authorization-model.fga --output-format json > openfga/authorization-model.json
The next thing you need to do is create a store and an authorization model. In OpenFGA, a store is an entity used to organize authorization check data, and each store contains one or more versions of an authorization model, that’s the JSON file you copied from the playground earlier, and where all your relations and types are defined.
First of all, let’s store that JSON file somewhere. Create a new file
config/openfga_authorization_model.json
and paste the content of that JSON file. This way you have your authorization model ready to go when it’s time to create it in your OpenFGA server. But how do you interact with your OpenFGA server? Well, you’ll need to interact with the OpenFGA API, which you’ll do next.
Implementing an OpenFGAService Class
In order to keep this modular, let’s create a service class to interact with the OpenFGA API. This service class will implement calls for the following endpoints:
- Create a store
- Create an authorization model
- Update a relation
- Perform a check
- List objects
Let’s create a new class. Create a new folder,
app/services
, and a new file in it, app/services/openfga_service.rb
, and add the following content: require ‘net/http’ require ‘uri’ require ‘json’ class OpenfgaService def self.make_post_request(path, body:) uri = URI.parse("#{ENV['FGA_API_URL']}/#{path}") request = Net::HTTP::Post.new(uri) request.content_type = "application/json" request.body = body Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http| http.request(request) end end def self.create_store response = make_post_request("stores", body: { name: "expenses" }.to_json) JSON.parse(response.body)[“id”] if response.code.to_i == 201 end def self.create_authorization_model(store_id) response = make_post_request("stores/#{store_id}/authorization-models", body: File.read(Rails.root.join('config', 'openfga_authorization_model.json'))) JSON.parse(response.body)[“authorization_model_id”] if response.code.to_i == 201 end def self.update_relation(user, relation, object) return unless authorization_data store_id, authorization_model_id = authorization_data body = { writes: { tuple_keys: [ { user: user, relation: relation, object: object } ] }, authorization_model_id: authorization_model_id }.to_json response = make_post_request("stores/#{store_id}/write", body: body) response.body end def self.check(user, relation, object) return false unless authorization_data store_id, authorization_model_id = authorization_data body = { authorization_model_id: authorization_model_id, tuple_key: { user: user, relation: relation, object: object } }.to_json response = make_post_request("stores/#{store_id}/check", body: body) response.code.to_i == 200 ? JSON.parse(response.body)["allowed"] : false end def self.list_objects(user, relation) return unless authorization_data store_id, authorization_model_id = authorization_data body = { authorization_model_id: authorization_model_id, type: “report”, relation: relation, user: "user:#{user.id}", context: {}, consistency: “MINIMIZE_LATENCY” }.to_json response = make_post_request("stores/#{store_id}/list-objects", body: body) response.code.to_i == 200 ? JSON.parse(response.body)["objects"].map{|obj| obj.split(":")[1].to_i} : [] end # atuhorized if at least one of the relations is allowed: true def self.batch_check_relations(user, relations, object) return unless authorization_data store_id, authorization_model_id = authorization_data checks = relations.map do |relation| { "tuple_key": { “user”: user, “relation”: relation, “object”: object, }, "correlation_id": SecureRandom.uuid } end response = make_post_request("stores/#{store_id}/batch-check", body: {authorization_model_id: "#{authorization_model_id}", checks: checks}.to_json) # # Response: # { # “results”: { # { "886224f6-04ae-4b13-bd8e-559c7d3754e1": { "allowed": false }}, # submmiter # { "da452239-a4e0-4791-b5d1-fb3d451ac078": { "allowed": true }}, # approver # } # } # in our case if at least one of those is true, then the user can view the report. This is VERY specific for this # use case! response.code.to_i == 200 ? JSON.parse(response.body)[“result”].values.map{|e| e.values}.flatten.any? : false end private def self.authorization_data authorization = Authorization.first [authorization&.store_id, authorization&.model_id] if authorization end end
This class uses
to make requests to the OpenFGA API. It’s basically a Ruby wrapper for making calls to the OpenFGA API. Note that you need to add your Net::HTTP
FGA_API_URL
to your .env
file. If using localhost, then the value is http://localhost:8080
. The
create_authorization_model
function uses your JSON file to create the authorization model. One thing in particular for this application is how to store the information about your OpenFGA Store and Authorization Model. The function
authorization_data
retrieves information about the authorization store and model. You’ve decided to store it locally using a table authorizations
. To do that, generate a new model and migration using the following command:rails generate model Authorization store_id:string model_id:string
This will generate a model
app/models/authorization.rb
, and you’re going to add some validations to make sure things stay consistent: class Authorization < ApplicationRecord validates :store_id, presence: true, uniqueness: true validates :model_id, presence: true, uniqueness: true end
And a migration
db/migrate/YYYYMMDDHHMMSS_create_authorizations.rb
:class CreateAuthorizations < ActiveRecord::Migration[7.0] def change create_table :authorizations do |t| t.string :store_id t.string :model_id t.timestamps end end end
You’re probably wondering when you stored that authorization data, right? Well, to answer that, I’m going to use the good old: "It depends."
You need to create a store and authorization model only once. It’s up to you and your business to decide how and when to do this. In your case, you’ve decided to use a custom rake task.
When you deploy your app and run your server for the first time, for example, you can run this task manually, and it will create the store and authorization model. Another way to do it could be using a Rails Initializer to run after the server runs.
You went with the rake task option, so you create this lovely task file that not only creates a store and an authorization model but also updates relations and performs checks because you’re using it for testing it locally:
namespace :openfga do desc “Create a store and authorization model” task create_store_and_model: :environment do store_id = OpenfgaService.create_store if store_id authorization_model_id = OpenfgaService.create_authorization_model(store_id) Authorization.create!(store_id: store_id, model_id: authorization_model_id) puts "Store ID: #{store_id}, Authorization Model ID: #{authorization_model_id}" else puts “Failed to create store” end end desc “Update relation” task :update_relation, [:user, :relation, :object] => :environment do |_t, args| result = OpenfgaService.update_relation("user:#{args[:user]}", args[:relation], "report:#{args[:object]}") puts “Update relation result: #{result}” end desc “Check authorization” task check_authorization: [:user, :relation, :object] => :environment do |_t, args| user = "user:#{args[:user]}" relation = "#{args[:relation]}" object = "report:#{args[:object]}" allowed = OpenfgaService.check(user, relation, object) puts “Authorization check: #{allowed}” end end
To create a store and model, you can run the following command in your terminal:
rails openfga:create_store_and_model
. Now, back to where you were. With the service class, you’ve encapsulated all interactions with the OpenFGA API, so let’s actually make calls to it!
Add the manager relation to your Rails API
At this point, you’ve defined a manager relation for OpenFGA but also want to keep a record in your local database. For that, let’s create a new association
manager
in app/models/user.rb
: class User < ApplicationRecord has_many :expenses, foreign_key: :submitter_id has_many :submitted_reports, class_name: 'Report', foreign_key: 'submitter_id' has_many :reports_to_review, class_name: 'Report', foreign_key: 'approver_id' has_one :manager, class_name: 'User', foreign_key: 'manager_id' # 👈 new code validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, presence: true, uniqueness: true validates :auth0_id, presence: true, uniqueness: true
Now, let’s create a migration to add the
manager_id
to the users
table. In your terminal, run the following command: rails generate migration AddManagerIdToUsers
This will generate a new file under
db/migrate/YYMMDDHHMMSS_add_manager_id_to_users.rb
, and you’ll need to add the following code:class AddManagerIdToUsers < ActiveRecord::Migration[7.0] def change add_column :users, :manager_id, :integer # 👈 new code end
Don’t forget to run
rails db:migrate
in your terminal right after!Managing Relations
You need to create a new relation in your Rails API at two points: when a user is created or updated and when a user submits an expense report.
Let’s start when a user is created or updated; this means that you can either pass the
manager_id
at creation time or update time. To implement this, you’ll need to make the following changes in your app/controllers/users_controller.rb
: class UsersController < ApplicationController before_action :authorize before_action :set_user, only: %i[ show update destroy ] # GET /users def index @users = User.all render json: @users end # GET /users/1 def show render json: @user end # POST /users def create @user = User.new(user_params) if @user.save # 👇 new code update_authorization_manager(user_params[:manager_id], "manager", @user) if user_params[:manager_id] render json: @user, status: :created, location: @user else render json: @user.errors, status: :unprocessable_entity end end # PATCH/PUT /users/1 def update if @user.update(user_params) # 👇 new code update_authorization_manager(user_params[:manager_id], "manager", @user) if user_params[:manager_id] render json: @user else render json: @user.errors, status: :unprocessable_entity end end # DELETE /users/1 def destroy @user.destroy end private # Use callbacks to share common setup or constraints between actions. def set_user @user = User.find(params[:id]) end # Only allow a list of trusted parameters through. def user_params params.require(:user).permit(:email, :auth0_id, :manager_id) # 👈 new code end end
At this point, you’re allowing a new field,
manager_id
, to be sent in the controller’s params
and then using it to create the relation using a method called update_authorization_manager
...which you haven’t defined yet, but let’s do it. Similarly to how you implemented the
concern when using roles, it’ll be nice to have all the OpenFGA things in one place as well and available for all controllers, so let’s create a new concern in Secured
app/controllers/concerns/authorized.rb
:# frozen_string_literal: true module Authorized extend ActiveSupport::Concern def authorized?(user, relations, object) if relations.size == 1 OpenfgaService.check("user:#{user.id}", relation, "report:#{object.id}") else OpenfgaService.batch_check_relations("user:#{user.id}", relations,"report:#{object.id}") end end def reports(user, relation) return [] unless %w[submitter approver].include?(relation) OpenfgaService.list_objects(user, relation) end def update_authorization_manager(manager_id, relation, object) OpenfgaService.update_relation("user:#{manager_id}", relation, "user:#{object.id}") end def update_authorization_submitter(submitter_id, object) OpenfgaService.update_relation("user:#{submitter_id}", "submitter", "report:#{object.id}") end end
The
Authorized
concern is a proxy between your controllers and the OpenfgaService
class. Make sure to also add it in your ApplicationController
under app/controllers/application_controller.rb
like so:class ApplicationController < ActionController::API include Secured include Authorized # 👈 new code ADMIN='admin’ end
Similarly, let’s create the submitter relation for when a user submits an expense report. This happens when a new expense is created under
app/controllers/expenses_controller.rb
: class ExpensesController < ApplicationController before_action :authorize before_action :set_user before_action :set_expense, only: %i[ show update destroy ] # GET users/:user_id/expenses def index @expenses = Expense.where(submitter_id: @user.id) render json: @expenses end # GET users/:user_id/expenses/1 def show render json: @expense end # POST users/:user_id/expenses def create ActiveRecord::Base.transaction do @expense = Expense.new(expense_params) @expense.submitter_id = @user.id if @expense.save report = Report.create(expense: @expense, submitter_id: @expense.submitter_id) # 👇 new code update_authorization_submitter(@user.id, report) if report.persisted? render json: @expense, status: :created else render json: @expense.errors, status: :unprocessable_entity raise ActiveRecord::Rollback # Rollback the transaction if saving the expense fails end end end # PATCH/PUT users/:user_id/expenses/1 # ... end
At this point, if you create new users and expense reports and check your OpenFGA playground, you’ll see them in the Tuples section at the bottom left!
Note that you should also make sure that when a user or report is deleted, you update the relation in OpenFGA. For that, you use the same endpoint but pass a
object instead of adeletes
object in the request body. This is out of the scope of this blog post.writes
Perform Checks for Authorization
There are three primary endpoints where you need to check for authorization, and they all live in the
ReportsController
in app/controllers/reports_controller.rb
:
— see reports that the user hasn’t approved yetGET users/:user_id/reports/review
— approve a report of a user’s directsPUT users/:user_id/reports/:id/approve
— see user’s submitted reportsGET users/:user_id/reports/submitted
Open the
app/controllers/reports_controller.rb
file and add the following code:class ReportsController < ApplicationController before_action :set_report, only: %i[ show approve ] before_action :set_user before_action :authorize # GET users/:user_id/reports/submitted def submitted # You no longer need to make these checks but only check on OpenFGA # ➖ validate_ownership(@user) do # user is the owner of these reports # ➖ @reports = Report.where(submitter_id: @user.id) # list only reports where user is submitter # 👇 new code @reports = Report.find(reports(@user, "submitter")) render json: @reports if @reports end # GET users/:user_id/reports/review def review # You no longer need to make these checks but only check on OpenFGA # ➖ validate_ownership(@user) do # user is the owner of these reports # ➖ @reports = Report.where(approver_id: @user.id) # list only reports where user is approver # 👇 new code @reports = Report.find(reports(@user, "approver")).select{|r| r.status != "approved"} render json: @reports if @reports end # PUT users/:user_id/reports/1/approve def approve # validate_roles [ADMIN] do # if user is admin # if @report.is_approver?(@user) # if user is the approver for this report # if Date.current.on_weekday? # can only approve on weekdays # @report.status = "approved" # @report.save # render json: @report # else # render json: {message: "Can only approve on weekdays"}, status: 401 # end # end # 👇 new code if authorized?(@user, ["approver"], @report) if Date.current.on_weekday? # can only approve on weekdays @report.status = "approved" @report.save render json: @report else render json: {message: “Can only approve on weekdays”}, status: 401 end else render json: {message: “You don’t have permission to approve this report”}, status: 401 end end # GET users/:user_id/reports/1 def show # 👇 new code # user can view if they are a submitter or approver of the report if authorized?(@user, ["submitter", "approver"], @report) render json: @report else render json: {message: “You don’t have permission to view this report”}, status: 401 end end private # Use callbacks to share common setup or constraints between actions. def set_report @report = Report.find(params[:id]) end def set_user @user = User.find(params[:user_id]) end end
To perform authorization checks, you’re using two different approaches. Let’s go action by action:
# GET users/:user_id/reports/submitted def submitted @reports = Report.find(reports(@user, "submitter")) render json: @reports if @reports end # GET users/:user_id/reports/review def review @reports = Report.find(reports(@user, "approver")).select{|r| r.status != "approved"} render json: @reports if @reports end
The
submitted
action queries and shows the reports where the user is a submitter. This check is performed using the reports
method, which internally calls the list objects endpoint from OpenFGA. This endpoint returns a list of all the objects of the given type that the user has a relation with, in this case, a submitter
relation. Similarly, the review
action performs the same check but for the approver
relation. Then the
approve
action performs a check to verify the user has a relation approver
with the report: # PUT users/:user_id/reports/1/approve def approve if authorized?(@user, ["approver"], @report) if Date.current.on_weekday? # can only approve on weekdays @report.status = "approved" @report.save render json: @report else render json: {message: “Can only approve on weekdays”}, status: 401 end else render json: {message: “You don’t have permission to approve this report”}, status: 401 end end
Finally, the
show
action allows a user to see a given report if they have any relation with it, like being a submitter
or an approver
:# GET users/:user_id/reports/1 def show # 👇 new code # user can view if they are a submitter or approver of the report if authorized?(@user, ["submitter", "approver"], @report) render json: @report else render json: {message: “You don’t have permission to view this report”}, status: 401 end end
Cleaning Up 🧹
You can clean up some stuff because you got rid of the admin role and verified ownership using the access token.
Let’s begin with the
Secured
concern in app/controllers/concerns/secured.rb
; you can remove the following code:# frozen_string_literal: true module Secured extend ActiveSupport::Concern REQUIRES_AUTHENTICATION = { message: ‘Requires authentication’}.freeze BAD_CREDENTIALS = { message: ‘Bad credentials’ }.freeze MALFORMED_AUTHORIZATION_HEADER = { error: ‘invalid_request’, error_description: ‘Authorization header value must follow this format: Bearer access-token’, message: ‘Bad credentials’ }.freeze # 🧹INSUFFICIENT_ROLES = { # 🧹 error: 'insufficient_roles', # 🧹 error_description: ‘The access token does not contain the required roles’, # 🧹 message: ‘Permission denied’ # 🧹}.freeze # 🧹NOT_OWNER = { # 🧹 error: 'not_owner', # 🧹 error_description: ‘The access token does not belong to the current user’, # 🧹 message: ‘Permission denied’ # 🧹}.freeze def authorize token = token_from_request validation_response = Auth0Client.validate_token(token) @decoded_token = validation_response.decoded_token return unless (error = validation_response.error) render json: { message: error.message }, status: error.status end # 🧹def validate_roles(roles) # 🧹 raise ‘validate_roles needs to be called with a block’ unless block_given? # 🧹 return yield if @decoded_token.validate_roles(roles) # 🧹 render json: INSUFFICIENT_ROLES, status: :forbidden # 🧹end # 🧹def validate_ownership(current_user) # 🧹 raise ‘validate_ownership needs to be called with a block’ unless block_given? # 🧹 return yield if @decoded_token.validate_user(current_user) # 🧹 render json: NOT_OWNER, status: :forbidden # 🧹end private def token_from_request authorization_header_elements = request.headers['Authorization']&.split render json: REQUIRES_AUTHENTICATION, status: :unauthorized and return unless authorization_header_elements unless authorization_header_elements.length == 2 render json: MALFORMED_AUTHORIZATION_HEADER, status: :unauthorized and return end scheme, token = authorization_header_elements render json: BAD_CREDENTIALS, status: :unauthorized and return unless scheme.downcase == 'bearer' token end
Because you no longer need to check the user’s access token, you can also remove the following code from the
app/lib/auth0_client.rb
file: # frozen_string_literal: true require ‘jwt’ require ‘net/http’ class Auth0Client # Class members Response = Struct.new(:decoded_token, :error) Error = Struct.new(:message, :status) # 🧹 Token = Struct.new(:token) do # 🧹 def validate_roles(roles) # 🧹 required_roles = Set.new roles # 🧹 token_roles = Set.new token[0][Rails.configuration.auth0.roles] # 🧹 required_roles <= token_roles # 🧹 end # 🧹 def validate_user(current_user) # 🧹 current_user.auth0_id == token[0]["sub"] # 🧹 end # 🧹end Token = Struct.new(:token) #...
Finally, you can remove the
AUTH0_ROLES
environment variable from config/auth0.yml
development: domain: <%= ENV.fetch('AUTH0_DOMAIN') %> audience: <%= ENV.fetch('AUTH0_AUDIENCE') %> # 🧹 roles: <%= ENV.fetch('AUTH0_ROLES') %> production: domain: <%= ENV.fetch('AUTH0_DOMAIN') %> audience: <%= ENV.fetch('AUTH0_AUDIENCE') %> # 🧹 roles: <%= ENV.fetch('AUTH0_ROLES') %>
...and your
.env
file: CLIENT_ORIGIN_URL=http://localhost:4040 AUTH0_AUDIENCE= AUTH0_DOMAIN= # 🧹 AUTH0_ROLES= FGA_API_URL=
Conclusion
Throughout this series, you took an expense management application and iterated it to implement different authorization systems.
Starting with no authorization at all, roles using Auth0 and an Auth0 Action are added to add a custom claim to the access token, access control over attributes is implemented, and finally, ReBAC with OpenFGA is implemented.
Overall, you should use the authorization system that best fits your business needs, and it’s ok to mix them up and have roles with ReBAC if that’s what your app needs.
Learn more about OpenFGA and OktaFGA in the blog and our docs.
About the author
![Carla Urrea Stabile](https://images.ctfassets.net/23aumh6u8s0i/6oI6DnEu4CJzuVTSALAOAt/e03a3bab4b7630cb586241beffac84b8/carla_urrea.jpg)
Carla Urrea Stabile
Senior Developer Advocate
I've been working as a software engineer since 2014, particularly as a backend engineer and doing system design. I consider myself a language-agnostic developer but if I had to choose, I like to work with Ruby, Python, and Elixir.
After realizing how fun it was to create content and share experiences with the developer community I made the switch to Developer Advocacy. I like to learn and work with new technologies.
When I'm not coding or creating content you could probably find me going on a bike ride, hiking, or just hanging out with my dog, Dasha.