How to Implement RBAC with Roles and Scopes in Logto

Logto implements organization-level RBAC through four relational tables that map users to roles and scopes, exposing permissions via special OAuth scopes and ID token claims.

Logto provides a comprehensive role-based access control (RBAC) system that operates at the organization level, allowing multi-tenant applications to define granular permissions within isolated organizational contexts. The logto-io/logto repository stores RBAC metadata across four core database tables and exposes management APIs to configure roles, scopes, and user assignments programmatically.

Understanding the RBAC Data Architecture

Logto's RBAC implementation relies on a relational schema that connects organizations, roles, scopes, and users. According to the source code in packages/schemas/tables/, the architecture consists of four primary tables:

  • organizations – Stores organization metadata and defines the organizational boundary
  • organization_roles – Contains role definitions scoped to specific organizations (e.g., admin, member)
  • organization_role_scope_relations – Maps which permissions (scopes) belong to each role
  • organization_role_user_relations – Links users to their assigned roles within an organization

This design enforces that roles and permissions are strictly isolated per organization, with foreign-key constraints ensuring users must be organization members before receiving role assignments.

Organization Scopes and URN Format

Logto defines two special OAuth scopes for accessing organization data, enumerated in packages/toolkit/core-kit/src/openid.ts:

  • urn:logto:scope:organizations – Returns the list of organization IDs a user belongs to
  • urn:logto:scope:organization_roles – Returns the roles the user holds inside each organization

Organizations utilize a Uniform Resource Name (URN) format of urn:logto:organization:<orgId>. The source code exports helper functions buildOrganizationUrn and getOrganizationIdFromUrn in openid.ts to handle deterministic conversion between organization IDs and URN strings.

When a client requests these organization scopes, Logto injects three key claims into the ID token (or userinfo endpoint):

  1. organizations – Array of organization IDs
  2. organization_data – Full organization objects
  3. organization_roles – Role IDs mapped per organization

The claim list is built from extendedIdTokenClaimsByScope in the same file.

Step-by-Step RBAC Implementation Workflow

Implementing RBAC in Logto requires six sequential steps involving the Management API and client-side token requests.

1. Create an Organization

An admin calls the Management API to create an organizational container. This inserts a record into the organizations table.

2. Define Organization Roles

Create roles within the organization scope. Each role is stored in organization_roles with a type constraint of "User" (validated by check_organization_role_type). Unlike global application roles, these are isolated to the specific organization.

3. Attach Scopes to Roles

Define granular permissions (such as read:projects or write:resources) and link them to roles through the organization_role_scope_relations table. This creates the permission matrix that determines what each role can access.

4. Assign Users to Organizations

Before assigning roles, users must be added to the organization via the organization_user_relations table. The foreign-key constraint on organization_role_user_relations enforces this membership requirement.

5. Assign Roles to Users

Link users to their specific roles using the organization_role_user_relations table. A single user can hold multiple roles within the same organization.

6. Request Tokens with Organization Scopes

Clients include urn:logto:scope:organizations and urn:logto:scope:organization_roles in the authorization request. Logto validates the request and embeds the corresponding claims in the ID token.

7. Verify on Resource Server

The resource server extracts the access token, validates the granted scopes, and checks the user's role-scope relations in the database to enforce authorization decisions.

API Endpoints and User Organization Data

The Logto core exposes endpoints for managing RBAC relationships. In packages/core/src/routes/admin-user/organization.ts, the GET /users/:userId/organizations endpoint returns an array of organizations with embedded role data for a specific user.

Management API endpoints (documented in packages/core/src/routes/role.openapi.json) allow administrators to:

  • Create organizations (POST /api/organizations)
  • Define roles (POST /api/organizations/{id}/roles)
  • Attach scopes (POST /api/organizations/{id}/roles/{roleId}/scopes)
  • Assign users to roles (POST /api/organizations/{id}/users/{userId}/roles)

Complete Implementation Example

Below is a TypeScript implementation using the Logto Management API. Replace YOUR_ACCESS_TOKEN with an admin token and configure your tenant endpoint accordingly.

import fetch from 'node-fetch';

