Authorization With FusionAuth FGA by Permify

To use FusionAuth FGA By Permify to implement fine-grained authorization in your web or mobile application, do the following:

  • install Permify
  • set up your Permify schema including users and application entities
  • add authorization data mapping relationships between FusionAuth users and Permify entities
  • add permission checks to your application to control access to functionality and data

Permify uses the term entities to refer to anything that can have a relation in a Permify authorization schema. This is distinct from FusionAuth entities, which has no formal schema and is instead managed by code.

For the remainder of this document, when the term entities is used, it refers to Permify entities. They may also be called fine-grained entities.

Let’s look at each of these steps in more detail using an example.

The Example Scenario

Suppose you have a banking application for which you want to manage access. Each bank has roles such as member or teller. People can have different roles at different banks. In addition, there are actions that can only be performed at certain times of day.

Using fine-grained authorization allows you to control access to each part of the banking application.

If you want to see a fully functioning application, clone the example GitHub repository and follow the instructions in the readme.

Installing Permify

To install Permify, please refer to the Permify deployment guides. These also offer guidance on system requirements and how to deploy the software into Kubernetes and other environments.

For local development, you can run Permify using Docker similarly to how you can run FusionAuth.

The following excerpt from a docker-compose.yml file creates a Permify instance for this banking example:

Permify docker compose section

permify:
  image: ghcr.io/permify/permify
  command: serve
  environment:
    PERMIFY_LOG_LEVEL: warn
    PERMIFY_AUTHN_ENABLED: true
    PERMIFY_AUTHN_METHOD: preshared
    PERMIFY_AUTHN_PRESHARED_KEYS: "sample_api_key_please_change"
  ports:
    - "3476:3476"
    - "3478:3478"

Review the Permify configuration reference for supported configuration options.

Set Up The Permify Schema

The following schema defines the relationships between entities in the example banking scenario:

FGA schema

entity user {} 

entity bank {

    relation vp @user    
    relation teller @user    
    relation member @user
    
    attribute open_hour integer
    attribute close_hour integer

  
    action account = teller or vp or member
    action makechange = teller and is_in_time_range(open_hour, close_hour)
    action admin = teller or vp

}

rule is_in_time_range(open_hour integer, close_hour integer) {
  (open_hour <= context.data.current_hour) && (close_hour >= context.data.current_hour) 
}

The user entity represents users and the bank entity represents a bank.

The bank entity has relations, attributes and actions:

  • a relation defines a relationship between two entities, such as the vp role for a user
  • an attribute is a value associated with an entity, such as the open_hour for a bank
  • an action is the name of a permission that can be checked using the Permify API or SDK, such as viewing the account page

You can also write a rule like is_in_time_range. Rules use attributes from an entity to codify more complicated permission logic. Rules can be global or associated with a particular entity definition.

You can test a custom schema using the Permify playground. This lets you verify correctness as well as visualize relationships.

For this tutorial, run the following Node.js script to load the schema into Permify:

FGA schema load script

import * as permify from "@permify/permify-node";
import * as dotenv from "dotenv";
import { readFileSync } from "fs";
import { join } from "path";

// Load environment variables
dotenv.config();

// Validate required environment variables
if (!process.env.PRESHARED_KEY) {
  console.error('Error: Missing PRESHARED_KEY from .env');
  process.exit(1);
}

if (!process.env.PERMIFY_ENDPOINT) {
  console.warn('Warning: PERMIFY_ENDPOINT not set, using default localhost:3478');
}

// Initialize Permify client
const permifyclient = permify.grpc.newClient({
  endpoint: process.env.PERMIFY_ENDPOINT || "localhost:3478",
  cert: null,
  insecure: process.env.NODE_ENV !== "production",
  pk: null,
  certChain: null,
},
  permify.grpc.newAccessTokenInterceptor(process.env.PRESHARED_KEY)
);

/**
 * Reads the authorization model from authmodel.txt
 * @returns {string} The authorization model content
 */
function readAuthModel() {
  try {
    const filePath = join(process.cwd(), 'authmodel.txt');
    const content = readFileSync(filePath, 'utf-8');
    
    if (!content.trim()) {
      throw new Error('authmodel.txt is empty');
    }
    
    return content;
  } catch (error) {
    if (error.code === 'ENOENT') {
      console.error('Error: authmodel.txt not found in current directory');
    } else {
      console.error(`Error reading authmodel.txt: ${error.message}`);
    }
    process.exit(1);
  }
}

/**
 * Creates the schema in Permify
 * @param {string} schemaContent - The authorization model schema
 */
async function createSchema(schemaContent) {
  try {
    console.log('Creating schema in Permify...');
    
    const response = await permifyclient.schema.write({
      tenantId: process.env.TENANT_ID || "t1",
      schema: schemaContent,
    });
    
    console.log(`Schema version: ${response.schemaVersion}`);
    
    return response;
  } catch (error) {
    console.error('Error creating schema:', error.message);
    
    if (error.details) {
      console.error('Details:', error.details);
    }
    
    process.exit(1);
  }
}

/**
 * Main function to orchestrate the schema creation
 */
async function main() {
  console.log('Starting Permify schema creation...\n');
  
  // Read the authorization model
  const authModel = readAuthModel();
  
  // Create the schema
  await createSchema(authModel);
}

// Run the script
main().catch((error) => {
  console.error('Unexpected error:', error);
  process.exit(1);
});

