RBAC & Permissions on Access Tokens

Role → permission → permissionsclaim · a real allow/deny decision, not just a session check

Role → Permission → Token → API Decision

Role

reports-editor-demo / reports-viewer-demo

Permissions

read:reports, write:reports (attached to the role)

RBAC on

Access Token

permissions claim, real & signed

1A resource server with both Enable RBAC and Add Permissions in the Access Token turned on mints a JWT access token carrying a real permissions array, not an opaque token.
2This app's own API routes decode that token and check permissions.includes('read:reports') before returning data — most protected routes in this demo suite only check that a session exists, this is the one that actually checks the claim.
3Switch the org context below: the same user has reports-editor-demo assigned tenant-wide, but only reports-viewer-demo assigned inside this demo's Organization — a deliberately different role, on purpose.
4Tenant-level and org-level role assignments are separate Auth0 objects. Logging in with organization=... only evaluates org-scoped role assignments for the permissions claim — the tenant-wide role has no effect on it.

A trap, not a feature

It is easy to assume a role assigned tenant-wide "just works" once Organizations gets added to a login flow. It doesn't — the org-context permissions below are visibly narrower than the tenant-context ones for the exact same user, because the org-scoped role (reports-viewer-demo) genuinely only has read:reports attached, unlike the tenant-wide role (reports-editor-demo).

Auth0 blog — 5 Reasons Why Your Permissions Are Not in the Token.

Demo login

This demo needs a specific, pre-built role setup to show the contrast below — a tenant-wide role that differs on purpose from an org-scoped one, on the same person — so it isn't self-serve for a brand-new account the way most of this suite's demos are. Sign in with the shared test account:

email
rbac-demo@atko.email
password
RbacDemo!2026#Zx

That account has reports-editor-demo (read + write) assigned tenant-wide, and only reports-viewer-demo (read only) assigned inside this demo's pre-provisioned Organization — the two scenarios below only make sense with that specific mismatch already in place.

Log in to try it →
Security Implications

Strengths

  • The permissions claim on the access token is signed and computed server-side by Auth0 at token-mint time from the actual role assignments in effect for that login — an API checking it isn't trusting anything the client asserted about itself.
  • RBAC + Organizations composes cleanly at the data-model level: org-scoped role assignments are a distinct, first-class object from tenant-wide ones, not a bolt-on filter over the same role list.

Risks & Considerations

  • Enabling RBAC on a resource server is two separate toggles ("Enable RBAC" and "Add Permissions in the Access Token") — either alone silently produces a token with no permissions claim at all, which looks identical to "user has no permissions" rather than "misconfigured resource server."
  • Tenant-level and organization-level role assignments are separate Auth0 objects. Assigning a role tenant-wide has no effect on what permissions show up when that same user logs in with an organization parameter — a real, confirmed trap (not a hypothetical one) when a demo tenant or a real deployment already has global RBAC roles set up and someone assumes those carry over into an org context.
  • A role with no permissions attached still "successfully" assigns — there is no error at role-assignment time to warn that the resulting token will carry an empty permissions array for that role.

Hardening Checklist

  • Check both RBAC toggles are on together when a permissions claim is unexpectedly missing — don't assume one implies the other.
  • When debugging org-context permissions, check the role assignment at organizations/{id}/members/{user}/roles specifically, not just the user's tenant-wide roles endpoint — they are genuinely different data.
  • Treat "delete:reports" (or any permission nobody currently holds) as a deliberate negative test case in any RBAC-gated API, the same way this demo does — confirming the deny path actually denies is as important as confirming the allow path allows.