Your B2B SaaS already supports SSO. Enterprise prospects log in with Azure Entra ID or Auth0, corporate credentials and all. And then their IT admin still has to create every single user account by hand inside your application, assign each one to the right groups, and remember to remove them again when someone leaves. That gap is where SCIM comes in.
SSO answers the question "who is this person?". SCIM answers a different one: "should this person have an account at all, and is it still up to date?" SSO is authentication. SCIM is the whole user lifecycle. Enterprise buyers expect both, and they will ask for both on the same vendor security assessment.
What is SCIM provisioning?
SCIM stands for System for Cross-domain Identity Management. It is an open standard for automating user provisioning. In practice it defines a REST API that identity providers like Azure Entra ID and Auth0 call to create, update, and deactivate user accounts in your application.
The current version is SCIM 2.0, defined in RFC 7642, RFC 7643, and RFC 7644. It specifies:
- User and Group resources with a standard JSON schema
- CRUD operations via REST endpoints (
/Users,/Groups) - Filtering and pagination for listing resources
- Bulk operations for the initial provisioning of large user bases
Why do enterprise customers require SCIM?
Picture a company with 2,000 employees adopting your SaaS without SCIM. The IT admin creates 2,000 accounts by hand, or uploads a CSV if you happened to build that. After that, it is all manual. New hires, department changes, people walking out the door on their last day: someone has to remember to reflect each of those in your app. When the security audit asks how offboarded employees lose access, the honest answer is "we hope the IT admin remembers."
With SCIM in place, the same story reads very differently:
- The IT admin configures SCIM provisioning once, in Azure Entra ID or Auth0.
- A new hire shows up in your app within minutes, provisioned automatically.
- When someone moves departments, their group membership updates on its own.
- When someone leaves, their account is deactivated without anyone touching your app.
- The security audit gets a clean answer: the IdP owns the user lifecycle, and provisioning runs through SCIM.
SSO handles authentication. The user lifecycle is SCIM's job. Enterprise buyers expect both, which is why the two sit next to each other on the same vendor security assessment.
How does SCIM work technically?
Your .NET application exposes a SCIM API, and the customer's identity provider calls it to manage users. The provider authenticates with a bearer token (OAuth2) or a long-lived API token. Azure Entra ID and Auth0 support both. These are the endpoints you implement:
| Method | Endpoint | Purpose |
|---|---|---|
POST | /scim/Users | Create a new user |
GET | /scim/Users/{id} | Get a specific user |
GET | /scim/Users?filter=... | Search users (for example by email) |
PATCH | /scim/Users/{id} | Update user attributes |
DELETE | /scim/Users/{id} | Deactivate or remove a user |
POST | /scim/Groups | Create a group |
PATCH | /scim/Groups/{id} | Add or remove group members |
A SCIM user resource is plain JSON with a fixed schema. A create request coming from the identity provider looks roughly like this:
POST /scim/Users
Authorization: Bearer <token>
Content-Type: application/scim+json
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "john@example.com",
"name": { "givenName": "John", "familyName": "Doe" },
"emails": [{ "value": "john@example.com", "primary": true }],
"externalId": "8f14e45f-ceea-467a-9f0e-1c2b3d4e5f60",
"active": true
}
How do you build SCIM in ASP.NET Core?
There is no official Microsoft library for a SCIM server in .NET. You build it yourself, with standard ASP.NET Core controllers and middleware. Here is a minimal controller skeleton:
[ApiController]
[Route("scim/Users")]
[Authorize(AuthenticationSchemes = "ScimBearer")]
public class ScimUsersController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Create([FromBody] ScimUser resource)
{
// Idempotent: return the existing user if userName already exists
var existing = await _users.FindByUserNameAsync(resource.UserName);
if (existing is not null)
return Ok(existing.ToScim());
var created = await _users.CreateAsync(resource);
return Created($"/scim/Users/{created.Id}", created.ToScim());
}
[HttpPatch("{id}")]
public async Task<IActionResult> Patch(string id, [FromBody] ScimPatchRequest patch)
{
// Apply op/path/value operations, e.g. set active = false
var user = await _users.ApplyPatchAsync(id, patch.Operations);
return Ok(user.ToScim());
}
}
The skeleton is easy. The interesting work hides in the details. Five of them matter most.
- Schema mapping. SCIM defines a standard user schema (
urn:ietf:params:scim:schemas:core:2.0:User) with fields likeuserName,emails,name.givenName,name.familyName, andactive. You map these onto your own user model. Most SaaS applications never touch the full schema, so map only the fields your application actually uses. Both Azure Entra ID and Auth0 let administrators pick which attributes to sync. - Filtering. Before creating a user, identity providers query your API to check whether that user already exists. The filter you will see almost every time is
filter=userName eq "john@example.com". So you need at least basic SCIM filter parsing. The full filter grammar is large, but Azure Entra ID and Auth0 lean on a small corner of it. - PATCH operations. SCIM PATCH uses a specific format. Each entry in
Operationscarries anop, apath, and avalue. This is where most implementation bugs live. Azure Entra ID fires PATCH requests to update individual attributes, for example deactivating a user by settingactivetofalse. Get this part right and half your support tickets disappear. - Idempotency. Identity providers retry requests. Your endpoints have to cope with that. Creating a user who already exists should return the existing user, not a 409 Conflict. Azure Entra ID leans on exactly this behaviour.
- Testing with real identity providers. Azure Entra ID and Auth0 both ship SCIM testing tools inside their admin portals. Use them. The request patterns they send sometimes wander away from what the RFC describes. Always test against the actual IdP your customer runs before you go live.
Users and groups are the straightforward part. Roles are not. SCIM syncs user accounts and group memberships cleanly, and Azure Entra ID and Auth0 both support that well: a create or PATCH keeps the user and their group membership in step. Roles are trickier. SCIM has no universally adopted role model, so most teams map an incoming group to a role inside their application, or lean on the enterprise user extension. Provider support for role syncing varies, so decide early how a role flows from the IdP into your app and test that path against the actual identity provider your customer runs.
What are the common SCIM pitfalls?
- Ignoring the
externalIdfield. The identity provider uses it to correlate users between systems. Store it. The day something breaks and you have to troubleshoot, it is the field that lets you line records up across both sides. - Hard-deleting users on DELETE. A SCIM DELETE should deactivate the account, leaving the customer's data and history in place. Setting
active = falseis what you want here. A real row delete throws away information you will wish you had kept. - Supporting only one update verb. Azure Entra ID leans on PATCH. Auth0 uses PUT and PATCH both. So you implement both.
- Missing error responses. SCIM defines specific error response formats, and identity providers parse them to show meaningful errors to IT admins. Return proper SCIM error objects. A generic 500 leaves the admin staring at a useless message.
- No rate limiting. When an enterprise first switches SCIM on, the IdP syncs every existing user in one go. Without rate limiting, that opening burst can knock your application over.
How do SSO and SCIM fit together?
SCIM works best sitting alongside SSO over OpenID Connect. In a typical enterprise setup the responsibilities split cleanly:
- SCIM provisions user accounts and keeps them in sync.
- SSO (OpenID Connect) handles login, which means no passwords ever live in your application.
- OAuth2 manages the API access tokens for machine-to-machine traffic.
Wire all three together and the customer's IT admin configures everything in one place, whether that is Azure Entra ID Enterprise Applications or Auth0 Application Integration. Users get provisioned automatically and sign in with their corporate credentials. The hard part is having one person who can see the whole picture at once. Split SSO, provisioning, and authorization across three teams and the seams between them are where it breaks.
Does your .NET application need to pass a vendor security assessment? Which piece are you missing first, the provisioning or the sign-in?