Skip to Content
ConceptsAuthorization

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.

LayerMechanismGranularityTypical question answered
1 — AuthenticationKeycloak JWTPer-tenant user identity”Is this user authenticated and do they belong to this tenant?“
2 — RBACPermission strings on rolesPer-action-type”Does this user have the general right to perform this kind of action?“
3 — FGAZanzibar relationship tuplesPer-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 strings
  • RolePermission — which permissions are assigned to a role, with an effect of ALLOW or DENY
  • UserPermissionOverride — per-user permission grants or denials that bypass role evaluation, with an optional expiresAt for time-boxed grants

Resolution order (Discord-style priority):

  1. User override — if the user has any active (non-expired) UserPermissionOverride for 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.
  2. admin:all bypass — if any of the user’s roles carries admin:all as an ALLOW (and no role denies it), every other permission is granted. A user-level ALLOW override on admin:all also triggers this bypass. Importantly, a user-level DENY on admin:all does not block individual permissions that are explicitly ALLOWed at the role level for that permission — the admin:all DENY only prevents the super-admin bypass path, not individual permission grants.
  3. Role DENY wins — if any role has a DENY for this permission, deny.
  4. Role ALLOW — if any role has an ALLOW (and none deny), grant.
  5. 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_xyz456

That 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:all

Built-in permission categories

CategoryExample strings
Super adminadmin:all
Rolesmanage:roles, view:roles, assign:roles
Usersmanage:users, view:users
Permissionsmanage:permissions, view:permissions
Auditview:audit, export:audit, view:audit_logs
Tenantmanage:tenant, view:tenant
Securitymanage:security, view:security
Systemmanage:system, view:system
Contentmanage:content, collaborate:content, share:content
User datamanage:user_data, export:user_data
Integrationsmanage:integrations, view:integrations
Marketplaceview:marketplace, manage:marketplace, install:integrations, developer:marketplace
Sessionsmanage:sessions
SSOmanage:sso_connections, view:sso_connections
SCIMmanage:scim_connections, view:scim_connections, view:scim_logs
Identity providersmanage:identity_providers, view:identity_providers
Members and teamsmanage:members, view:members
Consoleconsole:access
FGAview:fga_models, manage:fga_models, view:fga_tuples, manage:fga_tuples, debug:fga
Licensingmanage:license-policies, manage:license-keys, view:license-stats
AIai: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 billingview: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 user

For 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:

RelationMeaning
ownerFull control — read, write, share, delete
editorRead and write
viewerRead only
sharedCustom, application-defined access

The standard namespaces correspond to core platform resource types:

documents domains vps calendars channels tickets

Your 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 logic

Both 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:

FunctionPurpose
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(...) // WRONG

Note: 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:

  1. Authenticate the request via requireAuth()401 if the token is missing, malformed, or blacklisted.
  2. Read the user’s roles directly from the verified JWT payload (realm_access.roles).
  3. Resolve role names to Keycloak UUIDs via resolveRoleNamesToIds() — no live Keycloak API call for roles in the hot path.
  4. Call PermissionService.checkPermission(userId, roleIds, permission) which runs the Prisma permission engine.
  5. Platform realm fallback — if RBAC denies and the target is the platform realm itself, fall back to TenantMembership as a safety net for OWNER/ADMIN users.
  6. Throw 403 if 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.

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

QuestionUse RBACUse FGA
Does the check apply to all instances of a resource type?yesno
Does the check depend on who owns a specific object?noyes
Does the check depend on sharing or delegation?noyes
Is the access pattern the same for every user in a role?yesno
Do you need to list all objects a user can access?noyes
Is this a global capability like “can create any invoice”?yesno

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