How to implement RBAC authorization in Python APIs with WorkOS
Set up roles and permissions, verify session JWTs, and protect your FastAPI routes with dependency injection.
Role-based access control (RBAC) is one of those problems that looks solved until it isn't. Most tutorials show you how to check a role and return a 403. What they skip is the architecture that keeps that logic from rotting quietly across hundreds of endpoints over time.
This tutorial uses FastAPI and WorkOS to build RBAC the way it should work in production: a verified JWT carries claims into your API, a thin enforcement layer asks "is this allowed?", and a centralized policy layer answers. WorkOS handles the hard parts of managing roles, permissions, and org-scoped assignments; your app just enforces what it's told.
By the end, you'll have a multi-tenant API where:
- Routes are protected using FastAPI dependency injection.
- Permissions are checked against WorkOS-issued session tokens.
- Org-scoped roles let different tenants define their own access levels without touching your code.
Prerequisites
- Python 3.10 or later
- A WorkOS account (free up to 1 million monthly active users)
- Basic familiarity with FastAPI and async Python
Install dependencies:
Part 1: Setting up WorkOS RBAC
1.1 Install the WorkOS CLI
You can create and manage roles and permissions through the WorkOS dashboard or the API, but the CLI is faster and gives you something you can commit to version control. Install it with:
Then authenticate:
1.2 Define permissions
Permissions in WorkOS are immutable slugs that represent specific actions in your application. Think of them as the atoms of your authorization model. For a typical content API, you might define:
Create them from the terminal:
Or verify what's already provisioned:
1.3 Define roles
Roles are bundles of permissions. Create them and assign permissions to each:
WorkOS lets you set a default role (usually viewer) that every new org member gets automatically.
1.4 Declarative provisioning with seed
For team environments or CI pipelines, the CLI's seed command lets you define your entire permissions and roles model in a YAML file and provision it in one command. This is worth setting up early: it gives you a single file that documents your authorization model and can be checked into version control.
The CLI tracks state so you can tear everything down cleanly with --clean if you need to reset a sandbox environment.
1.5 Understand org-scoped roles
This is where WorkOS earns its place in B2B APIs. Your default roles apply across all tenants, but any customer organization can create its own custom roles scoped to their membership. A "Finance Admin" in Org A has no effect on Org B. Your code doesn't need to know about any of this: the correct roles and permissions for a given user-in-org combination are embedded in their session JWT. You just read them.
1.6 Configure your environment
Part 2: The enforcement layer
The enforcement layer is a thin piece of code that does one thing: asks "is this allowed?" and raises an exception if not. In FastAPI, this maps naturally onto dependency injection: you define a dependency that verifies a token and extracts claims, then inject it into any route you want to protect. The result is declarative, testable, and easy to audit. It should also stay stable even as your policy evolves from RBAC toward ABAC (attribute-based access control) or relationship-based models later.
2.1 Token verification
Authorization decisions are only as good as their inputs, which means token verification has to come first. Reading claims from an unverified token is one of the most common and quietest authorization bugs in production APIs. Always validate fully before trusting anything in the payload.
Create a auth.py module that verifies the WorkOS session token and extracts claims:
Notice what this function does not do: it does not make an authorization decision. It only answers "is this token valid, and who does it belong to?" Authorization is a separate concern.
2.2 The permission checker
Create an authorize.py module that houses the policy layer:
This is the key architectural move: enforcement is a factory that returns a dependency. Your routes never touch role names or permission strings directly in business logic; they declare what they need and the dependency system handles the rest.
Part 3: Protecting routes
With the enforcement layer in place, protecting a route is a single line:
The pattern is explicit and auditable. Every protected route states its required permission. A code reviewer can scan the file and immediately understand the access model without reading any middleware or checking any other file.
Part 4: Org-scoped enforcement
For multi-tenant APIs, you often need to ensure that a user can only act within their own organization, not just check that they have the right permission. Add a tenant guard to the authorize module:
Now a user with write:document in Org A cannot write documents in Org B, even if they somehow obtain a valid token for the wrong org. The org boundary is enforced at the policy layer, not scattered across individual handlers.
Part 5: Assigning roles via the WorkOS API
WorkOS manages role assignments through organization memberships. When a user joins an org, they receive the default role. To elevate them, call the organization memberships API:
Role changes are reflected in the next session token the user receives. For immediate enforcement, you can revoke the current session and force re-authentication, or design your app to re-fetch the session on short intervals.
The API is one of several ways to assign roles in WorkOS. Depending on your setup, you might use:
- Directory sync: role assignments can be driven automatically from a customer's corporate directory (Okta, Azure AD, Google Workspace). When a user's group membership changes in the IdP, their WorkOS role updates to match.
- SSO group mapping: for standalone SSO, organization administrators can map IdP groups to roles directly, so role assignment is handled by the customer's IT team rather than your application.
- WorkOS dashboard: useful for manual assignments during development or for support workflows where an admin needs to adjust a specific user's access without writing code.
- WorkOS CLI:
workos role assignworks well for scripted provisioning or seeding test environments.
Part 6: Common mistakes to avoid
- Checking roles instead of permissions. Checking
if session["role"] == "admin"couples your code to role names. When you add a "Super Admin" role later, you have to hunt down every== "admin"check in the codebase. Check permissions instead; they're stable. - Authorizing off unverified tokens. Always run the full token verification before reading any claims. A token that passes signature verification but has the wrong audience or issuer can still carry forged claims.
- Mixing authorization with business logic. Keep
require_permissionin your dependency chain, not inside handler functions. The moment you writeif "delete:document" not in session["permissions"]inside a route body, you've broken the audit trail. - Assuming all users have the same roles across orgs. A user can be an admin in one org and a viewer in another. The session JWT is scoped to the current org context. Always read org-scoped claims, not global user-level roles.
Part 7: Testing your authorization layer
Test authorization in isolation by mocking the get_session dependency. This lets you verify that your routes return the right status codes for every permission combination without touching the WorkOS API:
Write one test per permission boundary. If a future developer accidentally removes a require_permission dependency, the test suite catches it immediately.
When RBAC isn't enough: Fine-grained authorization
The pattern in this tutorial handles the majority of B2B access control cases well. But as your product grows, you'll likely hit scenarios that tenant-wide roles can't express cleanly: a user who is an editor in one workspace but a viewer in another, a project that inherits permissions from a parent organization, or a customer who needs to grant access to a specific resource without touching anything else.
That's where WorkOS FGA (Fine-Grained Authorization) comes in. FGA extends the RBAC system you've already set up here, keeping the same mental model of roles, permissions, and assignments, but making resources and their hierarchy first-class. A workspace admin automatically gets access to all projects and apps within that workspace without needing separate role assignments at each level. Permissions flow down the hierarchy; you assign a role once.
The adoption path is incremental and requires no migration. Your existing roles and org memberships keep working exactly as they do today. You start by defining resource types in the WorkOS dashboard to mirror your product structure, register resource instances as they're created at runtime, and introduce resource-scoped roles like workspace-admin or project-editor wherever you need them. The enforcement model in your FastAPI app stays largely the same: for org-wide checks, you continue reading from the session JWT; for resource-scoped checks, you call the WorkOS Authorization API.
If you anticipate complex, nested resource structures down the line, it's worth reading the FGA documentation before your authorization model gets too entrenched.
Wrapping up
The pattern you've built here separates three things that most implementations collapse into one:
- Verification (did WorkOS issue this token, and is it still valid?)
- Enforcement (does this route require a specific permission?)
- Policy (what permissions does this user-in-org have?)
WorkOS owns layer three entirely. Your app only has to handle layers one and two, and FastAPI's dependency system makes both of those clean and auditable.
As your authorization model grows, you can evolve the policy layer toward attribute-based rules or relationship checks without touching the enforcement code or the route definitions. The enforcement layer stays stable because it only asks "is this allowed?" It never needs to know why.
Ready to add RBAC to your Python API? Sign up for WorkOS and get started for free. No credit card required, up to 1 million monthly active users for free.