Authorization
Authentication answers “who are you?” Authorization answers “what are you allowed to do?” Auris handles both, but authorization is the deeper concern: almost every API call, every UI route, and every data read goes through at least one authorization check before it completes. This page explains how the system is structured, how each layer works, and how to apply the right layer for your use case.
Three Layers
Auris authorization is built in three distinct layers that stack on top of each other. Each layer adds precision.
| Layer | Mechanism | Granularity | Typical question answered |
|---|---|---|---|
| 1 — Authentication | Keycloak JWT | Per-tenant user identity | ”Is this user authenticated and do they belong to this tenant?“ |
| 2 — RBAC | Permission strings on roles | Per-action-type | ”Does this user have the general right to perform this kind of action?“ |
| 3 — FGA | Zanzibar relationship tuples | Per-resource instance | ”Does this user have access to this specific resource?” |
In most API routes you will use Layer 2 alone. When your application manages resources that different users own or share (documents, projects, calendars, tickets), you add Layer 3 on top.
Layer 1 — Authentication (Keycloak JWT)
Every request starts with a signed JWT issued by Keycloak. The token is validated on every request by protectRoute() / requireAuth() in lib/auth/protect.ts. Validation includes:
- Signature verification using Keycloak’s public keys
- Expiry and not-before checks
- Token blacklist check (for explicitly invalidated sessions)
The tenant is resolved from the x-tenant request header or from the iss claim in the JWT (which contains /realms/{realm} — the realm name maps directly to an Auris Tenant record).
If the token is missing, malformed, blacklisted, or does not belong to the requested tenant, the request fails with 401 Unauthorized before any permission logic runs.
Layer 2 — RBAC
Once a request is authenticated, the permission engine checks whether the user’s roles grant the required permission. Permissions are action:resource strings (e.g., manage:users, view:invoices). They are stored in Prisma across three tables:
PermissionDefinition— the catalog of all known permission stringsRolePermission— which permissions are assigned to a role, with an effect ofALLOWorDENYUserPermissionOverride— per-user permission grants or denials that bypass role evaluation, with an optionalexpiresAtfor time-boxed grants
Resolution order (Discord-style priority):
- User override — if the user has any active (non-expired)
UserPermissionOverridefor this permission, its effect wins immediately — both ALLOW and DENY are treated as the same priority tier. There is no separate step for user-DENY vs user-ALLOW; whichever override record exists takes precedence over all role-level logic. admin:allbypass — if any of the user’s roles carriesadmin:allas an ALLOW (and no role denies it), every other permission is granted. A user-level ALLOW override onadmin:allalso triggers this bypass. Importantly, a user-level DENY onadmin:alldoes not block individual permissions that are explicitly ALLOWed at the role level for that permission — theadmin:allDENY only prevents the super-admin bypass path, not individual permission grants.- Role DENY wins — if any role has a DENY for this permission, deny.
- Role ALLOW — if any role has an ALLOW (and none deny), grant.
- Default deny — if nothing matched, deny.
This means user-level overrides always win over role-level grants, and within the role tier, denies win over allows.
For a deeper treatment of role creation, permission assignment, and the API endpoints that manage them, see the RBAC guide.
Layer 3 — Fine-Grained Authorization (FGA)
RBAC tells you whether a user has the right to perform an action on a resource type. FGA tells you whether a user has access to a specific resource instance. The distinction matters whenever different users legitimately own or share different objects of the same type.
FGA in Auris is a Zanzibar-compatible engine backed by relationship tuples stored in the RelationshipTuple Prisma table. A tuple looks like:
documents:doc_abc123 viewer user:usr_xyz456That tuple says: user usr_xyz456 is a viewer of document doc_abc123. Authorization checks traverse the graph of tuples to determine whether a relationship exists.
For the full model syntax, check algorithm, rewrite types, and modeling patterns, see Fine-Grained Authorization (Zanzibar Model).
Permission Strings
Every RBAC permission is an action:resource string. The action describes the operation; the resource describes the domain entity it targets.
manage:users view:audit issue:invoices
create:documents delete:expenses debug:fga
ai:use console:access admin:allBuilt-in permission categories
| Category | Example strings |
|---|---|
| Super admin | admin:all |
| Roles | manage:roles, view:roles, assign:roles |
| Users | manage:users, view:users |
| Permissions | manage:permissions, view:permissions |
| Audit | view:audit, export:audit, view:audit_logs |
| Tenant | manage:tenant, view:tenant |
| Security | manage:security, view:security |
| System | manage:system, view:system |
| Content | manage:content, collaborate:content, share:content |
| User data | manage:user_data, export:user_data |
| Integrations | manage:integrations, view:integrations |
| Marketplace | view:marketplace, manage:marketplace, install:integrations, developer:marketplace |
| Sessions | manage:sessions |
| SSO | manage:sso_connections, view:sso_connections |
| SCIM | manage:scim_connections, view:scim_connections, view:scim_logs |
| Identity providers | manage:identity_providers, view:identity_providers |
| Members and teams | manage:members, view:members |
| Console | console:access |
| FGA | view:fga_models, manage:fga_models, view:fga_tuples, manage:fga_tuples, debug:fga |
| Licensing | manage:license-policies, manage:license-keys, view:license-stats |
| AI | ai:use |
| Documents (invoices, quotes, orders, expenses, interventions…) | view:invoices, create:invoices, edit:invoices, delete:invoices, export:invoices, issue:invoices, manage:invoice_payments, approve:expenses, sign:interventions |
| Payments and billing | view:billing, manage:billing, manage:subscriptions, pay:invoices, refund:payments, view:disputes, manage:dunning |
The special permission admin:all grants every other permission when held on a role. A user-level DENY override on admin:all prevents the super-admin bypass path, but it does not retroactively block individual permissions that are explicitly ALLOWed on the user’s roles — those still resolve normally through role evaluation.
Roles
A role is a named collection of permission grants and denials, scoped to a tenant. Users can hold multiple roles simultaneously. When a user holds multiple roles the engine merges them: if any role grants a permission and no role denies it, the permission is granted.
Roles also carry a position field (a numeric rank). Lower numbers indicate higher privilege. The role hierarchy controls which users can manage which other users: a user cannot act on another user whose highest-priority role outranks their own. getUserHighestPriority() returns the lowest position number across all of a user’s roles; 0 means most privileged, 999 means no roles assigned.
Create and manage roles via the Auris Console (Roles section) or via the API:
POST /api/roles Create a role
PATCH /api/roles/{id} Update name / description
POST /api/roles/{id}/permissions Assign permissions
DELETE /api/roles/{id}/permissions Remove permissions
POST /api/users/{id}/roles Assign a role to a user
DELETE /api/users/{id}/roles/{rid} Remove a role from a userFor detailed API reference and examples see the RBAC guide.
Fine-Grained Authorization
RBAC is appropriate for gates that apply uniformly across all instances of a resource type — “can this user create documents at all?” FGA is necessary when the answer depends on which specific instance is being accessed — “can this user edit this particular document?”
Standard relations and namespaces
The Auris platform defines four standard relations that apply across all resource types:
| Relation | Meaning |
|---|---|
owner | Full control — read, write, share, delete |
editor | Read and write |
viewer | Read only |
shared | Custom, application-defined access |
The standard namespaces correspond to core platform resource types:
documents domains vps calendars channels ticketsYour application can define additional namespaces by creating an authorization model in the console.
The two-call pattern
The canonical pattern for protecting a resource instance combines both layers:
// 1. RBAC gate — does this user have the general right to edit documents?
// requirePermission throws a NextResponse on failure; no return value on success.
await requirePermission(req, 'edit:documents', realm)
// 2. FGA gate — does this user have access to THIS document?
// requireResourceAccess also throws on failure; return type is Promise<void>.
await requireResourceAccess(req, userId, 'editor', 'documents', documentId)
// Proceed with business logicBoth functions throw a NextResponse (401 or 403) on failure — they do not return a discriminated union. In a Next.js route handler the thrown NextResponse is caught by the framework and returned to the client automatically.
requireResourceAccess() must always be called after requirePermission(), not instead of it. RBAC provides the coarse-grained type-level gate; FGA provides the fine-grained instance-level gate. Skipping RBAC and relying on FGA alone means a user with no document permissions at all could still be granted individual-document access through a tuple, which breaks the intended separation.
The bridge functions in lib/permissions/resource-access.ts manage tuple writes and reads:
| Function | Purpose |
|---|---|
checkResourceAccess(userId, relation, namespace, resourceId) | Returns a boolean — programmatic check |
checkAnyResourceAccess(userId, relations[], namespace, resourceId) | True if any of the listed relations holds |
grantResourceAccess(userId, relation, namespace, resourceId) | Writes a tuple (grants access) |
revokeResourceAccess(userId, relation, namespace, resourceId) | Deletes a tuple (revokes access) |
listResourceAccessors(namespace, resourceId, relation?) | Lists all subjects with access to a resource |
listUserResources(userId, namespace, relation?) | Lists all resources accessible to a user |
transferResourceOwnership(fromUserId, toUserId, namespace, resourceId) | Atomically transfers the owner tuple |
requireResourceAccess(req, userId, relation, namespace, resourceId) | Middleware — return type is Promise<void>; throws a NextResponse (403) on failure, never returns a discriminated union |
For the FGA model DSL, check algorithm, union/intersection/exclusion rewrites, and the console debugger, see Fine-Grained Authorization (Zanzibar Model).
The FGA_ENGINE_ENABLED environment variable controls only Layer 3 resource-level checks in lib/permissions/resource-access.ts. When true, those checks run through the internal Zanzibar engine backed by the RelationshipTuple Prisma table. When false, they fall back to Ory Keto via lib/permissions/keto-client.ts. This toggle has no effect on Layer 2 RBAC: role and permission checks always run through the Prisma permission engine regardless of this flag.
Server-Side Enforcement
requirePermission()
requirePermission(req, permission, realm) is the primary RBAC guard for API routes. It is defined in lib/middleware/auth-middleware.ts. The signature requires three arguments and returns Promise<void> — it throws a NextResponse (401 or 403) on failure and returns nothing on success.
// Correct usage — await it; any failure throws a NextResponse automatically caught by Next.js
await requirePermission(req, 'manage:users', realm)
// Do NOT use a discriminated-union pattern — there is no return value to check
// const result = await requirePermission(...) // WRONGNote: lib/permissions/permission-checker.ts exports a separate function called verifyUserPermission() with a different signature. That function is not called by production route handlers and is a legacy/alternative implementation.
Authorization flows
requirePermission implements three distinct authorization paths, tried in order:
1. M2M token flow — if the JWT payload contains type: 'm2m', the request is authorized by scope strings in the token, not by any permission table. M2MTokenService.validateM2MToken() verifies the token, and the required permission must appear as a scope string. RBAC, role overrides, and TenantMembership are all skipped entirely. An invalid M2M token throws 401; a valid M2M token missing the scope throws 403.
2. Console flow — if the JWT was issued against the platform realm but the x-tenant header targets a different organization, permissions are resolved via TenantMembership. The user’s membership role (OWNER, ADMIN, or MEMBER) is mapped to a set of allowed permissions. The Prisma V2 RBAC tables (PermissionDefinition, RolePermission, UserPermissionOverride) are not consulted in this path.
3. End-user flow — if the JWT realm matches the target tenant, authorization runs through the Prisma V2 RBAC engine:
- Authenticate the request via
requireAuth()—401if the token is missing, malformed, or blacklisted. - Read the user’s roles directly from the verified JWT payload (
realm_access.roles). - Resolve role names to Keycloak UUIDs via
resolveRoleNamesToIds()— no live Keycloak API call for roles in the hot path. - Call
PermissionService.checkPermission(userId, roleIds, permission)which runs the Prisma permission engine. - Platform realm fallback — if RBAC denies and the target is the platform realm itself, fall back to
TenantMembershipas a safety net for OWNER/ADMIN users. - Throw
403if all checks failed.
requireAnyPermission(req, permissions[], realm) accepts an array — the first matching permission grants access. requireAllPermissions(req, permissions[], realm) requires every permission in the array.
Via the SDK (recommended for app developers)
If you are building an application on top of Auris rather than working inside the Auris API itself, use the @auris/nextjs SDK wrapper:
import { requirePermission } from '@auris/nextjs/server'
// In a Next.js route handler or Server Action
await requirePermission('view:invoices')The SDK handles token extraction, tenant resolution, and error responses. See the RBAC guide for the full SDK reference.
Client-Side Checks
Client-side permission checks are display guards only. They control what the UI renders — they do not enforce security. Always enforce permissions on the server.
The @auris/react SDK provides hooks for reading the current user’s permissions inside React components.
import { useCheckPermission, usePermissions, PermissionGate } from '@auris/react'
// Check a single permission
const canManageUsers = useCheckPermission('manage:users')
// Bulk — get all permissions
const { permissions, loading } = usePermissions()
// Declarative gate component
<PermissionGate permission="manage:users">
<UserManagementPanel />
</PermissionGate>useCheckPermission() returns true | false | null (null during the initial load). PermissionGate renders its children only when the permission is granted; it renders nothing (or a fallback via the fallback prop) otherwise.
RBAC vs FGA — Decision Guide
| Question | Use RBAC | Use FGA |
|---|---|---|
| Does the check apply to all instances of a resource type? | yes | no |
| Does the check depend on who owns a specific object? | no | yes |
| Does the check depend on sharing or delegation? | no | yes |
| Is the access pattern the same for every user in a role? | yes | no |
| Do you need to list all objects a user can access? | no | yes |
| Is this a global capability like “can create any invoice”? | yes | no |
A common misconception is that FGA replaces RBAC. It does not. RBAC gates capabilities at the type level (“this user class can perform this kind of action”). FGA gates access at the instance level (“this user can access this specific object”). The two-call pattern described above is the canonical way to use both together.
Use RBAC alone when:
- Access is uniform across all resource instances (e.g., “only admins can access the security settings”)
- You are protecting a system-level operation with no per-instance ownership concept
Add FGA when:
- Different users own different instances of the same resource type
- Resources can be shared with specific users or groups
- You need to delegate access without changing roles
Related Pages
- RBAC guide — permission string reference, role management, SDK API
- Fine-Grained Authorization (Zanzibar Model) — tuple model, DSL, check algorithm, debugger
- Multi-Tenancy — how tenants scope all roles, permissions, and FGA models
- Sessions — token lifecycle and how authentication feeds into authorization