Authorization

Okta FGA Authorization Policy

This policy will authorize requests using Okta FGA. If the request is not authorized, a 403 response will be returned.

Beta

This policy is in beta. You can use it today, but it may change in non-backward compatible ways before the final release.

Configuration

The configuration shows how to configure the policy in the 'policies.json' document.

{
  "name": "my-okta-fga-authz-inbound-policy",
  "policyType": "okta-fga-authz-inbound",
  "handler": {
    "export": "OktaFGAAuthZInboundPolicy",
    "module": "$import(@zuplo/runtime)",
    "options": {
      "authorizationModelId": "$env(FGA_MODEL_ID)",
      "credentials": {
        "clientId": "$env(FGA_CLIENT_ID)",
        "clientSecret": "$env(FGA_CLIENT_SECRET)"
      },
      "region": "us1",
      "storeId": "$env(FGA_STORE_ID)"
    }
  }
}
json

Policy Configuration

  • name <string> - The name of your policy instance. This is used as a reference in your routes.
  • policyType <string> - The identifier of the policy. This is used by the Zuplo UI. Value should be okta-fga-authz-inbound.
  • handler.export <string> - The name of the exported type. Value should be OktaFGAAuthZInboundPolicy.
  • handler.module <string> - The module containing the policy. Value should be $import(@zuplo/runtime).
  • handler.options <object> - The options for this policy. See Policy Options below.

Policy Options

The options for this policy are specified below. All properties are optional unless specifically marked as required.

  • region (required) <string> - The region your store is deployed. Allowed values are us1, eu1, au1.
  • storeId (required) <string> - The ID of the store.
  • authorizationModelId (required) <string> - The ID of the authorization model.
  • allowUnauthorizedRequests <boolean> - Indicates whether the request should continue if authorization fails. Default is false which means unauthorized users will automatically receive a 403 response. Defaults to false.
  • credentials (required) <object> - No description available.
    • clientId (required) <string> - The client ID.
    • clientSecret (required) <string> - The client secret.

Using the Policy

Usage

To use this policy, you must programmatically set the relationship checks to be performed against your Okta FGA store. This is done using the static setContextChecks method.

The most common way to set the authorization checks are:

  1. Creating custom inbound policies for each authorization scenario
  2. Creating a custom inbound policy that reads data from the OpenAPI operation and sets the authorization checks dynamically

Example: Custom Authorization Policies

Create a file like modules/oktafga-checks.ts to define your custom authorization policies:

import {
  ZuploRequest,
  ZuploContext,
  RuntimeError,
  HttpProblems,
  OktaFGAAuthZInboundPolicy,
} from "@zuplo/runtime";

export async function canReadFolder(
  request: ZuploRequest,
  context: ZuploContext,
) {
  if (!request.params?.folderId) {
    throw new RuntimeError("Folder ID not found in request");
  }

  context.log.info("Setting OktaFGA context checks");

  if (!request.user?.sub) {
    return HttpProblems.forbidden(request, context, {
      detail: "User not found",
    });
  }

  // Set the authorization check to verify if the user has viewer access to the folder
  OktaFGAAuthZInboundPolicy.setContextChecks(context, {
    user: `user:${request.user.sub}`,
    relation: "viewer",
    object: `folder:${request.params.folderId}`,
  });

  return request;
}

export async function canEditDocument(
  request: ZuploRequest,
  context: ZuploContext,
) {
  if (!request.params?.documentId) {
    throw new RuntimeError("Document ID not found in request");
  }

  if (!request.user?.sub) {
    return HttpProblems.forbidden(request, context, {
      detail: "User not found",
    });
  }

  // Set the authorization check to verify if the user has editor access to the document
  OktaFGAAuthZInboundPolicy.setContextChecks(context, {
    user: `user:${request.user.sub}`,
    relation: "editor",
    object: `document:${request.params.documentId}`,
  });

  return request;
}
typescript

Applying to Routes

In your route configuration, apply both the custom authorization policy and the OktaFGA policy:

{
  "path": "/folders/:folderId",
  "methods": ["GET"],
  "policies": {
    "inbound": ["jwt-auth", "authz-can-read-folder", "oktafga-authz"]
  }
}
json

Then in your policies.json:

{
  "name": "authz-can-read-folder",
  "export": "canReadFolder",
  "module": "$import(./modules/oktafga-checks)"
},
{
  "name": "oktafga-authz",
  "export": "OktaFGAAuthZInboundPolicy",
  "module": "$import(@zuplo/runtime)",
  "options": {
    // OktaFGA configuration...
  }
}
json

Example: Dynamic Authorization Checks

You can make your authorization checks more dynamic by reading data from your OktaAPI specification or other sources. This allows you to define authorization rules that adapt based on the route, method, or other request properties.

For example, you could access custom data defined in your route:

export async function dynamicAuthCheck(
  request: ZuploRequest,
  context: ZuploContext,
) {
  // Access custom data from the route configuration
  const data = context.route.raw<{
    "x-authz": {
      resourceType: string;
      permission: string;
      resourceIdParam: string;
    };
  }>();
  const authzData = data["x-authz"];

  if (!authzData?.resourceType || !authzData?.permission) {
    throw new RuntimeError(
      "Missing resource type or permission in route config",
    );
  }

  if (!request.user?.sub) {
    return HttpProblems.forbidden(request, context);
  }

  // Extract resource ID from request parameters
  const resourceId = request.params?.[authzData.resourceIdParam];

  if (!resourceId) {
    throw new RuntimeError(
      `Resource ID parameter '${authzData.resourceIdParam}' not found`,
    );
  }

  // Set dynamic authorization check
  OktaFGAAuthZInboundPolicy.setContextChecks(context, {
    user: `user:${request.user.sub}`,
    relation: authzData.permission,
    object: `${authzData.resourceType}:${resourceId}`,
  });

  return request;
}
typescript

Then in your OpenAPI document, you would set the custom data on the x-authz property:

{
  "paths": {
    "/custom-data": {
      "post": {
        "x-authz": {
          "resourceType": "document",
          "resourceIdParam": "documentId",
          "permission": "editor"
        }
      }
    }
  }
}
json

Read more about how policies work