// Helper to call Logto Management API
const api = (path: string, init: RequestInit = {}) =>
  fetch(`https://api.logto.io/${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer YOUR_ACCESS_TOKEN`,
      'Content-Type': 'application/json',
    },
  }).then((res) => res.json());

// 1️⃣ Create an organization
const org = await api('api/organizations', {
  method: 'POST',
  body: JSON.stringify({
    name: 'Acme Corp',
    description: 'Acme Corp organization',
  }),
});
console.log('Organization created:', org.id);

// 2️⃣ Define a role inside the organization
const role = await api(`api/organizations/${org.id}/roles`, {
  method: 'POST',
  body: JSON.stringify({
    name: 'admin',
    description: 'Full access inside Acme',
    type: 'User', // Must be "User" for organization roles
  }),
});
console.log('Role created:', role.id);

// 3️⃣ Attach scopes to the role
await api(
  `api/organizations/${org.id}/roles/${role.id}/scopes`,
  {
    method: 'POST',
    body: JSON.stringify({
      scopes: [
        'read:projects',
        'write:projects',
      ],
    }),
  }
);
console.log('Scopes attached to role');

// 4️⃣ Add a user to the organization (if not already a member)
await api(`api/organizations/${org.id}/users`, {
  method: 'POST',
  body: JSON.stringify({ userId: 'user-123' }),
});
console.log('User added to organization');

// 5️⃣ Assign the role to the user
await api(
  `api/organizations/${org.id}/users/user-123/roles`,
  {
    method: 'POST',
    body: JSON.stringify({ roleId: role.id }),
  }
);
console.log('Role assigned to user');

// 6️⃣ Request an access token with organization scopes (client side)
import { AuthorizationCode } from '@logto/client';
const client = new AuthorizationCode({
  clientId: 'YOUR_CLIENT_ID',
  redirectUri: 'https://your.app/callback',
  issuer: 'https://logto.io',
});
const authUrl = client.getAuthorizationUri({
  scope: 'openid profile urn:logto:scope:organizations urn:logto:scope:organization_roles',
});
// Redirect the user to `authUrl`, then exchange the code for tokens.
// The resulting ID token will contain `organizations` and `organization_roles` claims.

Summary

  • Four-table architecture – Logto stores RBAC data in organizations, organization_roles, organization_role_scope_relations, and organization_role_user_relations tables.
  • Organization isolation – Roles and scopes are scoped to specific organizations, not global to the application.
  • Special OAuth scopes – urn:logto:scope:organizations and urn:logto:scope:organization_roles trigger the inclusion of organization claims in ID tokens.
  • URN format – Organizations use urn:logto:organization:<orgId> format with helper functions available in openid.ts.
  • Enforcement – Resource servers validate access tokens against the database relations to enforce permissions.

Frequently Asked Questions

What database tables store RBAC data in Logto?

Logto stores RBAC metadata across four tables defined in packages/schemas/tables/: organizations for organization data, organization_roles for role definitions, organization_role_scope_relations for the permission matrix, and organization_role_user_relations for user-role assignments. These tables enforce referential integrity through foreign-key constraints that ensure users are organization members before receiving role assignments.

How do organization-scoped roles differ from global roles in Logto?

Organization roles are isolated to specific organizations and stored in the organization_roles table with a required type of "User". They are managed through organization-specific Management API endpoints (e.g., /api/organizations/{id}/roles). Global roles, by contrast, apply application-wide and are not constrained to organizational boundaries. The function check_organization_role_type validates that organization roles use the correct type enum.

What claims does Logto add to the ID token for organization access?

When requesting the urn:logto:scope:organizations and urn:logto:scope:organization_roles scopes, Logto injects three claims into the ID token: organizations (array of organization IDs), organization_data (full organization objects), and organization_roles (role IDs per organization). These claims are mapped through the extendedIdTokenClaimsByScope configuration in packages/toolkit/core-kit/src/openid.ts.

How do I validate organization permissions in my resource server?

The resource server extracts the access token and verifies the presence of required scopes (such as urn:logto:scope:organization_roles). For fine-grained permission checks, the server queries the organization_role_scope_relations table to verify that the user's assigned roles include the necessary scopes for the requested resource. The scope validation logic references the protectedAppAdditionalScopes constant defined in the core toolkit.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →