<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Michay Kalika</title>
    <description>The latest articles on DEV Community by Michay Kalika (@ciam_michay).</description>
    <link>https://dev.to/ciam_michay</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4115279%2F2ed37830-4092-4a98-b69a-aecb8f4de7a6.png</url>
      <title>DEV Community: Michay Kalika</title>
      <link>https://dev.to/ciam_michay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ciam_michay"/>
    <language>en</language>
    <item>
      <title>SCIM Deprovisioning in Multi-Tenant SaaS: What Should Actually Be Deleted?</title>
      <dc:creator>Michay Kalika</dc:creator>
      <pubDate>Thu, 10 Sep 2026 08:42:36 +0000</pubDate>
      <link>https://dev.to/ciam_michay/scim-deprovisioning-in-multi-tenant-saas-what-should-actually-be-deleted-3pih</link>
      <guid>https://dev.to/ciam_michay/scim-deprovisioning-in-multi-tenant-saas-what-should-actually-be-deleted-3pih</guid>
      <description>&lt;p&gt;An employee leaves Acme, so Acme’s identity provider sends your SaaS application a SCIM deprovisioning request. The same user is still an administrator in Beta. Your application should remove the access controlled by Acme and leave Beta alone.&lt;/p&gt;

&lt;p&gt;In multi-tenant SaaS, SCIM deprovisioning should normally disable or remove the membership controlled by the authenticated tenant, not the global user identity. Delete the global identity only when no other memberships remain and your retention policy permits it.&lt;/p&gt;

&lt;p&gt;The bug usually starts in the data model. One database row has gradually come to represent a person, a login identity, an organization membership, a set of roles, and perhaps active sessions. A handler receives &lt;code&gt;DELETE /Users/{id}&lt;/code&gt;, deletes that row, and discovers too late that it also removed the user from another customer’s account.&lt;/p&gt;

&lt;p&gt;I work at Frontegg, where we build customer identity infrastructure for multi-tenant applications. The design model below is general; the final section identifies Frontegg-specific behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  SCIM does not define your deletion boundary
&lt;/h2&gt;

&lt;p&gt;SCIM standardizes how identity data moves between a client, usually an enterprise identity provider, and a service provider such as a SaaS application. It defines resources, HTTP operations, schemas, filtering, patching, and error responses.&lt;/p&gt;

&lt;p&gt;Your internal identity model still determines what the requested resource represents and which records the operation may change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7644#section-6" rel="noopener noreferrer"&gt;RFC 7644&lt;/a&gt; explicitly leaves the multi-tenancy scheme to the service provider. A SCIM client may control one tenant, several tenants, or a private subset of resources. The service provider must determine which tenant the authenticated connection is allowed to change.&lt;/p&gt;

&lt;p&gt;For a multi-tenant application, deprovisioning can refer to three separate operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Disable a user’s access to one tenant.&lt;/li&gt;
&lt;li&gt;Remove the membership connecting a user to that tenant.&lt;/li&gt;
&lt;li&gt;Delete the user’s global identity from the application.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A SCIM &lt;code&gt;DELETE /Users/{id}&lt;/code&gt; request deletes a SCIM resource according to the protocol. Your application still has to decide what that resource represents. In a multi-tenant system, it should not automatically mean “delete this person everywhere.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Model identity and membership separately
&lt;/h2&gt;

&lt;p&gt;A useful B2B identity model has at least three concepts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User identity
  ├── Acme membership
  │     ├── Admin role
  │     └── Acme status
  └── Beta membership
        ├── Viewer role
        └── Beta status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The identity represents the person and their login. Each membership connects that identity to an organization and carries the access granted there. The SCIM connection defines which organization the directory is authorized to administer.&lt;/p&gt;

&lt;p&gt;Keeping these separate gives the deprovisioning handler a safe target. Acme’s directory can remove or disable the Acme membership without touching the Beta membership.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safe deprovisioning flow
&lt;/h2&gt;

