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 DENY override — if the user has an explicit DENY for this permission, deny immediately regardless of roles.
  2. User ALLOW override — if the user has an explicit ALLOW override (and no DENY), grant immediately.
  3. admin:all role bypass — if any of the user’s roles carries admin:all as an ALLOW, every permission is granted. (A user DENY override for admin:all itself can block this bypass.)
  4. Role evaluation — if ANY role has a DENY for this permission, deny. If ANY role has an ALLOW (and none DENY), grant.
  5. Default deny — if nothing matched, deny.

This means denies always win within a tier, and user-level overrides always win over role-level grants.

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. It can still be blocked by a user-level DENY override, making it possible to lock a specific user out of super-admin access without removing the role itself.

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? const result = await requirePermission(req, 'edit:documents') if (!result.success) return result.response // 2. FGA gate — does this user have access to THIS document? const access = await requireResourceAccess(req, result.user.id, 'editor', 'documents', documentId) if (!access.success) return access.response // Proceed with business logic

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 — throws 403 if the check fails

For the FGA model DSL, check algorithm, union/intersection/exclusion rewrites, and the console debugger, see Fine-Grained Authorization (Zanzibar Model).

The FGA engine is toggled by the FGA_ENGINE_ENABLED environment variable. When true, all checks run through the internal Zanzibar engine backed by the RelationshipTuple Prisma table. When false, the system falls back to Ory Keto via lib/permissions/keto-client.ts. New deployments should enable the internal engine; the Keto fallback exists as a migration path for tenants moving from an older Auris deployment.

Server-Side Enforcement

requirePermission()

requirePermission(req, permission) is the primary RBAC guard for API routes. It is defined in lib/permissions/permission-checker.ts and performs the full authentication + permission resolution pipeline in a single call:

  1. Extract the Bearer token from the Authorization header.
  2. Decode the JWT to get the Keycloak user ID (sub / user_id).
  3. Resolve the tenant name from the x-tenant header or the iss claim.
  4. Look up the Tenant record in Prisma — 404 if the tenant does not exist.
  5. Look up the TenantUser record — 404 if the user is not a member of this tenant.
  6. Fetch the user’s Keycloak roles via roleService.getUserRoles().
  7. Fetch role memberships from the authorization backend via Keto / FGA.
  8. Merge role names and permission strings into a unified permission set.
  9. Check whether the required permission is present — 403 if not.
  10. Return { success: true, user, roles, permissions }.

The return value is a discriminated union:

// Success — proceed const result = await requirePermission(req, 'manage:users') if (!result.success) return result.response // 401 or 403 const { user } = result // TenantUser record

requireAnyPermission(req, permissions[]) accepts an array — the first matching permission grants access.

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