Secure Access to Content Using Fine-Grained Permissions with FGA

Published on March 21, 2025
Learning GoalLearn how to successfully add Relationship-Based Access Control decisions using Okta FGA to protect your application.

While 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:

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.

/lib/fga.ts
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.

Okta FGA Dashboard Model Explorer

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.

FGA_Model
model
schema 1.1
type user
type course
relations
define 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 with us1, eu1, or au1.

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.

Okta FGA Dashboard Model Explorer
Okta FGA Dashboard Model Explorer

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:

.env
# Auth0 variables
# A long, secret value used to encrypt the session cookie
AUTH0_SECRET=''
# The base url of your application
AUTH0_BASE_URL='http://localhost:3000'
# The url of your Auth0 tenant domain
AUTH0_ISSUER_BASE_URL=''
# Your Auth0 application's Client ID
AUTH0_CLIENT_ID=''
# Your Auth0 application's Client Secret
AUTH0_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 jurisdiction
FGA_API_URL='https://api.eu1.fga.dev'
# Your newly created store ID
FGA_STORE_ID=''
# Your newly created Authorization Model ID
FGA_AUTHORIZATION_MODEL_ID=''
# THe Okta FGA Token issuer, this is always fga.us.auth0.com
FGA_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 ID
FGA_CLIENT_ID=''
# Your Okta FGA Client secret
FGA_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:

/data/data.ts
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.

/data/data.ts
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 course
const { allowed } = await fgaClient.check({
user: `user:${userId}`,
relation: "can_read",
object: `course:${course.id}`,
});
// If the user is not enrolled, then enroll them
if (!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.

Okta FGA Dashboard Model Explorer

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.

Okta FGA Dashboard Model Explorer

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.

/data/data.ts
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 in
const 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.

Okta FGA Dashboard Model Explorer

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: