The Developer’s Guide to RBAC and IdPs: Part II
When building authorization for enterprise customers, supporting IdP role mapping is a challenging yet important task. This allows organizations to manage their roles and permissions through a single source of truth, the IdP, rather than dealing with unique permissions schemes for each SaaS tool.
So you’ve read Part I of our RBAC series, gotten a basic handle on RBAC and FGA, and think you’re ready to build enterprise-ready authorization into your app. Think again my friend! Most enterprises (and increasingly, smaller companies too) use an Identity Provider (IdP) like Okta to manage their internal user data. To support their needs, your authorization system will need to sync with and pull data from these platforms; and their APIs can be arcane, disorganized, and full of edge cases. This post will walk through how IdP integrations work with authorization and things to watch out for when building your own.
The basic concept of syncing with an IdP
The easiest way to think about the difference between regular authorization and IdP-based authorization is to consider the source:
- For run of the mill authorization systems, the data source for roles, resources, and permissions are generated inside your app by your users
- For IdP-based authorization systems, the data source for roles, resources, and permissions is an external data store (Okta, Azure AD, etc.)
Enterprises want to manage their organization’s roles and permissions from a single place instead of having to deal with tens (or hundreds) of different SaaS tools and their unique permissions schemes. So they designate different roles and groups in an IdP like Okta for each employee. An engineer might be in the engineering group, while a team lead for engineering might be in both the engineering group and the admin group.
![](https://cdn.prod.website-files.com/621f84dc15b5ed16dc85a18a/66996b4613dc07cac023a00a_Okta%20dashboard.png)
Here’s where you come in: your app needs to be able to pull that user data from Okta and then map it to the relevant roles (or resources, if you’re on the FGA track) that exist in your scheme. For example, you might want everyone in the engineering group at a customer of yours to have view-only access to resources in your product, while admins have write access. We’ll talk more about this mapping layer later, since you will likely need to build a custom UI for it down the road.
IdP syncing and SCIM: push, not pull
IdP syncing does not work the way you’d expect it to. Instead of publishing an API that you can poll on a regular basis, most IdPs actually push data to you when they want (and sometimes the timing can be wonky). Putting yourself in the shoes of an IT admin for a brief moment, the process to integrate a new application (yours) looks something like this:
- You create a new application in Okta and name it BitHub
- You assign the relevant users who should have access to BitHub (either directly, or using groups)
- This can be very un-fun for IT admins. Because if you’re a huge company, but only a few people need access to a new app, you either need to assign them directly or create an entire new group just for this app called something like “BitHub users.”
- You get a unique URL and key from BitHub and give it to Okta. This is how Okta knows where to send data
Okta then starts publishing information to BitHub. There’s usually an initial sync, and then subsequent updates when things change on Okta’s end (a group update, a last name change, etc.). You’re at the mercy of the IdP now and when they decide to publish data: you cannot just query an endpoint and get the information you need as you please. Because of this, you also need to store all of this data on your own immediately once you get it, so your app can have persistent permissions that don’t rely on a third party for every check.
The most ubiquitous protocol for handling this sync – and the culprit for why the data sync flow is so odd – is called the System for Cross-Domain Identity Management, or SCIM for short. It’s a specification for a hierarchy of users and groups, and (for better or worse) is the standard for how IdPs like Okta communicate group information to apps. An example of a SCIM object might look like this (from their docs):
Read more in our guide to SCIM and directory sync.
Another fun thing is that every IdP interprets SCIM slightly differently. A good example is group changes during deactivation. Imagine someone on your team goes on parental leave, and so IT deactivates their accounts temporarily. While they’re on leave, someone changes their group from engineering to management.
- Okta doesn’t tell your app anything: because the user is deactivated, the change isn’t relevant. When the user gets reactivated, they just tell you that they’re now in the management group.
- Azure does tell your app that the user was removed from the engineering group once they’re reactivated.
When your teammate gets back, they’re now in a different group – but for whatever reason, Okta still doesn’t send any notification to your app that this user is no longer a part of engineering, just that they’re reactivated and now in management. This is a clear security flaw, because now this user will still have access to engineering resources that they shouldn’t have access to.
What all of this inevitably means is that teams end up bifurcating their application logic for different IdPs: your code will need to do something like “if they’re using Okta, do this, if they’re using Azure, do something else.”
And a final wrench: SCIM is not the only way to handle authorization syncs. We’ve written previously about SAML, the standard protocol for handling SSO. SAML is the protocol that tells applications who you are and why you should have access to a particular tool; but some companies will actually use it for authorization too. Stripe, for example, doesn’t support SCIM and instead requires enterprises to embed role and group information in SAML responses. But there’s a major vulnerability here too: you only get updates to group information when a user authenticates.
The TL;DR on all of this: if you look closely enough you can see apps out there that implement all possible permutations of SCIM, groups, attributes, and SAML. It’s kind of the wild west because of how difficult it is to build all of this stuff. Supporting just one IdP across all of these modalities is likely to be at least a month of engineering time to do it well.
Creating a mapping layer to your permissions
![](https://cdn.prod.website-files.com/621f84dc15b5ed16dc85a18a/66996b5f466158ceee32d7b0_WorkOS%20dashboard.png)
A lot of the complexity in building IdP-based permissions is in mapping the information in the IdP to your unique set of permissions. There’s no way to do this automatically, you’ll need to build a UI that allows IT admins to manually map their IdP’s groups to your app’s permissions and roles (or resources). It might look something like this:
- Show all the groups coming in from the IdP on the left side
- Show all of the available roles or resources on the right side
- Allow the admin to connect the two
Back to our BitHub example, our customer’s IdP might tell us that a user is in an engineering group, and another user is in an admin group. What does this mean for our app though, which has a creator, viewer, and admin role? We’d need to build a UI that allows an IT admin to say that everyone in the engineering group should be a default viewer, everyone in the admin group should be a default admin, etc.
It’s worth noting that in some cases, companies will try to avoid this by requiring IT admins to the lengths of creating custom roles in the IdP for your app’s roles. This would mean attaching an attribute to every user in the IdP that says something like “BitHub – admin” or “BitHub – viewer.” And in this scenario, you don’t need to build a mapping layer at all. This is how Loom’s SCIM integration works.
In some (rare) cases, IT admins will prefer this attribute-based approach. And then you’ll need to bifurcate your application logic yet again to handle both customers that want a mapping layer and customers who don’t. But this attribute-based approach is extremely brittle: if BitHub ever wants to make changes to their roles or permissions, then the customer’s IT admin needs to go make the according changes on their end (which is almost impossible). This is exactly why building for SCIM is so fragmented.
FGA and IdP roles
We’ve been awfully quiet about FGA up until this point: that’s because the fundamental concept of resource-based authorization doesn’t really work well with IdP-based authorization at all. FGA is dynamic: I just created a new Figma file, I just built a new Hubspot contact list, and here’s a project ID. There’s no way an IT admin would be able to interpret all of these and apply the appropriate groups and roles.
For example, BitHub might want someone to be an owner (delete and access management rights) for repository A, but only a viewer for repository B. But as a whole, in the BitHub app overall, they’re only a viewer. You can create a sort of hybrid system where your general user permissions are synced from the IdP, but you can have resource-level exceptions; this information exists only in your tool, not the IdP.
There basically are no shared concepts between your app and the IdP in a pure FGA scenario; SCIM exists in a role-based universe. Having said that, we have seen teams try to force this by requiring customers to input actual resource IDs in an IdP so that data can get sent to an app. Imagine each repository in BitHub has a resource ID, and you ask your customer’s IT admin to add (for each user) this resource ID plus a relevant role (creator, viewer) in a string, and then your app consumes this data and sets up the appropriate permissions. Don’t do this!
We are guessing that things will go towards a hybrid RBAC/FGA direction in the future. A few best practices for setting up the FGA/role architecture on your end to make this as smooth as possible:
1. Design your global, static roles around IdP needs
IT admins will care the most about high level org roles like admin, and how well they can automate syncing those from the IdP to your app. Make sure those sync well independent of lower level application roles (like viewer, contributor, etc.).
2. Design your resource level roles around application needs
IT admins are much more concerned with high level org roles than resource level stuff like whether someone is a viewer or contributor on a specific repository. The persona you’re designing for here is actually the application user, not the IT admin – it’s OK to not expect these kinds of roles to sync well from the IdP.
3. Build an intuitive hierarchy between (1) and (2)
Global, org-level roles like admin should automatically have default resource level roles and permissions. For example, an “admin” at the org level (which you sync through an IdP) might be a default “owner” of every repository, while “member” at the org level is a default “viewer” of all repositories.