&lt;p&gt;Assume Alex belongs to both Acme and Beta. Acme’s directory sends a request that sets Alex’s SCIM &lt;code&gt;active&lt;/code&gt; attribute to &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is the order I would use when reviewing the handler.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Resolve tenant context from the connection
&lt;/h3&gt;

&lt;p&gt;Bind each SCIM credential or connection to an internal tenant when the connection is created. Do not trust a tenant ID supplied in the request body as proof of authority.&lt;/p&gt;

&lt;p&gt;The authenticated connection should resolve to Acme before the handler looks up Alex.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SCIM credential -&amp;gt; connection -&amp;gt; Acme tenant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a connection can administer multiple tenants, require an explicit tenant selector and verify that the connection is authorized for it. RFC 7644 calls out this access-control requirement for cross-tenant use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Resolve the tenant-scoped resource
&lt;/h3&gt;

&lt;p&gt;Look up the SCIM resource in Acme’s namespace. Do not perform an unconstrained global lookup followed by a global delete.&lt;/p&gt;

&lt;p&gt;Identifiers require care here. SCIM requires the service provider to interpret &lt;code&gt;externalId&lt;/code&gt; within the provisioning domain. The provisioning client controls its uniqueness; the service provider does not enforce global uniqueness. Two directories can legitimately send the same &lt;code&gt;externalId&lt;/code&gt; for different tenant contexts.&lt;/p&gt;

&lt;p&gt;A safer lookup resembles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;membership_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;memberships&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;authenticated_tenant&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;scim_external_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;external_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tenant restriction belongs in the lookup itself, not in a later check someone might forget.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Disable access before considering deletion
&lt;/h3&gt;

&lt;p&gt;Map &lt;code&gt;active: false&lt;/code&gt; to a reversible access state unless your documented contract says otherwise. Disable the tenant membership, remove tenant-scoped role assignments as appropriate, and prevent new sessions for that tenant.&lt;/p&gt;

&lt;p&gt;Do not assume that deactivation requires immediate physical deletion. Security offboarding and data-retention deletion are different workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Revoke the right session scope
&lt;/h3&gt;

&lt;p&gt;If your architecture supports tenant-scoped sessions, invalidate or prevent refresh of the affected tenant context. Do not revoke access to unrelated tenants unless your security model explicitly requires a global response.&lt;/p&gt;

&lt;p&gt;Your behavior here depends on token lifetime, refresh-token design, session storage, and revocation capabilities. Document the delay between the directory event and effective access removal, then test it.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Delete the global identity only when it is orphaned
&lt;/h3&gt;

&lt;p&gt;After removing the Acme membership, check whether the identity still has another valid membership.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;memberships remain -&amp;gt; keep identity
no memberships -&amp;gt; apply orphan policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;“Apply the policy” is deliberate. Your application may need to retain an identity record for audit, legal, fraud-prevention, or recovery purposes even after the last membership is removed. Hard deletion should follow an approved retention process, not happen as an accidental side effect of a tenant offboarding call.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Record enough context to investigate the event
&lt;/h3&gt;

