Guides
SCIM directory sync
Provision users and groups from an enterprise IdP into AuthServer and FGA.
This guide follows the common setup path first. A developer enables SCIM once, creates one connection for the customer organization, and gives the customer's IT administrator two values to paste into their identity provider.
SCIM is off by default. A provider must be able to call an absolute public HTTPS URL, so configure the externally reachable origin at the same time:
var publicOrigin = builder.Configuration["SqlOS:PublicOrigin"]
?? throw new InvalidOperationException("SqlOS:PublicOrigin is required for SCIM.");
builder.AddSqlOS<AppDbContext>(options =>
{
options.AuthServer.PublicOrigin = publicOrigin;
options.AuthServer.Issuer = $"{publicOrigin.TrimEnd('/')}/sqlos/auth";
options.AuthServer.EnableScim = true;
});PublicOrigin must be an absolute HTTPS origin only, such as https://identity.example.com, with no path, query, or fragment. The default service-provider path is /sqlos/scim/v2, so the resulting Base URL is https://identity.example.com/sqlos/scim/v2. Set ScimBasePath only when the host application needs a different public path.
If PublicOrigin is omitted, SqlOS can return only a relative path. That is useful for same-origin local curl requests but cannot be pasted into Entra or Okta.
Aspire Dev Tunnels can give a local SqlOS host a temporary TLS-terminated public endpoint. On a current Aspire AppHost, add the integration:
aspire add devtunnelsThen expose the SqlOS-enabled project from the AppHost:
var app = builder.AddProject<Projects.App>("app");
builder.AddDevTunnel("sqlos-scim")
.WithReference(app)
.WithAnonymousAccess();Run the AppHost and copy the tunnel's public https://...devtunnels.ms endpoint from the Aspire dashboard. Configure that exact origin as SqlOS:PublicOrigin, keep Issuer at {PublicOrigin}/sqlos/auth, and restart the host before creating the connection. Use the resulting {PublicOrigin}/sqlos/scim/v2 value as the provider's Base or Tenant URL.
Anonymous access makes the tunneled application endpoint reachable from the internet; the SCIM path still requires its bearer token. Use a dedicated test tenant and test identities, keep dashboard authentication enabled, do not reuse production secrets, and stop or remove the tunnel after the provider test. Dev Tunnels are for development, not production.
Open /sqlos/admin/auth/, select the organization, and open SCIM. Create an enabled connection.
The create action returns:
Copy the token before leaving the result. SqlOS stores its hash, not the plaintext value, so it cannot show the token again.
SqlOS allows one enabled SCIM connection per organization. Keep that connection for the directory's lifetime. If you must replace it, disable the old connection before enabling another one.
The Base URL does not need an organization ID. The bearer token identifies both the connection and its organization, which keeps provider setup simple and prevents a client from choosing another tenant in its request.
| Provider field | SqlOS value |
|---|---|
| Base URL, Tenant URL, or SCIM connector URL | The Base URL returned by SqlOS |
| API token or Secret Token | The one-time bearer token |
| Authentication mode | Bearer token |
| Unique user field | userName, normally the user's work email |
| Supported resources | Users and Groups |
For Okta, enable API integration, paste the Base URL and token, test the credentials, then enable Create Users, Update User Attributes, Deactivate Users, and group push as needed.
For Microsoft Entra, paste the Base URL into Tenant URL, paste the bearer token into Secret Token, test the connection, and start with a small assigned user or group before enabling the full provisioning scope.
Continue with the provider-specific setup after the connection test:
Google Workspace does not provide a public generic SCIM push client. Do not paste these values into the Google Admin SDK or treat its pull-based directory API as SCIM. Use a supported IdP, a purpose-built Google connector, or your own SCIM 2.0 client.
Start with a single test user. Confirm that the IdP can query, create, update, deactivate, and reactivate that user. Then push one small group and verify member add and remove before assigning the whole directory.
SqlOS supports the provider behaviors needed for this flow:
| Operation | Supported behavior |
|---|---|
| User lookup | Exact eq filters on id, userName, externalId, and emails.value |
| Group lookup | Exact eq filters on id, displayName, and externalId |
| User PATCH | Pathless Okta-style values and paths for active, displayName, name parts, userName, and emails |
| Group PATCH | Full member replacement, member add, member remove, and members[value eq "..."] removal |
| Discovery | Service provider configuration plus collection and item reads for resource types and schemas |
Other filter operators such as co, sw, pr, and, and or are not accepted. Unsupported filters return a SCIM invalidFilter error instead of being silently ignored.
SqlOS bounds one PATCH document to 100 operations and one Group resource to 10,000 distinct members. An oversized request fails atomically with 413 and scimType: "tooMany"; partition a larger directory group into smaller application-specific groups rather than retrying the same payload.
Check these surfaces after the first sync:
You can also validate discovery and lookup without changing data:
export SCIM_BASE_URL="https://app.example.com/sqlos/scim/v2"
export SCIM_TOKEN="paste-the-token-shown-by-sqlos"
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $SCIM_TOKEN" \
-H "Accept: application/scim+json" \
"$SCIM_BASE_URL/ServiceProviderConfig"
curl --fail-with-body --silent --show-error --get \
-H "Authorization: Bearer $SCIM_TOKEN" \
-H "Accept: application/scim+json" \
--data-urlencode 'filter=userName eq "ada@example.com"' \
"$SCIM_BASE_URL/Users"For a production rollout, follow the complete SCIM plus SAML access-lifecycle guide. The short version is that SCIM decides who belongs to the organization, SAML authenticates those already-provisioned people, and SAML JIT provisioning stays off.
The most predictable enterprise setup gives each protocol one job: SCIM decides who has organization access, while SAML authenticates those already-provisioned people. Use one stable work-email source in the IdP for every identity field that SqlOS links:
| IdP value | Destination |
|---|---|
Stable work email, commonly user.userprincipalname in Entra | SCIM userName |
| The same work email | SCIM emails[type eq "work"].value |
| The same work email | SAML attribute named email, or the attribute configured as the SAML connection's email field |
| IdP object identifier | SCIM externalId; preserve its exact casing |
Use user.mail instead only when that tenant guarantees it is populated and stable. The important rule is that the SCIM primary email and SAML email claim normalize to the same address.
Create the SAML connection with linking enabled and JIT provisioning disabled:
var draft = await adminService.CreateSsoConnectionDraftAsync(
new SqlOSCreateSsoConnectionDraftRequest(
OrganizationId: organizationId,
DisplayName: "Acme Entra SSO",
PrimaryDomain: "acme.com",
AutoProvisionUsers: false,
AutoLinkByEmail: true));In the dashboard or delegated SSO portal, the equivalent policy is Require SSO for existing members = on and Allow JIT provisioning = off. Provision the test user through SCIM before attempting the first SAML sign-in. SqlOS marks the SCIM primary email verified; the SAML callback can then link the IdP subject to that existing user without creating a new user or membership.
That setting gives the smoothest email-first routing for a trusted organization domain. For a narrower policy, leave AutoLinkByEmail / Require SSO for existing members off and have the application start the organization's SAML connection explicitly. SqlOS can still complete the first subject link when the signed assertion email exactly matches an active, same-organization SCIM record; it does not treat this as general email auto-linking.
Do not enable SAML JIT provisioning when IdP assignment and SCIM deprovisioning are supposed to control access. Otherwise a valid SAML assertion for a not-yet-provisioned person can create a user or membership outside the SCIM assignment lifecycle. Enable email linking only for a trusted enterprise connection whose email claim is controlled by the same organization.
SqlOS is the SCIM service provider and the IdP is the SCIM client. The IdP sends provisioning requests to SqlOS; SqlOS does not poll the IdP through these endpoints.
| SCIM object | SqlOS AuthServer object | SqlOS FGA object |
|---|---|---|
| User | SqlOSUser, external identity, and organization membership | user subject used for authorization checks |
| Group | connection-owned external link | mirrored SqlOSFgaUserGroup |
| Group member | active organization member | SqlOSFgaUserGroupMembership |
| Optional mapping | connection mapping rule | SCIM-managed SqlOSFgaGrant for the group subject |
Directory groups mirror even when no authorization mapping exists. Add mapping rules only for groups that should grant a product role on a known FGA resource.
Okta commonly deactivates a user with PATCH or PUT and active: false; Entra and other clients may send DELETE. SqlOS treats both as soft deprovisioning for that organization:
A SqlOS user can belong to more than one organization. Deprovisioning from Acme must not deactivate the same person's membership or sessions in Contoso. The bearer token, resource lookup, membership updates, group changes, and session revocation all stay within the token's organization.
Manual grants, non-SCIM groups, and memberships in other organizations are not deleted by SCIM. Review those separate access paths if a person still has access after directory deprovisioning.
Open the connection and choose Rotate token. Copy the replacement immediately and update the IdP before its next provisioning run.
Rotation invalidates the previous token. There is no recovery path for a lost plaintext token; rotate it again. A disabled connection also rejects its token until the connection is re-enabled.
Disabling a connection immediately revokes the FGA grants managed by that connection. It does not delete mirrored groups, directory membership history, manual grants, or unrelated FGA state. Re-enabling accepts the token again, but managed grants return only after the IdP pushes or resynchronizes the affected groups.
Use code-first seeding for platform-owned tenants and repeatable authorization mappings. Keep token material in a secret store:
builder.AddSqlOS<AppDbContext>(options =>
{
options.AuthServer.SeedScimConnection("acme", scim =>
{
scim.OrganizationSlug = "acme";
scim.DisplayName = "Acme directory sync";
scim.TokenSecretName = "ACME_SCIM_BEARER_TOKEN";
scim.MapGroup("Acme-Admins", mapping =>
{
mapping.RoleKey = "org_admin";
mapping.ResourceId = "organization::acme";
});
});
});TokenSecretName is the name of a process environment variable; SqlOS reads its value during startup reconciliation. Populate ACME_SCIM_BEARER_TOKEN from your deployment secret system. Token values must contain at least 32 characters and cannot contain whitespace. Generate a high-entropy value, store it as a secret, and never commit it to source control.
Startup configuration is authoritative for seeded connections and seeded mapping rules. Each restart reapplies the configured display name, enabled state, token secret, and mappings. Rotate a seeded credential by replacing the deployment secret and restarting SqlOS, then update the IdP with that same value. The dashboard intentionally makes seeded lifecycle, token, and mapping controls read-only so a later restart cannot silently restore an older credential.
To stage a connection before its secret is available, set scim.Enabled = false. Before connecting the IdP, generate and store the secret, keep TokenSecretName pointed at it, change scim.Enabled = true, and restart. Do not rotate or enable a seeded connection from the dashboard; those values belong in startup configuration.
Removing or renaming a seed disables the orphaned persisted connection, clears its bearer credential, and revokes its SCIM-managed grants during reconciliation. The dashboard remains the support surface for status, token metadata, read-only seeded state, optional dashboard-owned mappings, and persisted sync activity.
| Symptom | What it means | Resolution |
|---|---|---|
401 Unauthorized | Missing, incorrect, rotated, disabled-connection, or wrong-environment token | Rotate or recopy the token and confirm the IdP is using the matching Base URL |
404 Not Found | Resource is not part of this token's organization, or SCIM is disabled | Check the organization, connection state, resource ID, and EnableScim configuration |
400 with invalidFilter | The client sent an unsupported filter or operator | Use an exact supported eq filter from the table above |
400 with invalidSyntax or invalidPath | The PATCH body, operation, or path is not supported | Inspect the SCIM error response and compare the request with the SCIM reference |
409 with uniqueness | The requested username, email, or external ID conflicts with an existing connection resource | Correct the IdP identity values before retrying |
| Users sync but groups do not | Group push or group assignment is not enabled | Enable the provider's group provisioning and start with one small test group |
| Group mirrors but grants no access | Mirroring does not automatically create authorization | Add and enable a mapping rule for the correct FGA role and existing resource |
| User still has access after deactivation | Access comes from another organization or a non-SCIM source | Inspect organization memberships, manual grants, and non-SCIM groups |
Dashboard shows only /sqlos/scim/v2 | PublicOrigin is missing | Configure the absolute public HTTPS origin and matching issuer, then restart before copying the provider URL |
| Host fails while reconciling a seeded connection | An enabled seed's configured Token or TokenSecretName did not resolve on this startup | Provide a 32+ character whitespace-free configured secret, or keep the seed disabled until the secret is available |
The sync log records operations that reached persistence and application-level reconciliation failures. Request parsing, authentication, filter, and PATCH validation errors are returned directly to the IdP and may not create a sync event. Audit Logs record resulting security changes. Use the IdP provisioning log, SCIM response, SqlOS sync activity, and Audit Logs together when debugging a rollout.
Run the provider-shaped HTTP suite, service tests, real-SQL protocol tests, and docs gate:
dotnet test tests/SqlOS.Tests/SqlOS.Tests.csproj \
--filter 'FullyQualifiedName~SqlOSScim'
dotnet test tests/SqlOS.IntegrationTests/SqlOS.IntegrationTests.csproj \
--filter 'FullyQualifiedName~ScimProtocolIntegrationTests'
./scripts/docs-check.sh