# Authorization

From [Descope Fundamentals](https://learn.descope.io/c/foundations) — Learn the platform by configuring your own project — every module ends in work the app checks for real.

Take this course interactively at https://learn.descope.io/c/foundations/authorization

---

![Application-Side Enforcement. 3 minutes.](video:authorization-application-side-enforcement)

## What Descope handles

- Authentication across every method — OTP, passkeys, SSO, social login, and the rest
- Issuing and signing session and refresh JWTs your backend verifies cryptographically
- Carrying authorization data — roles, permissions, tenant membership, custom claims — embedded at issuance and refresh
- FGA relationship storage and traversal, resolving checks including inherited relations
- Session validation primitives: validateSession, validateRoles, validatePermissions
- Route-protection middleware for supported frameworks, such as the Next.js SDK's authMiddleware

## What your application must enforce

- Deciding what a role or permission actually allows for a specific route, button, or query
- Running the check at every layer that matters — a hidden button is not access control
- Data isolation between tenants: the JWT names the tenant, but nothing stops your queries crossing the boundary unless your code scopes every one by tenant ID
- Calling the FGA check at the right point before performing an action, and handling the negative case
- Calling token introspection when up-to-the-second state matters rather than the issuance-time snapshot

The mechanics of the Descope half are deliberately unremarkable. Backend SDKs exist for Node.js, Python, Go, Java, Ruby, PHP and .NET; you install one, initialize it with your Project ID, pull the session token off the Authorization header, and validate. If your project uses a custom domain, initialize with a matching baseUrl so validation resolves against the same origin your tokens were minted for.

> [!WARNING]
> Middleware that confirms a request is authenticated has not confirmed it is authorized. A valid session says who the user is, not that they may perform this action on this resource. The route still needs its own role, permission or FGA check — and the fact that this catches experienced teams is why it is worth stating twice.

The failure mode this lesson exists to prevent is specific and common: a cross-tenant data leak where the JWT was correct the whole time. Descope told the application which tenant the request belonged to; the application ran a query that did not filter on it. No amount of identity configuration prevents that, because it is a line of your data-access code.

## Key terms

- Authorization enforcement — interpreting a role, permission or relation and deciding whether to allow an action
- Route protection middleware — validates authentication before a request reaches a route
- Data isolation — ensuring one tenant's records are never returned to another, enforced by query logic
- Tenant-level user isolation — a project setting making the same login ID a separate identity per tenant

## Summary

- The dividing line between what Descope guarantees and what your code owes
- Which languages have backend SDKs, and why a custom domain changes initialization
- Why authenticated is not authorized, and what middleware does not do
- Where a cross-tenant leak actually lives, and why identity configuration cannot prevent it

---

![Custom Claims and JWT Templates. 3 minutes.](video:authorization-custom-claims-and-jwt-templates)

## Two places to shape claims

- JWT Templates (Project Settings → JWT Templates) — the preferred method for claims that should appear on every token a project or Inbound App issues. Good for consistent project-wide formatting, including matching third-party JWT formats.
- Custom Claims flow action — added inside a specific Flow, for claims that depend on flow context: conditional logic, connector responses, step-up authentication, or values collected mid-flow such as mapping IdP groups into the token after SSO.

Both support simple key/value entry or an advanced JSON mode for nested objects. If the same claim key is set in both places, the flow action wins for that run — useful when a JWT Template sets a sane default and one specific flow needs to override it. Values can be static strings, booleans, or numbers, or dynamic values pulled from user or tenant attributes, which refresh automatically when the underlying attribute changes and the session refreshes.

> [!NOTE]
> Design within the limits: each key up to 60 characters, each value up to 500 characters, and up to 100 keys per JWT. And never put PII or secrets in a claim — a JWT is only Base64-encoded, not encrypted.

A validated JWT tells you what was true the moment it was issued, not what is true now. If a role changed, a user was removed from a tenant, or an admin updated an attribute since the token was minted, a signature check will not reflect it until the token refreshes. Token introspection solves that by returning the user's live claims on demand, via the OIDC /userinfo endpoint.

Reach for introspection when roles, permissions, or tenant assignments may have changed since issuance; when you need custom attributes not baked into the JWT; or when you need the active tenant mid-session, for example after a switch_tenant action.

## Key terms

- JWT Template — a project-wide, Console-configured definition of claims included in every issued JWT.
- Custom Claims flow action — a Flow step adding claims from flow-specific context, overriding template values for that run.
- nsec claim — the claim housing client-added custom claims, signalling they are unverified and should not be trusted blindly.
- Dynamic claim value — pulled from a user or tenant attribute, auto-updating when the source changes.
- Token introspection — calling the OIDC /userinfo endpoint for live roles, permissions, and attributes.

![A JWT Template under Project settings, with the Custom Claims section open. Each claim is a key, a type, and a value: Dynamic resolves `user.email` or `tenant.name` at issue time, Static is a fixed value. The preview on the right updates as you add them, so you can see exactly what the token will carry. The left nav also shows where RBAC and FGA live — under Authorization, not at the top level.](authorization-custom-claims-and-jwt-templates)

## Summary

- Where a project-wide claim belongs versus a flow-specific one, and which wins on conflict
- Why client-supplied claims live under nsec rather than at the top level
- Why a revoked role can still appear in a live session, and what call returns the truth
- What must never go in a custom claim

---

![Fine-Grained Authorization. 4 minutes.](video:authorization-fine-grained-authorization)

## Implementing ReBAC is a three-step loop

- Define a schema — the entity types and possible relations for your app, written in Descope's DSL.
- Create relations — the real-world tuples, like "user-123 is owner of doc-1", built from your actual data.
- Check relations — at request time, ask Descope whether a given relation, direct or inherited, exists before allowing the action.

A folder-and-document hierarchy is the natural example. Define parent as a relation from a document to its folder, and a permission like can_edit that resolves through editor on the document or editor on its parent folder. Descope handles the traversal: you ask whether this user can_edit this document and get a boolean back.

Where ReBAC asks about relationships, ABAC asks about attributes — properties of the user, resource, action, or environment, evaluated at request time. "Allow access to financial reports only for users in Finance, during business hours, from a managed device" is an ABAC policy. It is more expressive than RBAC for contextual rules, but policies accumulate into a sprawl of if-then conditions that are hard to audit, which is why Descope frames ABAC as an extension layered onto ReBAC and RBAC rather than a replacement for either.

## When roles stop being enough

- Ownership matters more than role — two users share a role, but the one who created the resource should be able to delete it and the other should not.
- Self-serve sharing — users share a specific resource with specific people at a permission level they choose.
- Deep hierarchies and many stakeholder types — employees, contractors, and partners with resource-scoped rules hit role explosion fast.
- Contextual, environment-driven rules — time of day, device posture, department. That is an ABAC problem, not an RBAC one.

Teams rarely rip out RBAC to adopt FGA. The usual path is noticing scattered attribute checks bolted onto role logic, naming what is already implicit as an explicit relation — an owner relation instead of an ownerId comparison — and moving that logic into an FGA schema, validated in shadow mode alongside the old checks before cutting over.

## Key terms

- Fine-Grained Authorization (FGA) — Descope's authz service supporting ReBAC and ABAC.
- ReBAC — access decided by named relationships between targets and resources.
- ABAC — access decided by attributes of the user, resource, action, or environment.
- Schema — the definition of entity types and possible relations, written in Descope's DSL.
- Relation — a tuple connecting a target to a resource; direct or implied.
- Resource — a specific entity instance a relation or permission applies to, e.g. doc-42.

![The FGA schema editor, with a banking model on the left and the relations it describes drawn on the right. Each `type` declares relations to other types — an account has an owner and is `managed_by` a branch — and each `permission` resolves through them: `can_withdraw: owner | managed_by.manager` grants the branch manager without ever naming them. That traversal is what RBAC cannot express.](authorization-fine-grained-authorization)

## Summary

- Why RBAC cannot express per-instance access cleanly
- The difference between a direct and an implied relation
- The three steps to implementing ReBAC
- Which kinds of rule are naturally ABAC rather than ReBAC

---

![RBAC Fundamentals. 2 minutes.](video:authorization-rbac-fundamentals)

Authentication answers "who is this user?" Authorization answers "what are they allowed to do?" Role-Based Access Control is Descope's default answer to the second question: you define permissions — specific actions like documents:read or edit-product-pricing — group them into roles like Admin or Digital Store Supervisor, and assign roles to users. Every access decision reduces to one question: does this user's role include the permission this action requires?

Think of a keyring. A permission is a single key that opens one door; a role is the keyring itself. Instead of handing someone twelve individual keys you hand them the Manager keyring, already cut. When responsibilities change you re-cut the keyring rather than tracking down every key that person was ever given.

Permissions and roles are created under Authorization → RBAC in the Console, or via the Management SDKs and API. Every permission must belong to at least one role — you cannot assign a bare permission directly to a user — while roles can stand alone with no permissions, purely for organizational or programmatic grouping. A user can hold multiple roles, and their effective permissions are the union of everything those roles grant. Descope also ships two preconfigured roles: Tenant Admin, bundling SSO Admin, User Admin, and Impersonate; and a SCIM role that appears once a SCIM access key is created for a tenant.

After authentication, Descope embeds roles and permissions directly into the session JWT, so your app needs no separate lookup to know what a user can do. Project-level roles and permissions appear at the root of the token; tenant-level ones are nested inside each tenant's entry under the tenants claim.

> [!WARNING]
> Roles and permissions are baked into the JWT at issuance. If an admin grants a new role and the app still shows the old permissions, nothing is broken — the user needs a new session, via token refresh, before the change is reflected.

## Key terms

- Permission — a single, specific action a user can take, e.g. edit-product-pricing.
- Role — a named collection of permissions; a user can hold several.
- Project-level role — available to all users across every tenant in the project.
- Tenant-level role — defined within a single tenant and scoped only to it.
- Default role — automatically assigned to newly created users.
- Hidden role — concealed from Tenant Admins in Admin Widgets and SSO attribute mapping.

![Authorization then RBAC, with roles on the left and permissions on the right. A role is a named bundle of permissions and nothing else: Standard carries User, Premium carries User and Extra User, and the built-in Tenant Admin carries SSO Admin, User Admin and Impersonate. Permissions are created first, on the right, because a role has nothing to contain until they exist.](authorization-rbac-fundamentals)

## Summary

- That permissions belong to roles, not directly to users
- That the same user can hold different roles in different tenants, and where each appears in the JWT
- Why a role change is invisible until the token refreshes
- To use validateRoles / validatePermissions rather than parsing JWT claims by hand

---