&lt;p&gt;An audit record should identify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The SCIM connection and tenant&lt;/li&gt;
&lt;li&gt;The external and internal resource identifiers&lt;/li&gt;
&lt;li&gt;The requested operation&lt;/li&gt;
&lt;li&gt;The membership affected&lt;/li&gt;
&lt;li&gt;Whether the global identity was retained, disabled, or queued for deletion&lt;/li&gt;
&lt;li&gt;The result and correlation ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid copying unnecessary directory attributes or bearer tokens into logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should happen in common cases?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Incoming event&lt;/th&gt;
&lt;th&gt;Tenant-scoped action&lt;/th&gt;
&lt;th&gt;Global identity action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;active: false&lt;/code&gt; from Acme&lt;/td&gt;
&lt;td&gt;Disable Alex’s Acme membership&lt;/td&gt;
&lt;td&gt;Keep it if Alex belongs elsewhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove Alex from an Acme group&lt;/td&gt;
&lt;td&gt;Update Acme group membership and mapped access&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;DELETE&lt;/code&gt; from Acme&lt;/td&gt;
&lt;td&gt;Remove or tombstone the Acme-controlled SCIM resource according to the documented contract&lt;/td&gt;
&lt;td&gt;Evaluate only after checking other memberships and retention rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Acme disconnects SCIM&lt;/td&gt;
&lt;td&gt;Stop synchronization and follow the agreed tenant-offboarding policy&lt;/td&gt;
&lt;td&gt;Never delete unrelated memberships&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delayed duplicate deprovisioning event&lt;/td&gt;
&lt;td&gt;Return the same safe final state&lt;/td&gt;
&lt;td&gt;Do not turn a retry into a wider deletion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Failure cases worth testing
&lt;/h2&gt;

&lt;p&gt;The successful offboarding path is not enough. Test these cases before calling the integration complete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alex belongs to Acme and Beta; Acme deactivates Alex.&lt;/li&gt;
&lt;li&gt;Two tenants use the same &lt;code&gt;externalId&lt;/code&gt; value.&lt;/li&gt;
&lt;li&gt;A deprovisioning event arrives twice.&lt;/li&gt;
&lt;li&gt;Deactivation arrives before an earlier profile update.&lt;/li&gt;
&lt;li&gt;A retry occurs after the membership changed but before the response was delivered.&lt;/li&gt;
&lt;li&gt;A group removal and user deactivation arrive in the same bulk request.&lt;/li&gt;
&lt;li&gt;The connection is valid but attempts to address another tenant’s resource.&lt;/li&gt;
&lt;li&gt;Alex has an active session for the removed tenant and another for a tenant that should remain available.&lt;/li&gt;
&lt;li&gt;The last membership is removed, but retention policy forbids immediate hard deletion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For every test, assert both sides of the boundary: the targeted tenant changed, and every unrelated tenant remained unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Frontegg handles the boundary
&lt;/h2&gt;

&lt;p&gt;In Frontegg’s &lt;a href="https://developers.frontegg.com/ciam/guides/management/manage-users/overview" rel="noopener noreferrer"&gt;multi-tenant identity model&lt;/a&gt;, a user can have memberships in multiple accounts. Deprovisioning the user from Tenant A removes that tenant membership without changing memberships in other tenants. The global user identity is deleted only when Tenant A was the user’s final membership.&lt;/p&gt;

&lt;p&gt;Frontegg also provides tenant-scoped SCIM provisioning and separates management operations from tenant self-service operations. Your application remains responsible for enforcing tenant context in its own resources, data stores, caches, queues, search indexes, and background jobs.&lt;/p&gt;

&lt;p&gt;For the full protocol overview, request examples, SCIM-versus-JIT comparison, and implementation safeguards, see &lt;a href="https://frontegg.com/blog/system-cross-domain-identity-management-scim" rel="noopener noreferrer"&gt;What Is SCIM? System for Cross-Domain Identity Management Explained&lt;/a&gt;. You can also inspect the &lt;a href="https://developers.frontegg.com/ciam/api/scim" rel="noopener noreferrer"&gt;Frontegg SCIM Provisioning API&lt;/a&gt; and the underlying &lt;a href="https://datatracker.ietf.org/doc/html/rfc7644" rel="noopener noreferrer"&gt;SCIM 2.0 protocol&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Before shipping your own implementation, write down one sentence that defines what &lt;code&gt;/Users/{id}&lt;/code&gt; means in your tenant model. If the answer can accidentally include another customer’s access, the deletion boundary is still too broad.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>security</category>
      <category>authentication</category>
      <category>identity</category>
    </item>
  </channel>
</rss>
