Secure Access to Content Using Fine-Grained Permissions with FGA
Published on March 21, 2025While most of our sessions and labs focus on Authentication, in this lab, we'll look at the other side of the same coin: authorization. Just as it is essential to identify your users, it's equally important to know what actions they can or cannot perform in our application.
At Identiflix, there are currently no safeguards regarding who can access any content, and all students have access to courses independently of their enrollment status.
In this lab you'll use an Authorization strategy called Relationship-Based Access Control, or ReBAC for short to manage students' enrollments and access to courses and customize their course dashboards. This gives you a lot of flexibility, and using Okta's FGA product is easy to get started with.
NOTE: Okta FGA uses the OpenFGA decision engine, modeleing language and SDK's. OpenFGA is an open-source ReBAC solution, owned by the Cloud Native Computing Foundation (CNCF), and actively developed by us at Okta. If you prefer to setup your own instance instead of using our ready-to-use cloud offering, they are interchangeable for this lab.
In this lab, you'll learn how to:
- Install and configure the OpenFGA SDK.
- Create your first Authorization model using OpenFGA's Domain Specific Language (DSL).
- Query the OpenFGA SDK to check for permissions.
- Create new authorization tuples
- List all objects with a relationship to a certain user
Why Use This Technology?
You might already be familiar with Role-Based Access Control, which considers roles and permissions to make an Authorization decision. Relationship-based Access Control, on the other hand, considers relationships between consumers and resources (users and objects). This approach offers more flexibility and allows for finer-grained access control.
Lab Setup
Before we get started, let's make sure you have all you need to run this workshop:
- Complete the Identiflix Project Setup.
- Create a free Okta FGA account.
NOTE: If you already have an Auth0 account, you can use the same account to log into Okta FGA.
When you log in to the Okta FGA Dashboard, you'll be prompted to set up your new Okta FGA account. Choose a name and jurisdiction that makes sense for you, and use developer-day-24
as the store name. This store will contain the authorization model you'll use for this lab together with all the relevant data needed to make Authorization decisions.
NOTE: If you have a pre-existing Okta FGA Account, you can create a new store from the "select store" drop-down in the top left corner of the dashboard.
Step 1: Install the OpenFGA SDK
To use the Okta FGA decision engine in our project, you'll need to install and configure the OpenFGA SDK. Run npm install @openfga/sdk
in your project folder to install the SDK. When this is done, you'll notice that the package.json
now has the @openfga/sdk
dependency added and the SDK has been installed successfully.
You can create an fga.ts
file in the /lib
filder. You'll use this file to initialize the SDK using some configuration parameters from our .env
file. You will add those in the next step of this lab.
Paste the following code snippet in that new /lib/fga.ts
file.
import "server-only";import { CredentialsMethod, OpenFgaClient } from "@openfga/sdk";export const fgaClient = new OpenFgaClient({apiUrl: process.env.FGA_API_URL,storeId: process.env.FGA_STORE_ID,authorizationModelId: process.env.FGA_AUTHORIZATION_MODEL_ID,credentials: {method: CredentialsMethod.ClientCredentials,config: {apiTokenIssuer: process.env.FGA_API_TOKEN_ISSUER as string,apiAudience: process.env.FGA_API_AUDIENCE as string,clientId: process.env.FGA_CLIENT_ID as string,clientSecret: process.env.FGA_CLIENT_SECRET as string,},},});
Step 2: Create an Authorization model
Before you can ask Okta FGA for authorization decisions, you'll need to create an authorization model. This model will be the foundation on which the Okta FGA engine makes its decisions. This model is written declaratively using OpenFGA’s Domain Specific Language or DSL.
In the Okta FGA dashboard, navigate to the "Model Explorer" page through the sidebar navigation. You'll see it comes with a pre-populated model example for a Google Drive style application.
Let's change the model so it makes sense for our Identiflix application.
You'll add 2 types, a user
and a course
. A user can be directly assigned as a student
to a course
. Once assigned, a student has the can_read
permission for that course.
modelschema 1.1type usertype courserelationsdefine student: [user]define can_read: student
When you've changed the model, don't forget to click the save button!
If you've missed our sessions on Okta FGA or want a refresher, head on over to developerday.com, and watch the on-demand sessions.
Step 3: Configure Okta FGA in our codebase
Now that you have created your authorization model, it's time to configure the OpenFGA SDK in your codebase to connect to our Okta FGA Store. We've waited to do this until after you created your authorization model. These models are immutable, and their identifier changes every time you save your model. If you make changes to your model, the store ID needs to be updated in the .env
file.
Let's go to the settings page on your Okta FGA Dashboard. Here, you'll find the Store and Model ID's. Add them to your .env
file together with https://api.<jurisdiction>.fga.dev
as the API URL.
NOTE: Based on which jurisdiction you chose when setting up your store, you can replace the
<jurisdiction>
placeholder in this URL withus1
,eu1
, orau1
.
All that's left to do is create a new authorized client. Click the "Create client" button on that same settings page, give it a name, and select all client permissions. Once you click "create", a modal will appear with your client credentials. Add the Client ID and Client Secret to your .env
file, together with fga.us.auth0.com
as the token issuer and https://api.<jurisdiction>.fga.dev/
(note the trailing /
) as the audience. Again, replace the <jurisdiction>
placeholder with the jurisdiction you chose when you created the store.
Now you can save your credentials in the .env
file in your codebase and close the modal on the dashboard.
Your .env
file should look something like this:
# Auth0 variables# A long, secret value used to encrypt the session cookieAUTH0_SECRET=''# The base url of your applicationAUTH0_BASE_URL='http://localhost:3000'# The url of your Auth0 tenant domainAUTH0_ISSUER_BASE_URL=''# Your Auth0 application's Client IDAUTH0_CLIENT_ID=''# Your Auth0 application's Client SecretAUTH0_CLIENT_SECRET=''# Okta FGA variables# Found in the Store settings on your Okta FGA dashboard:# https://dashboard.fga.dev# The URL of the Okta FGA decision server API.# This can change based on the chose jurisdictionFGA_API_URL='https://api.eu1.fga.dev'# Your newly created store IDFGA_STORE_ID=''# Your newly created Authorization Model IDFGA_AUTHORIZATION_MODEL_ID=''# THe Okta FGA Token issuer, this is always fga.us.auth0.comFGA_API_TOKEN_ISSUER='fga.us.auth0.com'# The URL of the Okta FGA decision server API, as the audience.# This can change based on the chose jurisdiction.# Note the trailing slash (/)FGA_API_AUDIENCE='https://api.eu1.fga.dev/'# Your Okta FGA Client IDFGA_CLIENT_ID=''# Your Okta FGA Client secretFGA_CLIENT_SECRET=''
Step 4: Check if a user has enrolled for a course
By now, you have created a new Okta FGA store, installed the OpenFGA SDK, and configured it to connect to your Open FGA store. Now, it's time to use the SDK to perform an authorization check to ensure a user has enrolled for a course.
On line 44
in the data.ts
file, you'll find a getCourse()
method. This method will get a course from the database. Before you return the course details is a perfect place to do an authorization check.
Let's start by importing our OpenFGA from the SDK. Add the following import statement to the top of the file together with the other imports.
import { fgaClient } from "@/lib/fga";
Now on line 33
you'll see we return a course from the database. Let's add an FGA Check before we do so. We can call fgaClient.check()
, and pass the user ID, course ID, and a relation of can_read
to this check. This will query the Okta FGA decision engine to check whether the current user is enrolled as a student for the course you're trying to get. If the current user is a student for this course, they should have the can_read
permission, and this check will return true
.
Our getCourse()
method should now look like this:
async function getCourse(slug: string): Promise<Course | undefined> {const session = await getSession();if (!session) {return;}const userId = session.user["sub"];const course = db.courses.find((course) => course["slug"] === slug);if (!course) {return;}const { allowed } = await fgaClient.check({user: `user:${userId}`,object: `course:${course.id}`,relation: "can_read",});if (allowed) {return course;}}
This check will currently return false
because you have not yet enrolled any users for any courses. To do this, you'll need to add some authorization data to your OpenFGA Store.
Step 5: Enroll for a course
Whenever a student enrolls for a course, you must ensure your decision engine knows about this. You can do this by writing a "tuple" to your Okta FGA store. A tuple is an object with a user
, object
, and relation
value.
In the same data.ts
file, on line 45
, you'll find the enrollToCourse()
method. This method will help you create these tuples using the fgaClient.writeTuples()
method available in the OpenFGA SDK.
You'll start by checking if the current user already has permission to view the course; if so, that means they are already enrolled for it, and we don't need to create a new tuple. If they don't have the can_read
permission yet, we'll create a new tuple with the user's ID, the course ID, and a relation of student
. This will inform the Open FGA decision engine that the current user is a course student, and therefore has that can_read
permission.
The method will look like this after we've added the new fgaClient.writeTuples()
call.
async function enrollToCourse(courseSlug: string) {const session = await getSession();if (!session) {throw Error("You are not logged in!");}const userId = session.user["sub"];const course = db.courses.find((course) => course["slug"] === courseSlug);if (!course) {throw Error("Course not found");}// Check if the user is already enrolled in the courseconst { allowed } = await fgaClient.check({user: `user:${userId}`,relation: "can_read",object: `course:${course.id}`,});// If the user is not enrolled, then enroll themif (!allowed) {fgaClient.writeTuples([{user: `user:${userId}`,object: `course:${course.id}`,relation: "student",},]);}}
You can now test this out in the application. Navigate to the Catalog page and enroll for a course.
Once you've done so, head on over to your Okta FGA Dashboard, go to the Tuple management page and hit refresh. You should see a new tuple, with your user's ID, the course for which you just enrolled, and the relation student
.
Step 6: List all enrolled courses
You also want to show an overview of all courses a user has been enrolled in so that they can easily access these. We can do this by leveraging OpenFGA's listObjects()
. This method will return all objects of a particular type, that have a relation to a specific user.
In this case, we can list all courses for which our user has a can_view
permission. On line 15
in the data.ts
file, you'll find a getMyCourses()
method. In this function you can use the fgaClient.listObjects()
function to retrieve all ID's for the courses our user has been enrolled in, since that data lives in our Okta FGA store. Once you have a list of all enrolled courses, you can map over those IDs to exchange them for the course content from your database.
async function getMyCourses(): Promise<Course[]> {const session = await getSession();if (!session) {return [];}const userId = session.user["sub"];// List all courses that the user is enrolled inconst response: ListObjectsResponse = await fgaClient.listObjects({user: `user:${userId}`,type: 'course',relation: 'can_read',})return response.objects.map((response: string) => {return db.courses.find((course) => course.id === stripObjectName(response))}).filter(Boolean) as Course[];}
Note that this function uses the stripObjectName()
helper function. This will strip the object names from the IDs returned by the fgaClient.listObjects()
function.
Add the import statement for the helper function to the top of your data.ts
file.
import { stripObjectName } from "@/lib/utils";
You can find all the courses in which your user has enrolled on the My Courses page.
Recap
Using Okta FGA as a decision engine makes it easy to check for permissions in our codebase. No nested if-else statements, but clean check()
or listObject()
calls. Whenever our users perform an action, it only takes a writeTuple()
call to notify our decision engine of these changes and reflect them in the next check()
call.
To learn more about OktaFGA check out the following dedicated passkeys sessions:
If you'd like to learn more about Okta FGA or its underlying decisions engine OpenFGA check out the following sessions and materials:
- Paradigm Shift: Moving Beyond Roles and Permissions to a Fine-Grained Access Control by Sam Bellen
- A Practical Guide to Migrating from RBAC to ReBAC with Okta FGA by Tyler Nix
- Visit the fga.dev and openfga.dev websites.