Inbound SCIM Support for Users
As an identity administrator, I want ZITADEL to support the SCIM (System for Cross-domain Identity Management) standard for inbound user provisioning, so that other systems can automatically provision and manage users within ZITADEL. This includes the ability to create, update, and delete users based on the user data changes in the external system. This feature will streamline user management across systems and ensure user states are consistent.
Acceptance Criteria
- [ ] ZITADEL provides a REST only inbound SCIM Interface according to the standard SCIM 2.0 for users: https://scim.cloud/
- [ ] All SCIM standard attributes are implemented (e.g username, email, displayname, etc)
- [ ] The following operations are implemented:
- [ ] Create: POST https://example.com/{v}/{resource}
- [ ] Read: GET https://example.com/{v}/{resource}/{id}
- [ ] Replace: PUT https://example.com/{v}/{resource}/{id}
- [ ] Delete: DELETE https://example.com/{v}/{resource}/{id}
- [ ] Update: PATCH https://example.com/{v}/{resource}/{id}
- [ ] Search: GET https://example.com/{v}/{resource}?filter={attribute}{op}{value}&sortBy={attributeName}&sortOrder={ascending|descending}
- [ ] The SCIM API can only be called by an authenticated user, and the permission follow the internal ZITADEL permissions (manager), Example to create a user the permission user.write is needed
- [ ] The SCIM API is documented
- [ ] We provide a guide with an example of the create and deactivate flow
User schema
Add an extension for scim
- [ ] allow to select the schema type by jsonpath filter (e.g. exists
$[?(@."urn:ietf:params:scim:schemas:extension:CustomExtensionName:2.0:User")]) - [ ] add field extension for mapping scim field to schema field by jsonpath
- [ ] Define mapping of enum types inside schema
Excluded
In this implementation only the User resource will be implemented, the groups resource is not a part of the implementation. This also includes the groups field of the user.
For group mapping, we should take into account the "User Group Authorization" #5822 issue, later on.
Additional Context
How can this be implemented with User Schema?
Additions to OP
Acceptance Criteria
- [ ] The following REST only operations are implemented:
User schema
Add an extension for scim
- [ ] allow to select the schema type by jsonpath filter (e.g. exists urn:ietf:params:scim:schemas:extension:CustomExtensionName:2.0:User)
- [ ] add field extension for mapping scim field to schema field by jsonpath
- [ ] Define mapping of enum types inside schema
Use case from a conversation: Delete user per SCIM is important when offering a "per-seat" pricing of a SaaS application. When a customer deletes/offboards an account the user should also be deleted from the IDP.
Today, we had another potential B2B SaaS customer who specifically requested SCIM. They consider other methods for user synchronization outdated and have listed SCIM as a requirement on their checklist for evaluating new software. Conclusion: We would highly appreciate the possibility :).
I mentioned it in the SCIM discussion, but it might be more relevant here: even just read-only support for SCIM (and custom schemas) would let us solve #7909 with a generic helper, only using OIDC & SCIM rather than Zitadel-specific APIs.
Just want to confirm what I've researched. The inbound use case means that ZITADEL acts as a SCIM server and IdP platforms act as SCIM clients. ZITADEL provides the REST endpoints and the IdP admins / HR click on their UI for adding/removing users to a given app. As a SaaS developer, inbound direction / SCIM server is what I need. Is that correct?
Just want to confirm what I've researched. The inbound use case means that ZITADEL acts as a SCIM server and IdP platforms act as SCIM clients. ZITADEL provides the REST endpoints and the IdP admins / HR click on their UI for adding/removing users to a given app. As a SaaS developer, inbound direction / SCIM server is what I need. Is that correct?
You are right, inbound means an external service is able to provision users to ZITADEL through a SCIM interface. Outbound on the other hand mean, that ZITADEL will send users out to an external service through a SCIM interface. To make it more clear twi examples:
- Inbound: You create a user in your entra id, the user is then sent to ZITADELs SCIM interface and will be created in ZITADEL.
- Outbound: You create a user in ZITADEL, the user will then be sent to your entra ID with the SCIM standard, and can be created in your entra ID.
A remark to the mapping:
Shouldn't the externalIid be mapped to a IdP link on the user?
Additionally, (maybe only for the future) wouldn't it make sense to have some customization on mapping groups to authorizations before we have the groups feature.
Typical use case i see is that one wants to onboard a new employee and provision their accounts from EntraID to ZITADEL and grant necessary permissions. As soon as the user then first signs in, ZITADEL ideally already know the federated userid and does not even need to do JIT provisioning / linking.
A remark to the mapping: Shouldn't the
externalIidbe mapped to a IdP link on the user? Additionally, (maybe only for the future) wouldn't it make sense to have some customization on mapping groups to authorizations before we have the groups feature.Typical use case i see is that one wants to onboard a new employee and provision their accounts from EntraID to ZITADEL and grant necessary permissions. As soon as the user then first signs in, ZITADEL ideally already know the federated userid and does not even need to do JIT provisioning / linking.
I agree on both, the externalid might be difficult as we would also need to know to which idp. For a first mvp I would leave that out. and we can extend in a second version
Another thing that just came to my mind, how do we know to which organization the user belongs? My suggestion, if there is a matching domain on the primary email / username we map to the corresponding organization. we might want to introduce a custom attribute where we can send the org id. If we have nothing else, to the default organization
I think we can leave the org ID logically the same as the other APIs. As the SCIM requests are authenticated by a machine user, we can default to it's organization. We also have the org header to override this.
In any case the machine user needs proper permissions to manage users on a given org.
Therefore I think we should reuse the existing middleware and definition of org selection. We can go with custom fields later if needed.
I think we can leave the org ID logically the same as the other APIs. As the SCIM requests are authenticated by a machine user, we can default to it's organization. We also have the org header to override this.
In any case the machine user needs proper permissions to manage users on a given org.
Therefore I think we should reuse the existing middleware and definition of org selection. We can go with custom fields later if needed.
As SCIM is a standard implementation, I am pretty sure those SCIM clients can't just send an organization header. A custom field would be the better choice in that case. I guess just adding the org id as custom field would already be enough, without any other email domain logic.
Looks good to me. I edited some metadata keys as the profileURL key was used in multiple places. Possibly copy/paste error.
We should add that the Email and Phone verified option is configurable in ZITADEL, right? So that is is either true or false for all users.
Looks good to me. I edited some metadata keys as the
profileURLkey was used in multiple places. Possibly copy/paste error.We should add that the Email and Phone
verifiedoption is configurable in ZITADEL, right? So that is is either true or false for all users.
Great thanks, I added your recommendation and also the org id as custom attribute
Proposal for PATCH:
| SCIM | ZITADEL | PATCH |
|---|---|---|
| id | userid | patch not possible |
| externalid | user metadata: key: externalid | Add, Replace, and Remove |
| username | username | replace |
| loginnames | ||
| name | ||
| name.formatted | profile.displayName | Add, Replace, and Remove |
| name.familyName | profile.familyName | Add, Replace |
| name.givenName | profile.givenName | Add, Replace |
| name.middleName | user metadata: key: name.middleName | Add, Replace, and Remove |
| name.honorificPrefix | user metadata: key: name.honorificPrefix | Add, Replace, and Remove |
| name.honorificSuffix | user metadata: key: name.honorificSuffix | Add, Replace, and Remove |
| displayName | profile.displayName | Add, Replace, and Remove |
| nickName | profile.nickName | Add, Replace, and Remove |
| not mapped | gender | patch not possible |
| profileUrl | user metadata: key: profileURL | Add, Replace, and Remove |
| title | user metadata: key: title | Add, Replace, and Remove |
| preferredLanguage | profile.preferredLanguage | Add, Replace, and Remove |
| locale | user metadata: key: locale | Add, Replace, and Remove |
| timezone | user metadata: key: timezone | Add, Replace, and Remove |
| active | user.state | Replace (active = activate, active false = deactivate, only if it has changed) |
| password | password.password | Add, Replace |
| Emails (list) - Value - Type - primary | Email (single) Note: only primary email will be mapped- email.emaile - mail.isVerified? | Replace |
| phoneNumbers (list) - Value - type | Phone (single) Note: only primary phone will be mapped- phone.phone - phone.isVerified | Add, Replace, Remove |
| ims | user metadata: key: ims | Add, Replace, Remove |
| photos | user metadata: key: photos | Add, Replace, Remove |
| addresses | user metadata: key: addresses | Add, Replace, Remove |
| groups | user metadata: key: groups | Add, Replace, Remove |
| entitlements | user metadata: key: entitlements | Add, Replace, Remove |
| roles | user metadata: key: roles | Add, Replace, Remove |