Agen.co enables organizations to securely expose enterprise context to internal agents, copilots, and AI workflows through an identity-aware control layer that governs access, reduces risk, and centralizes oversight.
A low-code CIAM platform for managing customer identity as you scale.
Empower your workforce with secure agents
RBAC works well when roles stay understandable, assignments are scoped to the correct tenant, and every protected operation is tested against explicit allow and deny rules. It fails when teams treat a role name as sufficient proof of access, let exceptions become permanent roles, or enforce permissions in only part of the application.
This guide focuses on implementing and governing role-based access control in B2B SaaS. For the underlying model and terminology, start with the complete RBAC guide. For the wider request-to-decision-to-enforcement flow, see the Authorization guide.
In multi-tenant SaaS, a user’s role is meaningful only within a specific customer context. The same identity might be an administrator for Tenant A and a viewer for Tenant B. An authorization check therefore needs more than role = admin: it needs the active tenant, the role assignment for that tenant, the requested action, and the tenant that owns the resource.
role = admin
Keep these concepts separate:
A reusable editor role can exist across the application, but Alice’s assignment as editor in Tenant A must not grant the same access in Tenant B. Some systems also let each tenant define custom roles. Either way, the assignment and the resource check need a tenant boundary.
editor
Start with application actions, not an organization chart. A useful permission describes an operation on a resource, such as invoice.read, invoice.refund, project.export or member.invite. Roles then group permissions for a coherent job or product responsibility. The separate user roles and permissions guide covers the underlying relationship in more detail.
invoice.read
invoice.refund
project.export
member.invite
If a permission is too broad, every role that contains it inherits that ambiguity. If it is tied to a screen label, it may break when the interface changes even though the underlying action is the same. Prefer stable resource-and-action keys that can be enforced consistently in the UI, API, jobs and tests.
For each permission, record:
This is also where teams should distinguish permissions from entitlements. A role might authorize a support agent to use an export function, while a subscription or feature entitlement determines whether the customer’s plan includes that function at all. Those are separate decisions even when the application evaluates both before proceeding.
Never model a B2B SaaS user as having one global application role unless the role is intentionally provider-wide. Store or resolve the assignment as a relationship among identity, tenant and role, then verify that the resource belongs to the active tenant before applying the role’s permissions.
For an object action, a safe decision contract is:
ALLOW only when: membership(user, active_tenant) is valid AND resource.tenant_id == active_tenant AND permission(required_action) is present in a role assigned in active_tenant
Do not accept an untrusted tenant ID from a route or request body as sufficient context. Resolve tenant context through a trusted session or server-side mechanism, then compare it with the requested resource. If the application supports account switching, the switch must establish a new active context before permissions for the other account are used.
This boundary must also apply to list and aggregate operations. Search, count, export, facets and pagination can leak foreign-tenant information even when individual detail routes are protected. Apply tenant and authorization scope before results, totals or cursors leave storage.
Self-service role administration is valuable in B2B SaaS because customers can reflect their own operating model without asking the vendor to hard-code every variation. The SaaS provider should still define the maximum permission boundary.
Classify permissions into three practical groups:
Also separate role definition from role-management authority. The permission to use billing features is different from the permission to create a role that grants billing access. Limit who can create, modify, assign and delete roles, and ensure administrators cannot create a role stronger than the boundary the provider allows.
Frontegg uses this pattern for custom roles: permissions can be classified as Never, Assignable or Always. Customers can create account roles in the self-service portal from the permissions the application provider exposes. The exact configuration belongs in the Frontegg Roles documentation, not in this conceptual guide. The article on self-service role configuration explains why a provider might delegate that administration to its customers.
Never
Assignable
Always
NIST defines least privilege as restricting access to the minimum needed for assigned tasks. In RBAC, that requires controlling both sides of the model: which permissions a role contains and which users receive the role. A neatly named role can still be overprivileged.
Review high-impact permissions first:
Then identify permission combinations that should not belong to one person or active session. The NIST RBAC model treats static and dynamic separation of duty as explicit constraints. A payment workflow, for example, may require the person who creates a payout to be different from the person who approves it.
Do not implement separation of duties as a note in a spreadsheet. Encode the constraint in policy, assignment validation or workflow enforcement, and add a test showing that the forbidden combination is rejected.
Role explosion occurs when every customer, department, exception or resource variation becomes a new role. The result is a catalog of near-duplicates that nobody can review confidently.
Use these controls:
For example, editor_europe, editor_us and editor_contractors are often a sign that location and employment type are being forced into role names. Keep editor as the job-function role and evaluate the additional condition with an appropriate attribute or policy rule.
editor_europe
editor_us
editor_contractors
RBAC should remain the stable, understandable layer. When access depends on ownership, project membership, resource state or live context, compare RBAC with ABAC or a relationship-based approach rather than multiplying roles.
A role is a living policy object, not a one-time configuration. Define how it is requested, approved, assigned, changed, reviewed, deprecated and removed before the first production rollout.
Use a specification like this for every role:
billing_analyst
refund.prepare
refund.approve
role.manage
Integrate assignments with the systems that know when people join, move or leave. SSO assertions, directory groups and SCIM provisioning can help, but map their behavior precisely. For example, the Frontegg SCIM and SSO role-assignment documentation explains that SCIM synchronizes users and groups, while Frontegg roles are derived through default roles and group-to-role mappings rather than being universally assigned directly by SCIM.
For temporary roles, store an expiry condition and test what happens when it passes. Before deleting a role, define how its assignments will be migrated.
The OWASP Authorization Cheat Sheet recommends denying by default and validating permissions on every request. In practice, that means putting enforcement in a shared layer that every protected route and operation must traverse, not relying on developers to remember a custom if statement.
if
Cover more than visible screens:
The UI may hide actions that a user cannot perform, but UI state is not an authorization control. The backend must make the final decision using trusted identity, tenant and resource context.
Keep the enforcement call expressed in permissions rather than role names. requirePermission("invoice.refund") survives role redesign better than if role == "billing_admin", because multiple roles may legitimately contain the same permission.
requirePermission("invoice.refund")
if role == "billing_admin"
Role and permission changes must have a documented freshness contract. Identify every place authorization data can persist: access tokens, refresh tokens, application sessions, SDK caches, policy caches and replicated stores. Then define when each one observes a change.
For each change type, answer:
Do not publish “instant revocation” unless the complete path has been verified. Frontegg’s current Roles documentation states that role changes reflected in JWT permissions take effect after the current token expires and a new token is issued, and recommends configuring token lifetime with that risk in mind. Applications must design their own enforcement and resource-isolation behavior around the verified token and session contract.
An RBAC test suite is incomplete if it only proves that an administrator can perform administrative actions. Every allowed case should have a corresponding denied case, and every tenant-scoped operation should have a cross-tenant test.
Make the matrix machine-readable and run it in CI where possible. Include object, list, bulk and indirect identifiers. A failure should show the subject, active tenant, action, resource tenant and expected decision without exposing sensitive data. OWASP’s Multi-Tenant Security Cheat Sheet likewise calls for testing the authorization matrix and preventing cross-tenant object access.
Testing staging personas manually is useful for usability, but it cannot replace deterministic authorization tests. The system needs to prove that a missing check, guessed resource ID or mismatched tenant produces a denial.
Review the role catalog and user assignments on different cadences. A role-definition review asks whether the permission bundle is still coherent. An assignment review asks whether a particular identity still needs that role in that tenant.
Prioritize reviews after:
Track unused roles, roles with no owner, duplicate permission sets, high-risk combinations and assignments that lack a current justification. Deprecate before deleting: stop new assignments, migrate existing users, run the negative-test suite, observe the rollout and keep a rollback path.
The ten practices fit into six delivery stages:
Roll out one bounded product area first. Choose a workflow with meaningful permissions but manageable risk, migrate a small group, compare expected and actual decisions, and test denial paths before expanding. A big-bang migration makes it difficult to distinguish a bad role definition from a missed enforcement point or stale assignment.
Frontegg separates roles from permissions and supports both provider-managed and customer-managed role workflows. Current documentation confirms that:
Use the Roles documentation and Permissions documentation for the exact configuration and API behavior. For the broader architecture and responsibility boundary, return to the Authorization guide. To evaluate centralized RBAC, ReBAC and ABAC capabilities commercially, see Frontegg Authorization + Entitlements.
The application remains responsible for validating trusted tenant context, checking resource ownership and enforcing the decision at every protected operation. Treat the identity provider’s role and permission data as one input to that complete authorization path, not as a substitute for resource isolation.
Before releasing an RBAC change, confirm that:
RBAC remains useful because it gives teams and customers an understandable vocabulary for access. Its value depends on the boundaries and operating discipline around those roles. A small, tenant-aware, permission-driven model with negative tests is safer and easier to evolve than a large catalog of role names that the application interprets inconsistently.