Add Authorization Data

Once you have a schema, add authorization data to define entity relationships and attributes. This data will be used during the permission check step to determine if requested permissions are allowed.

The following example assigns each user a role at a bank with an Id of 1:

Loading relationships

async function writeRelationshipTuples() {
  try {
    console.log('Writing relationship tuples...');
    
    const response = await permifyclient.data.write({
      tenantId: process.env.TENANT_ID || "t1",
      metadata: {
        schemaVersion: "", // Optional: specify schema version
      },
      tuples: [
        {
          entity: {
            type: "bank",
            id: "1"
          },
          relation: "vp",
          subject: {
            type: "user",
            id: "00000000-0000-0000-0000-000000000001"
          }
        },
        {
          entity: {
            type: "bank",
            id: "1"
          },
          relation: "member",
          subject: {
            type: "user",
            id: "00000000-0000-0000-0000-111111111111"
          }
        },
        {
          entity: {
            type: "bank",
            id: "1"
          },
          relation: "teller",
          subject: {
            type: "user",
            id: "00000000-0000-0000-0000-222222222222"
          }
        }
      ]
    });
    
    console.log(`Snap token: ${response.snapToken}`);
    
    return response;
  } catch (error) {
    console.error('Error writing relationship tuples:', error.message);
    if (error.details) {
      console.error('Details:', error.details);
    }
    throw error;
  }
}

You can also assign attributes to entities. The following example assigns the bank with the Id of 1 an opening hour and a closing hour:

Loading attributes

async function writeAttributes() {
  try {
    console.log('\nWriting attributes...');

    const openValue = Any.fromJSON({
        typeUrl: 'type.googleapis.com/base.v1.IntegerValue',
        value: IntegerValue.encode(IntegerValue.fromJSON({ data: 7 })).finish()
    });
    
    const closeValue = Any.fromJSON({
        typeUrl: 'type.googleapis.com/base.v1.IntegerValue',
        value: IntegerValue.encode(IntegerValue.fromJSON({ data: 17 })).finish()
    });
    
    const response = await permifyclient.data.write({
      tenantId: process.env.TENANT_ID || "t1",
      metadata: {
      },
      attributes: [
        {
          entity: {
            type: "bank",
            id: "1"
          },
          attribute: "open_hour",
          value: openValue
        },
        {
          entity: {
            type: "bank",
            id: "1"
          },
          attribute: "close_hour",
          value: closeValue
        }
      ]
    });
    
    console.log(`Snap token: ${response.snapToken}`);
    
    return response;
  } catch (error) {
    console.error('Error writing attributes:', error.message);
    if (error.details) {
      console.error('Details:', error);
    }
    throw error;
  }
}

Add Permission Checks

Finally, add permission checks to your application code. A check returns a value based on whether the action is allowed. Permify uses multiple pieces of data to determine if a given permission is granted. These include:

  • the requested permission, which corresponds to the action defined in the schema, such as account or admin
  • the entity Ids and types
  • the subject Id, which indicates who is asking for this permission
  • any context, such as the time or the requester’s IP address

Here’s an example which has the following data:

  • a permission stored in the permission variable
  • an entity of type bank with an identifier of 1
  • a subject of type user with an identifier drawn from a token
  • the current hour

Checking permissions

const now = new Date();
const mtTime = new Date(now.toLocaleString("en-US", {timeZone: "America/Denver"}));
const hour = mtTime.getHours();

let response = await permifyclient.permission.check({
  tenantId: "t1",
  metadata: {
    schemaVersion: "",
    depth: 20,
  },
  entity: {
    type: "bank",
    id: "1",
  },
  permission: permission,
  subject: {
    type: "user",
    id: decodedFromJwt?.sub
  },
  context: {
    data: {
        "current_hour": hour
    }
  }
});

let checkresult = response.can === permify.grpc.base.CheckResult.CHECK_RESULT_ALLOWED;

This check compares the value returned from Permify to the CHECK_RESULT_ALLOWED constant to determine if the check succeeded.

Where To Place Checks

The example application maps permissions directly to pages and does the check on every request.

There are other points in any application where you can enforce authorization. These are business logic specific.

You can also perform multiple checks. For example, you can check a user’s permission:

  • in the UX, which offers a better user experience, but don’t rely on this to secure access
  • in the backend API
  • at the gateway
  • when performing non-idempotent operations such as modifying data
  • when performing high risk operations

Designing Your Schema

Designing your authorization schema is a large topic that won’t be covered here. Permify has modeling documentation:

If you’d like assistance beyond the documentation, contact us.

Limitations

When using FusionAuth FGA by Permify, both FusionAuth and Permify are required. Plan to manage and upgrade both of these software applications.

Maintaining the authorization data is a continuous process, not a one-time operation. Plan to keep the following data synced between the source of record and Permify:

  • user data
  • application specific entities
  • relations between users and entities

For user data, you can use the User Create and User Update webhooks to propagate user data. Make these webhooks transactional, but fast, so use something like a durable queue to receive the events. Or if you have Kafka in your system already, publish them to Kafka. Then, use one of the Permify SDKs and whenever changes occur, update Permify.

For application-specific entities and relations, use one of the Permify SDKs and whenever changes occur, update Permify.

You can store relations in FusionAuth, in the user.data field. In this case, you’ll need to sync relations from FusionAuth to Permify using the same webhook consumer that updates user data.