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 boundaryorganization_roles– Contains role definitions scoped to specific organizations (e.g.,admin,member)organization_role_scope_relations– Maps which permissions (scopes) belong to each roleorganization_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 tourn: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):
organizations– Array of organization IDsorganization_data– Full organization objectsorganization_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, andorganization_role_user_relationstables. - Organization isolation – Roles and scopes are scoped to specific organizations, not global to the application.
- Special OAuth scopes –
urn:logto:scope:organizationsandurn:logto:scope:organization_rolestrigger the inclusion of organization claims in ID tokens. - URN format – Organizations use
urn:logto:organization:<orgId>format with helper functions available inopenid.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →