Skip to content

SCIM Provisioning

The System for Cross-domain Identity Management (SCIM) is an open standard for exchanging user and group data between systems. It is defined by RFC 7643 (core schema) and RFC 7644 (protocol).

Duende User Management includes a SCIM 2.0 service provider. When enabled, your IdentityServer application exposes SCIM endpoints that an external identity provider, HR system, or directory can call to create, update, and remove users and groups. Users and groups provisioned this way are stored in User Management, so they are available to the rest of your application in the same way as users created through any other User Management API.

User Management supports inbound SCIM only. It receives provisioning requests from a SCIM client and it does not send changes to other systems.

A SCIM client manages two resource types, which map to User Management concepts:

  • Users map to a User Management user and its user profile. The SCIM id of a user is the user’s subject id. SCIM attributes such as userName, name, and emails are stored as user profile attributes, so the profile schema must define them.
  • Groups map to User Management groups. Group members are stored as group memberships, using the same data as IMembershipAdmin.

Deleting a SCIM user removes the User Management user. SCIM’s active attribute is stored on the profile as data only. User Management does not use it to block sign-in or revoke existing sessions; your application can apply it in its own business rules if needed.

When User Management authentication is enabled and a SCIM create request includes a valid password, SCIM registers a password authenticator for the new user. Users provisioned without a password do not have an authenticator registered automatically. To let these users sign in, configure an authentication method and attach it to the existing provisioned user, for example by looking up the profile by userName or email. Some authentication methods, such as OTP, create a new user on first sign-in by default. If the directory should be the only source of users, make sure your sign-in flow rejects users that were not provisioned.

By default, the SCIM endpoints are mapped under /scim. Each route can be changed with ScimEndpointOptions, passed as the second argument to EnableScim.

For example, to move the endpoints under /identity/scim:

Program.cs
#pragma warning disable duende_experimental
_ = identityServer.AddUserManagement(users => users.EnableScim(
_ => { },
endpoints =>
{
endpoints.Route = "/identity/scim/Users";
endpoints.GroupsRoute = "/identity/scim/Groups";
endpoints.MetadataRoute = "/identity/scim";
endpoints.BulkRoute = "/identity/scim/Bulk";
}));
#pragma warning restore duende_experimental
EndpointOperationsAuthorization
/scim/UsersList, filter, and search (.search), get, create, replace, patch, deleteRequired
/scim/GroupsList, filter, and search (.search), get, create, replace, patch, deleteRequired
/scim/BulkMultiple Users and Groups operations in one requestRequired
/scim/ServiceProviderConfigDescribes the features the service provider supportsAnonymous
/scim/ResourceTypes, /scim/SchemasDescribe the available resource types and their schemasAnonymous

The Users endpoints are enabled when IUserProfileAdmin is registered, and the Groups endpoints when IGroupAdmin is registered. ServiceProviderConfig advertises these capabilities, along with list and Bulk limits, to SCIM clients.

SCIM is part of User Management 2.0 and requires the IdentityServer integration package:

Terminal window
dotnet add package Duende.IdentityServer --version 8.1.0-preview.6
dotnet add package Duende.UserManagement.IdentityServer8 --version 2.0.0-preview.6

Also add a storage provider. The examples below extend an application configured as described in IdentityServer Integration, where identityServer is the existing IIdentityServerBuilder.

SCIM User resources require the userName attribute, which the default User Management profile schema does not include. Use DefaultScimUserSchema, which defines the SCIM core user attributes, or add its attributes to your own profile schema:

Program.cs
var profileSchema = new SchemaConfiguration
{
SchemaId = SchemaId.UserProfile,
DisplayName = "User Profile",
Groups = [.. DefaultScimUserSchema.Groups],
AttributeDefinitions = [.. DefaultScimUserSchema.AttributeDefinitions]
};
_ = identityServer.AddInMemoryDataExtensionSchemas([profileSchema]);

Call EnableScim when registering User Management, then map the endpoints with MapScim:

Program.cs
#pragma warning disable duende_experimental
_ = identityServer.AddUserManagement(users => users.EnableScim(_ => { }));
#pragma warning restore duende_experimental
var app = builder.Build();
// ...existing middleware and endpoints, including MapUserManagement()
#pragma warning disable duende_experimental
_ = app.MapScim();
#pragma warning restore duende_experimental

MapScim requires EnableScim to have been called. Keep your existing authentication and authorization middleware in the pipeline.

Use the EnableScim callback to configure ScimOptions, for example to limit list and Bulk request sizes:

Program.cs
#pragma warning disable duende_experimental
_ = identityServer.AddUserManagement(users => users.EnableScim(options =>
{
options.MaxResults = 50;
options.MaxBulkOperations = 20;
}));
#pragma warning restore duende_experimental

EnableUsers, EnableGroups, and ChangePassword default to detecting the registered services. For example, ChangePassword is reported as supported when a password authenticator is registered. Set them explicitly to override what ServiceProviderConfig reports. See the SCIM configuration reference for all options.

The SCIM data endpoints require an access token. By default, they use a JWT bearer scheme that validates tokens issued by your IdentityServer for the audience urn:duende:scim, with two scopes:

ScopeAccess
scimRead and write
scim.readRead only

Register the SCIM API resource and scopes with IdentityServer, and grant a provisioning client the scim scope:

If your application already registers in-memory API scopes or clients, merge the SCIM entries into those existing registrations. Calling AddInMemoryApiScopes or AddInMemoryClients again with only the SCIM entries can replace the configuration used by your existing scopes and clients.

Program.cs
#pragma warning disable duende_experimental
_ = identityServer
.AddInMemoryApiResources([ScimResources.ApiResource])
.AddInMemoryApiScopes([
ScimResources.FullAccessScope,
ScimResources.ReadOnlyScope
])
.AddInMemoryClients([
new Client
{
ClientId = "provisioning-client",
ClientSecrets = [new Secret("<secret>".Sha256())],
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = [ScimResources.FullAccessScope.Name]
}
]);
#pragma warning restore duende_experimental

The token authority must match the issuer of the tokens. With the IdentityServer integration, it is resolved from IdentityServerOptions.IssuerUri. You can also set it, and other token validation options, with ConfigureScimOAuth:

Program.cs
#pragma warning disable duende_experimental
_ = identityServer.AddUserManagement(users => users
.EnableScim(_ => { })
.ConfigureScimOAuth(options =>
{
options.Authority = "https://identity.example.com";
}));
#pragma warning restore duende_experimental

Startup fails if no authority or audience is configured.

If your SCIM client authenticates in a different way, for example with a long-lived bearer token issued by another system, register an ASP.NET Core authorization policy and set AuthorizationPolicyName to its name. The policy is applied to all SCIM data endpoints, so it must also implement any read-versus-write distinction you need. Metadata endpoints remain anonymous.

When a custom policy is configured, the built-in Authority and Audience validation is skipped. The policy must select and validate its own authentication scheme.

Configure the identity provider or directory with:

  • The SCIM base URL, for example https://identity.example.com/scim. Always use HTTPS.
  • Credentials that satisfy your authorization configuration, such as a client that can obtain a token with the scim scope.

Follow the client’s own documentation for how it maps its attributes to SCIM attributes, and make sure each attribute it sends is defined in your profile schema.

If you implement your own SCIM client, do not automatically retry POST requests, including Bulk requests. The original request may have succeeded before its response was lost, so a retry can create duplicate users or repeat part of a Bulk request.

This section describes how to connect Microsoft Entra ID and Okta to an IdentityServer application that uses the default template and the default profile schema. Read Known Issues first, because several provider defaults hit current limitations.

  • Use https://<host>/scim as the SCIM base URL, or the route you configured with ScimEndpointOptions.
  • Create a client_credentials client with the scim scope, as shown in Authorization. The provider uses the token endpoint of your IdentityServer.
  • Define the SCIM User attributes in the profile schema, for example with DefaultScimUserSchema. Without them, provisioning fails with errors such as Attribute 'active' is not defined in the schema.
  • Add the single-valued attributes email, given_name, and family_name to the profile schema if it doesn’t define them. They are the source of the OIDC claims.

Both providers send the email in the SCIM emails collection and the names in the name object. User Management does not copy these values into email, given_name, and family_name. Without the attribute mappings below, users are created with these attributes empty.

To check that provisioning works, look at the stored profile attributes or the issued claims. The SCIM name object can contain names that aren’t stored in the profile, so it doesn’t tell you whether the mappings work.

  1. Create a non-gallery enterprise application.
  2. Set the provisioning mode to Automatic and the authentication method to OAuth2 client credentials grant. Enter the tenant URL (the SCIM base URL), the token endpoint, the client ID, and the client secret. Entra requests and renews tokens itself.
  3. In the classic provisioning UI, edit the attribute mappings:
    • Delete the rows whose target is in urn:ietf:params:scim:schemas:extension:enterprise:2.0:User.
    • Delete the default email mapping, whose target is emails[type eq "work"].value. User Management rejects this filtered path (see Known Issues).
    • Under Show advanced options, choose Edit attribute list for the application and add email, given_name, and family_name with type String.
    • Add Direct mappings from mail to email, from givenName to given_name, and from surname to family_name.
    • Keep the default active mapping. Entra sends active as the strings "True" and "False", and User Management accepts them.
  4. Disable group provisioning unless you need groups.

The email mapping reads the Entra mail attribute, which can be empty even when the user principal name is set. Users without a mail value are created without an email.

If you keep Entra’s default email mapping, Entra leaves emails out of the create request when mail is empty. It sends the value later as a PATCH on the filtered path, and that PATCH fails.

  1. Add the SCIM 2.0 Test App (OAuth Bearer Token).

  2. Request an access token for the SCIM client yourself with the client credentials grant. Okta doesn’t request tokens for this app. See Token Lifetime before you choose the token lifetime.

    Terminal window
    curl -X POST https://<host>/connect/token \
    --data-urlencode "grant_type=client_credentials" \
    --data-urlencode "client_id=<client-id>" \
    --data-urlencode "client_secret=<client-secret>" \
    --data-urlencode "scope=scim"
  3. Under Provisioning, select Configure API Integration and enable the API integration. Enter the SCIM base URL and paste the access_token value from the response as the OAuth bearer token. Test the credentials, then save.

  4. Under Provisioning > To App, enable Create Users, Update User Attributes, and Deactivate Users. Without Create Users, assignment fails with Matching user not found.

  5. Go to Directory > Profile Editor and open the application’s user profile, not the Okta user profile. Select Add Attribute three times. Use the data type string and the external namespace urn:ietf:params:scim:schemas:core:2.0:User for each:

    Variable nameExternal nameMap from
    oidcEmailemailuser.email
    oidcGivenNamegiven_nameuser.firstName
    oidcFamilyNamefamily_nameuser.lastName

    The variable names can’t be email, givenName, or familyName, because the default application profile uses those for the SCIM emails and name mappings.

  6. Open Mappings, choose the Okta User to application direction, and map the sources to the new attributes. Apply the change to existing users.

Okta then sends email, given_name, and family_name next to emails and name.

Okta’s default create and replace requests contain "groups": [], which this version rejects (see Known Issues).

Okta stores the bearer token as a fixed value and doesn’t renew it. With the default access token lifetime of one hour, provisioning fails after an hour with Error while verifying if user ... exists: Unauthorized.

Set a longer access token lifetime on the SCIM client, and replace the token in Okta before it expires. Anyone who obtains the token can read and write users until it expires, so keep the lifetime as short as your rotation process allows. To confirm that an expired token is the cause, see Troubleshooting.

Use separate Okta groups for application assignment and for Group Push. When one group does both, removing a user from it unassigns the user from the application. Okta then sends active: false and no member removal, so the user stays in the downstream group. Remove the user from the push group before you unassign them. Okta also sends a group PATCH that replaces only id and displayName, which doesn’t change membership. See App assignments and Group Push in the Okta documentation.

Two log categories explain most provisioning failures:

  • Duende.UserManagement.Scim logs rejected create and replace requests with the reason, at Information.
  • Microsoft.AspNetCore.Authentication logs token validation failures, such as an expired token, at Information.

If your minimum log level is Warning or higher, set both categories to Information:

appsettings.json
{
"Logging": {
"LogLevel": {
"Duende.UserManagement.Scim": "Information",
"Microsoft.AspNetCore.Authentication": "Information"
}
}
}

If you use Serilog, add the same categories under Serilog:MinimumLevel:Override.

The SCIM logs don’t contain request bodies. Most failed PATCH requests, such as a request with an unsupported path, aren’t logged at all. To see what a provider sends, enable HTTP logging for the SCIM endpoints only:

Program.cs
using Microsoft.AspNetCore.HttpLogging;
builder.Services.AddHttpLogging(options =>
{
options.LoggingFields = HttpLoggingFields.RequestMethod
| HttpLoggingFields.RequestPath
| HttpLoggingFields.RequestBody
| HttpLoggingFields.ResponseStatusCode
| HttpLoggingFields.ResponseBody;
});
var app = builder.Build();
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/scim"),
scim => scim.UseHttpLogging());

HTTP logging writes to the Microsoft.AspNetCore.HttpLogging category at Information. If your minimum log level is Warning or higher, set this category to Information as well, or the request and response logs are filtered out:

appsettings.json
{
"Logging": {
"LogLevel": {
"Microsoft.AspNetCore.HttpLogging": "Information"
}
}
}

The fields above leave out request headers, so the bearer token in the Authorization header isn’t logged.

Valid Tokens Rejected With An Invalid Issuer Error

Section titled “Valid Tokens Rejected With An Invalid Issuer Error”

The default bearer scheme applies the configured authority too late for ASP.NET Core to load the issuer’s discovery document, so valid tokens are rejected with 401 invalid_token. Until this is fixed, configure the authority directly on the SCIM bearer scheme, which is named DuendeScimBearer:

Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
builder.Services.Configure<JwtBearerOptions>(
"DuendeScimBearer",
options => options.Authority = "https://identity.example.com");

Use the issuer URL that IdentityServer publishes. Token issuer, signature, audience, and lifetime validation remain enabled.

Spaces Are Not Supported For SCIM Authentication

Section titled “Spaces Are Not Supported For SCIM Authentication”

The built-in SCIM bearer authentication uses one authority and one set of signing keys, so it can’t validate tokens per space. With Spaces enabled, IdentityServer determines the issuer per request and IssuerUri stays empty. The SCIM authority can’t be derived from it, and startup fails when you don’t set Authority. Setting Authority to the host root URL or to one space’s issuer URL doesn’t help: a fixed authority can’t validate tokens issued by different spaces, and SCIM requests from those spaces can fail with 401 responses. Don’t use the built-in SCIM authentication together with Spaces.

groups In Create And Replace Requests Is Rejected

Section titled “groups In Create And Replace Requests Is Rejected”

SCIM defines the User groups attribute as read-only, and clients such as Okta send "groups": [] in create and replace requests. This version rejects these requests with 400 and scimType: invalidPath, because groups isn’t defined in the profile schema. As a result, Okta can’t create or replace users with its default settings.

PATCH paths that contain a filter, such as emails[type eq "work"].value, return 400 with scimType: invalidPath and the detail Complex filter path expressions are not supported. Adding emails to the profile schema does not change this. Microsoft Entra ID uses this path in its default email mapping. See Microsoft Entra ID for the mapping to use instead.

A PATCH operation with the path externalId on a group returns 400 with scimType: invalidPath. The PATCH stops at the first failed operation and applies none of the changes. Entra sends Add externalId followed by Replace displayName when you rename a group, so the rename fails. To avoid this, remove the externalId mapping from the group attribute mappings in Entra. This workaround hasn’t been tested yet.

IUserProfileAdmin.TryGetAsync with the userName attribute does not find SCIM-provisioned profiles. Use IUserProfileAdmin.QueryAsync with a filter on userName instead, and check for exactly one exact match.

User and Group responses do not include externalId, even when the client sent it. SCIM clients that match resources by externalId should match on userName for users and displayName for groups instead.

The SCIM Provisioning sample uses .NET Aspire to connect a simulated directory, acting as the SCIM client, to an IdentityServer application with SCIM enabled. It shows the configuration on this page and lets provisioned users sign in with email OTP.