SCIM best practices
User provisioning is hard, and there are many things you can get wrong if you do it in-house. We gathered some best practices on SCIM to help you with that.
When it comes to user provisioning, the most important best practice is automating it, and the protocol to use for that job is SCIM.
SCIM offers many benefits, like improved security with automatic provisioning, better user experience with user account pre-provisioning, and more accurate billing since seats no longer have to be tracked manually. Doing SCIM right, though, is no small feat. Different providers interpret and implement SCIM in slightly varied ways, and properly addressing these differences is not for the faint-hearted.
In this article, we will go through some best practices when it comes to implementing SCIM.
Address data fragmentation
SCIM is a spec, not a strictly implemented protocol, so various identity providers interpret and implement it in slightly varied ways. This introduces data fragmentation, which you should account for.
One example is a user’s email address. This field is traditionally used to identify users but is not mandatory in many providers. As a result, this can lead to events that have no owner.
Another common use case is adding custom attributes to a user profile in your application, like a user’s job title or profile picture. This is handled differently across identity providers:
- Entra ID (Azure) requires adding the proper schema extension as a prefix
urn:ietf:params:scim:schemas:extension:enterprise:2.0:User
orurn:ietf:params:scim:schemas:core:2.0:User
for the attribute to show up, and the attributes will always come in nested. - In Okta, attributes with
urn:ietf:params:scim:schemas:core:2.0:User
prefixes do show up as top-level attributes. - Rippling allows only a limited set of custom fields in SCIM applications.
The more providers you support, the worse these schema differences get.
Another problem with how different providers implement SCIM is uniquely identifying users. According to the SCIM spec, the externalId attribute should be the unique identifier for a user between the IdP and your application. This means that the externalId is your application's unique user identifier and is what the IdP will use when communicating information about this user to your app. Unfortunately, this is not a hard requirement. Some providers respect it, while others use permutations of a username and ID, which creates even more fragmentation.
Finally, another thing to account for to avoid problems is group membership. Every provider handles group memberships slightly differently. For example, imagine you go on a sabbatical and get promoted while you’re away from being an engineer to an engineering manager. You’d expect the identity provider to issue a request that says that you’re no longer part of the engineering group. However, IdPs handle this differently:
- Okta issues a request saying that you’re no longer part of the engineering group since you’re on sabbatical. So, when you come back, you may erroneously still have access to engineering resources.
- Entra ID (Azure) does send a request in this situation.
For more information on the challenges arising from different identity provider implementations, see SCIM challenges: navigating the idiosyncrasies of different providers.
Handle inactive users
Identity providers handle user deactivation differently. Some IdPs delete the user, others only suspend the user if they’re not part of a group, and others send a request to set the user as no longer active. Worse still, the user’s permissions and access rights may remain active when they should not, and these changes may not be communicated to your app.
For example, Entra ID (Azure) deletes users when they are deactivated using the DELETE endpoint, while Okta instead sends a PUT/PATCH request to set a user’s status to inactive. This behavior has a point since it can help with compliance. From Okta's point of view, it is more important to have the user data around so you can run an audit than to purge the user data by default. Considering your needs, you must decide how to handle inactive users in your app. The options are:
- Opt for a security-focused workflow and delete any user marked as inactive. This approach reduces the data footprint and minimizes potential security risks associated with unused accounts.
- Keep the users in the directory as inactive. This approach allows for user reactivation and ensures a comprehensive directory view.
There isn’t one correct choice here. It depends on your organization’s unique needs and security posture. For example, depending on industry regulations and compliance requirements, one option may align better with your organization’s obligations.
Make sure it scales
The SCIM RFC specifies that identity providers push data to your app, not the other way around. This opens up your app to major scale problems; a bad SCIM implementation or the onboarding of a large organization could flood you with a massive influx of requests in a few minutes and bring down your entire system. In addition, most IdPs do not support creating rate limits.
The RFC does not limit the maximum number of users that can belong to a group. This means a single REST API call (such as a POST or PUT request) on a Group entity could include hundreds of thousands of users. At scale, such large payloads can place significant stress on application servers and database resources. To manage these unpredictable events effectively, applications should be designed to gracefully handle spiky bursts in requests by scaling resources dynamically.
While the SCIM RFC recommends that applications return a 429 Too Many Requests status code when overwhelmed, many IdPs do not adhere to this guideline. This can lead to excessive request volumes that strain application resources, highlighting the need for robust rate-limiting and traffic management strategies.
It’s important that you design your system in a scalable manner. Ensure it can go from handling one request to thousands of requests without slowing down or crashing.
Handle out-of-sync events
There might (and probably will) be cases where the data between your app and your customer’s directories gets out of sync. This could be because of a bug that results in data loss. Ideally, you would have mechanisms in place that perform regular sync checks and data reconciliation. This is not feasible, though, since the SCIM spec doesn't cover data reconciliation, and there is no way to pull data from the identity provider using SCIM. What you can do instead is have documentation showing your users how to make sure that their identity provider is synchronized with your app.
Always keep in mind that data reconciliation can have side effects. Data modifications might trigger additional processes, update external systems, or affect the app’s overall state, e.g., sending an email or updating a third-party API on a user’s profile change. You should separate data handling from business logic to have control over what actions you want your app to take. This allows your app to replay events to sync data but bypass transactional logic, e.g., not sending out the same email twice.
Handle concurrent requests
Identity providers may sometimes send concurrent requests. If different requests to add and remove user John Doe from the Engineering group get issued simultaneously, your implementation should be prepared to solve the race condition.
Handling SCIM in a serial, reliable way is extremely important. However, this is difficult to accomplish in practice because you have no control over how information from an identity provider is pushed to you. You should be aware of these cases and be prepared to handle them.
Use TLS 1.2
As a RESTful API, the SCIM protocol is based on HTTP and does not define a SCIM-specific scheme for securing data. It relies on the Transport Layer Security (TLS) protocol to protect data as it moves between your customer’s identity provider and your SCIM server. TLS ensures that data is encrypted over the network, preventing eavesdropping and tampering with user data. SCIM requires using TLS 1.2 for all requests.
TLS should be combined with other authentication schemes, such as bearer tokens. All tokens should be validated. If not, you may process unauthorized SCIM requests and expose your customer’s data.
Secure authentication and authorization
SCIM doesn’t mandate a specific authentication mechanism but holds the service providers responsible for implementing the appropriate authentication measures to ensure that only authorized SCIM clients can access and manage identity resources. The spec recommends using industry-standard authentication protocols like OAuth 2.0.
The same goes for authorization. SCIM can be integrated with existing access control mechanisms like RBAC or ABAC. Service providers are responsible for ensuring that the identity providers have the right to perform specific operations.
Return detailed error messages
This will help your customers understand what went wrong during their request processing and simplify debugging. Some common error codes that SCIM providers use are:
401 Unauthorized
: For invalid authentication credentials.404 Not Found
: When a resource is not found (e.g. a user).409 Conflict
: When trying to create a resource that already exists.400 Bad Request
: When there is something wrong with the request body syntax.
Make sure to add sufficient details in the error message for each case.
Test rigorously
Test your implementation for interoperability and performance. Test it with various identity providers to ensure it works across providers and assess how it handles load spikes. Test all the CRUD operations, errors, and edge cases. You should cover as many cases as possible of user creation, updates, deactivation, and reactivation. Check for race conditions and stress test your implementation.
Focus on what’s important
While implementing SCIM in-house is feasible, it’s also very complicated, especially if you have to support multiple identity providers. Complexity can increase rapidly, and maintenance will certainly take way longer than you think. You will need a dedicated team that, besides the initial learning curve, will have to keep up with the fast-changing identity and user management landscape. This can kill early-stage startups.
Instead, you can outsource this work to providers that have already solved the problem, like WorkOS. With Directory Sync , you can get user provisioning up-and-running with just a few lines of code. It's one of the best SCIM connectors out there for a few reasons:
- Getting started is fast: With SDKs for every popular platform and Slack-based support, you can implement Directory Sync in minutes rather than weeks.
- Unified SCIM integration: WorkOS’s Directory Sync API integrates with over a dozen major identity providers, including Microsoft Entra, Okta, Workday, and any other SCIM-compliant directories. You can support any enterprise customer out of the box without creating custom connectors for each.
- Events-based processing: While webhooks are also supported, WorkOS’s Events API means every SCIM request is processed in order and in real-time. You’ll never miss a provisioning request again.
- Support for custom attributes: Many enterprise customers need custom attributes beyond basic user profiles — such as employee numbers, departments, and cost centers. WorkOS allows you to add custom attributes specific to each organization’s needs.
- Simplified onboarding: WorkOS’s Admin Portal removes the pain of onboarding your customers’ IT teams and configuring your app to work with their identity provider. Your customers can independently set up and configure Directory Sync without requiring extensive back-and-forth with your team.
- Pricing that makes sense: Unlike competitors who price by monthly active users, WorkOS charges a flat rate for each company you onboard — whether they’re syncing 10 or 10,000 users with your app.
To learn more about the challenges SCIM poses, see the following: