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
admin
role that can see everything. - A submitter user who can only see their reports or where their ID matches the attribute
submitter_id
of the report. - An approver user who can only see the reports where their ID matches the attribute
approver_id
of the report.
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
admin
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. - You are going to keep the attributes
submitter_id
andapprover_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). - You are going to add a
manager_id
to theusers
table to represent the manager relation so your app knows about this relation
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 Net::HTTP
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 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 Secured
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 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
deletes
object instead of awrites
object in the request body. This is out of the scope of this blog post.
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
:
GET users/:user_id/reports/review
ā see reports that the user hasnāt approved yetPUT users/:user_id/reports/:id/approve
ā approve a report of a userās directsGET users/:user_id/reports/submitted
ā see userās submitted reports
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.