SCIM Stream
Author: Jarle Elshaug
Overview
SCIM Stream is the contemporary solution that redefines user provisioning by replacing traditional IGA top-down processing with cutting-edge message subscription capabilities. Clients can now subscribe to real-time messages, allowing for dynamic and automated provisioning, unlike anything you’ve experienced before.
SCIM Stream effortlessly gathers data from authoritative sources, formatting it into SCIM messages that are intelligently published to channels based on predefined rules. Endpoints and applications can then subscribe to relevant stream channels, ensuring swift and accurate message handling.
SCIM Stream includes SCIM Gateway that supports message subscription and automated provisioning, providing a robust foundation for your provisioning needs. In addition, gateway can become a publisher. In this mode, standard incoming SCIM requests from your Identity Provider (IdP) or API are directed and published to the stream. Subsequently, one of the gateways subscribing to the channel utilized by the publisher will manage the SCIM request, and response back.
Key takeaway: In cases where ingress or inbound traffic is restricted, provisioning may require specific measures. By deploying the SCIM Gateway subscriber within these restricted zones, we facilitate provisioning through egress/outbound communication.
Highlights:
- Pub/Sub streaming using NATS having subscription-based provisioning initiated by client
- Well-suited and tailored to meet all kind of needs and conditions; cloud, on-premises, at the edge and even on a Raspberry Pi or offline operations e.g., submarine
- Yes, offline clients are supported, by default messages not processed by client remains in the stream for 14 days before purged
- Multi Tenancy support for versatile usage
- One binary and a configuration file, built using modern programming language, Go
- Docker, Kubernetes and Unikernel-friendly
- Security-first, only egress/outbound traffic initiated by the client ensuring the utmost communication security, leveraging JWT-based decentralized, zero trust principles that require both authentication and authorization
- Currently Microsoft Entra ID and REST connected HR Systems are supported as authoritative collection sources, typically using Entra for IGA and HR for Joiner-Mover-Leaver
- HR collector can be configured to use users most current employment when multiple entries exist, something that is a known limitation for other products
- Replace traditional IGA tools like Microsoft Identity Manager (MIM), Okta, OneLogin, and more by using Entra ID Governance via SCIM Stream
- Outperforms Entra ID standard provisioning; more flexibility and deliver faster results with minimal latency
- Resolves Entra ID limitations of unmanaged read-only Active Directory synced groups, enabling effective group management of on-premises Active Directory from Entra ID
- All changes to users and their group/application membership are processed
- Using pre-defined Entra ID to SCIM attribute mapping, which can be extended and customized by configuration
- Rules are configured per streaming channel (subscriber)
Defined by one or morefilter
having advanced regular expression capabilites for all user/group/application attributes e.g.:- filter: (user.groups.display -match "App4.*") and (user.title -match "(Dog Trainer|Project Manager)")
- Only process messages for users who meet the specified streaming rules criteria
- User object in message only includes groups/applications according to rules, other groups/applications that user is member of are excluded
- Application roles are mapped to SCIM roles giving final message like:
{"roles":[{type":"Application1","display":"Employees access","value":"Employees"}]
- Support nested groups, also linked to Application roles
- Rule-based dynamic groups, roles, and profile attributes for advanced ABAC/RBAC functionality. E.g. gives dynamic groups and application roles when using the free version of Entra ID that do not include this functionality.
- Supports initial load having all or filtered users in Entra ID or HR Systems processed according to both streaming and dynamic rules
- Hot reload feature on configuration changes, eliminating the need for restarts
- Option for sending e-mail on error
- Builtin methods like GetUniqueValue(), Increment(), Normalize(), ElementNumber(), Join(), FirstN(), Replace(), LowerCase() and UpperCase(). These can be used in attribute mappings, typically when creating new users, unique value must be found and set for upn, samAccountName, e-mail,…
- Includes SCIM Gateway
- Existing gateway plugins can be used out-of-the-box, ensuring seamless integration with your infrastructure
- Facilitates message subscription and automated provisioning
- In publisher mode, standard incoming SCIM requests to the gateweay will be published to stream and one of the gateways subscribing to the channel will manage the request and response back. Note, since scimgateway also is an api-gateway that may handle all kind of request through the /api url, we now also have the possibility to stream whatever we want
- Gives loadbalancing and failover by adding more gateways subscribing to same channel
- Option for converting roles to groups, allowing the use of existing group-logic
- All groups/roles at target endpoint can be imported to Entra ID as Application Roles for centralized management
Message format
SCIM Stream use NATS message technology having a SCIM formatted message that includes following:
- General information: type of operation, when changed, who did the change etc
- Operations: SCIM v2.0 Operations object that includes what have been changed
- user: SCIM formatted user object including all user attributes according to attribute mapper and stream rules
SCIM formatted message below shows an example of a user that have been assigned the Entra ID group “Employee” (can be found in Operations
and user.groups
)
{
"activityOperation": "modifyUser",
"activityDateTime": "2022-07-01T10:27:55.403989Z",
"initiatedById": "2b47d2fd-a98e-4051-b053-93d2da7f834e",
"initiatedByUpn": "[email protected]",
"targetId": "da33c8bd-4511-5058-b79e-7a45938b1b08",
"targetUpn": "[email protected]",
"Operations": [{
"op": "add",
"value": [{
"displayName": "Employees",
"id": "Employees"
}
],
"path": "groups"
}
],
"user": {
"active": true,
"addresses": [{
"type": "work",
"region": "CA",
"city": "Hollywood",
"postalCode": "91608",
"streetAddress": "100 Universal City Plaza1",
"country": "USA"
}
],
"displayName": "John Smith",
"emails": [{
"type": "other",
"value": "[email protected]"
}, {
"type": "work",
"value": "[email protected]"
}
],
"externalId": "johns",
"groups": [{
"display": "Employees",
"value": "Employees"
}, {
"display": "Admins",
"value": "Admins"
}
],
"name": {
"givenName": "John",
"familyName": "Smith",
"formatted": "John Smith"
},
"phoneNumbers": [{
"type": "mobile",
"value": "555-555-6521"
}, {
"type": "work",
"value": "555-555-1256"
}
],
"roles": [{
"type": "Application1",
"display": "User",
"value": "User"
}
],
"title": "Consultant",
"userName": "[email protected]",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"organization": "Universal Studios",
"employeeNumber": "991999",
"manager": {
"value": "[email protected]",
"displayName": "Barbara Jensen"
},
"department": "Tour Operations"
},
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"]
}
}
Collector - Entra ID
Users, groups and application access can be provisioned from Microsoft Entra ID to all types of endpoints/applications using SCIM Stream in combination with SCIM Gateway.
The most suitable approach of doing this would likely involve utilizing Entra ID Applications. Within the Application, roles can be established, each with a distinctive display and value. The crucial step lies in configuring the role value to correspond with the actual ID used for the respective group/access at the endpoint. Example SCIM message below shows that Application role value Employees
have been added and user already have Admins
:
{
...
"Operations":[{"op":"add","value":[{"type":"Application1","display":"Employee access","value":"Employees"}],"path":"roles"}]
...
"user":{
...
"roles":[{"type":"Application1","display":"Employees access","value":"Employees"},
{"type":"Application1","display":"Admins access","value":"Admins"}],
...
}
}
We could then have one stream rule telling to include Application1 and all roles for this application e.g.,
rules:
- filter: (user.roles.Application1.value -match ".*")
or even better, using the Application1 Enterprise Application Object ID:
rules:
- filter: (user.roles.<Application1-EnterpriseAppObjectID>.value -match ".*")
Subscriber or the SCIM Gateway plugin then have logic for handling roles
Entra ID groups can be used. Groups can also be linked to an Application role using above mentioned role logic. Groups have display and value. Using standard group logic, we cannot define our own value. Instead SCIM Stream will set group value to the same value as display. We can optionally keep original group ID as value by configuration use_original_group_id
.
To avoid having rules for all groups we want to include, we may prefix Entra ID groups e.g., App1_Employee
and use the prefix in stream filtering rule:
rules:
- filter: (user.groups.display -match "App1_.*")
We may have use-cases where we want to provision users based on group or role membership, but we do not want to include group/role in stream message. Reason could be we do not allow group/role provisioning, or it might not be supported by endpoint/plugin. Using -exclude
in filter rule will skip actual groups/roles from message.
rules:
- filter: (user.groups.value -match "dc474382-b3b6-4d7b-a6da-e9721c4ba150" -exclude)
One rule may have several filters defined giving OR-logic
. E.g., we want all users having title=Consultant to be included, in addition we want members of any Application1 roles to be included and also include application role provisioning for those
rules:
- filter: (user.title -match "Consultant")
- filter: (user.roles.<Application1-EnterpriseAppObjectID>.value -match ".*")
For AND-logic
we may use
rules:
- filter: (user.title -match "Consultant") and (user.roles.<Application1-EnterpriseAppObjectID>.value -match ".*")
Note, when provisioning to Active Directory (AD), and AD is using Microsoft Entra Connect synchronization, we might have user and group updates in Entra that have been initiated by changes in AD. These changes do not need a loopback to AD, and we therefore may define an and
filter-rule for excluding everything that have been initiated by the Entra connect Sync-user
like shown below
rules:
- filter: (user.roles.<Application1-EnterpriseAppObjectID>.value -match ".*") and (initiatedByUpn -match "(?!Sync_.*)")
Preferable strategy:
Use Entra ID Application roles instead of groups
SCIM Gateway includes logic for exporting all target endpoint groups/accesses into Application roles that can be imported to Entra. This way we can ensure that Application roles in Entra reflects all corresponding accesses on endpoints and we get out-of-the-box management from Entra.
SCIM Gateway have optional logic for converting roles to groups. Plugin existing group-logic can then be used instead of implementing new logic for handling roles
Collector - HR
HR collector is typically used for Joiner-Mover-Leaver purposes. When a new employee starts, we want user to be created on misc. endpoints/applications. Same when employee leave company, user should be disabled or removed. This logic is based on employee startDate
and endDate
, supporting most common date formats like: <unix-time-milli-seconds>
, /Date(<unix-time-milli-seconds>)/
, YYYY-MM-dd
, YYYY/MM/dd
, dd/MM/YYYYY
. Collector is equipped with built-in logic to publish only those messages that have changed since the previous collection. For republishing all messages, we could startup with do_initial_load=true
HR collector is a general REST collector. If we need collection from non-REST e.g. direct SQL/LDAP/SOAP or complex logic is needed, we could use a SCIM Gateway in the middle providing the REST API.
Both collector types, hr and entra, use a common configuration structure. But, there are some additional configuration options needed for hr:
- auth - there are several authentication options e.g.,
oauth_saml_assertion
used for SAP SuccessFactors - collect_interval - using crontab expression instead of seconds e.g.
15 1 * * 1-5
- collect_query_string - query used for collecting all users
- collect_query_result_path - where users can be found in collected result
- pre_activate_days - number of days we want users to be created/activated before startDate
In addition hr collector needs attribute mapping configuration, HR to SCIM:
- attribute mapping based on attributes from collect_query_string result
- SCIM attributes
startDate
andendDate
must be included
HR collector includes an optional mapper rule configuration -most_current
, e.g.,
mapper:
rules:
- filter: (employmentNav.results -most_current "startDate")
Above mapper rule will ensure all attribute mapper definitions with regards to employmentNav.results
will use the most current employment in the list/array. User might have several employments and we want to use the most current one, excluding historical and future.
Stream rule for hr will typically include everything, all users e.g.,
rules:
- filter: (.*)
Configuration
SCIM Stream includes:
- a binary: scim-stream
- a configuration file: ./config/config.yaml
Starting scim-stream will check for configuration file ./config/config.yaml
. If not found, it will be created with default settings and we will be forced to startup with additional arguments for auto-generating missing nats configuration including entra/hr-collectors tenant name definition.
Below is an example configuration:
Using entra collector having one tenant mycompany
defined
Collects from tenant mycompany
every 5 second
Streams to 5 different channels: APP1, APP2, APP3, APP4 and APP5
Sending email on error
entra:
mycompany:
auth:
oauth:
clients:
- client_id: <client-id>
client_secret: <client-secret>
general:
collect_interval: 5
do_initial_load_filter: null
entra:
include_guest_users: false
tenant_id: <tenant-id>
streams:
- channel: APP1
comment: Includes all roles for Enterprise Application App1
rules:
- filter: (user.roles.<App1-EnterpriseAppObjectID>.value -match ".*")
- channel: APP2
comment: Includes two groups
rules:
- filter: (user.groups.value -match "<group1-id>")
- filter: (user.groups.value -match "<group2-id>")
- channel: APP3
comment: Includes all groups having name starting with "App3" and Enterprise Application name "Application 1" with role value "Employees"
rules:
- filter: (user.groups.display -match "App3.*")
- filter: (user.roles.Application 1.value -match "Employees")
- channel: APP4
comment: Includes all users having email address suffix "@mycompany.com" or all users member of group name startig with App4 having user title Dog Trainer or Project Manager
rules:
- filter: (user.emails.work.value -match "*[email protected]")
- filter: (user.groups.display -match "App4.*") and (user.title -match "(Dog Trainer|Project Manager)")
- channel: APP5
comment: All included (all users/groups/applications)
rules:
- filter: (.*)
dynamics: null
mapper: null
hr: null
log:
console: false
level: info
email:
on_error:
enabled: true
to: [email protected],[email protected]
smtp:
host: smtp.office365.com
port: 587
username: [email protected]
password: xxx
nats:
<...>
Tenant configuration may include rule based logic for applying dynamic groups, roles and profile attributes giving ABAC/RBAC functionality:
dynamics:
- comment: ABAC/RBAC matching title Consultant and will update user with 2 groups, 2 roles including some profile attributes
dynamic_id: dynamic-1
rules:
- filter: (user.title -match ".*Consultant.*")
assignments:
user.groups:
- display: Grp1-name
value: Grp1-id
- display: Grp2-name
value: Grp2-id
user.roles:
- display: Admin access
value: Admin
type: App1
- display: Employee access
value: Employee
type: App1
user.addresses:
- postalCode: 1234
streetAddress: Back Office Street
type: work
user.type: External
Tenant configuration may also include a mapper section for mapping collector attributes (entra/hr) to SCIM attributes. Mapper configuration is mandatory for HR. For Entra any mapper definition included will be merged with default Entra mapper configuration that have following settings:
mapper:
group:
- displayName:
map_to: displayName
user:
- userPrincipalName:
map_to: userName
- accountEnabled:
map_to: active
- displayName:
map_to: displayName
- givenName:
map_to: name.givenName
- surname:
map_to: name.familyName
- Join([name.givenName], " ", [name.familyName]):
map_to: name.formatted
- mail:
map_to: emails[type eq "work"].value
- otherMails:
map_to: emails[type eq "other"].value
- businessPhones:
map_to: phoneNumbers[type eq "work"].value
- jobTitle:
map_to: title
- preferredLanguage:
map_to: preferredLanguage
- physicalDeliveryOfficeName:
map_to: addresses[type eq "work"].formatted
- streetAddress:
map_to: addresses[type eq "work"].streetAddress
- state:
map_to: addresses[type eq "work"].region
- city:
map_to: addresses[type eq "work"].city
- postalCode:
map_to: addresses[type eq "work"].postalCode
- country:
map_to: addresses[type eq "work"].country
- mobilePhone:
map_to: phoneNumbers[type eq "mobile"].value
- faxNumber:
map_to: phoneNumbers[type eq "fax"].value
- mailNickname:
map_to: externalId
- employeeHireDate:
map_to: startDate
- companyName:
map_to: urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization
- employeeId:
map_to: urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber
- employeeType:
map_to: urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeType
- department:
map_to: urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department
- employeeOrgData.costCenter:
map_to: urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:costCenter
- employeeOrgData.division:
map_to: urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:division
- manager.userPrincipalName:
map_to: urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.value
- manager.displayName:
map_to: urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager.displayName
- groups:
map_to: groups
- appRoles:
map_to: roles
Note, following mapper settings cannot be overridden:
mapper:
group:
- displayName:
map_to: displayName
user:
- groups:
map_to: groups
- appRoles:
map_to: roles
Note, the most important mapper configuration is the unique naming attribute - SCIM mandatory userName:
mapper:
user:
- userPrincipalName:
map_to: userName
For hr we must specify baseUrls and type of authentication. Configuration includes a section general.hr
that is unique for the hr collector, like we have general.entra that is unique for entra. We also need to specify other details like collect query, result path and mapper. Using crontab expression for scheduled collector intervals. Mapper definintion like map_to: onCreate.<attribute>
for create user only mapping, typically used in combination with built-in functions like shown in SAP SuccessFactors example below
Note, using explicite yaml style that includes line-prefix ?key :value
on complex syntax like GetUniqueValue()
Additionally, it’s important to highlight that there’s no requirement to utilize built-in mapper functions if SCIM Gateway plugin already incorporates the necessary logic
hr:
mycompany:
auth:
base_urls:
- https://api2.successfactors.eu
oauth_saml_assertion:
client_id: <...>
company_id: <...>
name_id: <...>
key_file: sf-key.pem
token_url: https://api2.successfactors.eu/oauth/token
general:
collect_interval: 15 1 * * 1-5
do_initial_load: false
hr:
pre_activate_days: 0
collect_query_result_path: d.results
collect_query_string: /odata/v2/PerPerson?$count=true&customPageSize=100&$format=json&$expand=employmentNav/userNav,employmentNav/jobInfoNav,personalInfoNav,personEmpTerminationInfoNav,phoneNav,emailNav,employmentNav/jobInfoNav/companyNav/countryOfRegistrationNav,employmentNav/jobInfoNav/divisionNav,employmentNav/jobInfoNav/departmentNav&$filter=employmentNav/startDate ne null
streams:
- channel: SF
comment: All included
rules:
- filter: (.*)
dynamics: null
mapper:
rules:
- filter: (employmentNav.results -most_current "startDate")
user:
- personIdExternal:
map_to: userName
- ? GetUniqueValue(Normalize(Join(LowerCase(Replace([name.givenName]," ",".")),".",LowerCase(Replace([name.familyName]," ",".")),Increment("01",false),"@mycompany.com")))
: map_to: onCreate.userPrincipalName
- ? GetUniqueValue(Normalize(Join(FirstN(LowerCase([name.familyName]),3),FirstN(LowerCase([name.givenName]),2),Increment("01",true))))
: map_to: onCreate.sAMAccountName
- employmentNav.results.[0].startDate:
map_to: startDate
- employmentNav.results.[0].endDate:
map_to: endDate
- personalInfoNav.results.[0].firstName:
map_to: name.givenName
- personalInfoNav.results.[0].lastName:
map_to: name.familyName
- Join([name.givenName], " ", [name.familyName]):
map_to: name.formatted
- phoneNav.results.[{"isPrimary":true}].phoneNumber:
map_to: phoneNumbers[type eq "mobile"].value
- employmentNav.results.[0].userNav.city:
map_to: addresses[type eq "work"].city
- employmentNav.results.[0].userNav.country:
map_to: addresses[type eq "work"].country
- employmentNav.results.[0].userNav.state:
map_to: addresses[type eq "work"].state
- employmentNav.results.[0].userNav.zipCode:
map_to: addresses[type eq "work"].postalCode
- employmentNav.results.[0].userNav.addressLine1:
map_to: addresses[type eq "work"].streetAddress
- employmentNav.results.[0].jobInfoNav.results.[0].jobTitle:
map_to: title
- employmentNav.results.[0].jobInfoNav.results.[0].managerId:
map_to: urn:ietf:params:scim:schemas:extension:enterprise:2.0:User.manager.value
- employmentNav.results.[0].jobInfoNav.results.[0].department:
map_to: urn:ietf:params:scim:schemas:extension:enterprise:2.0:User.department
- employmentNav.results.[0].jobInfoNav.results.[0].division:
map_to: urn:ietf:params:scim:schemas:extension:enterprise:2.0:User.division
Details
Configuration file ./config/config.yaml
, encompasses the following primary objects:
- entra - collectors for Entra ID
- hr - collectors for REST based HR systems
- log - logging
- email - sending email onerror
- nats - builtin NATS streaming server
Config key | Description |
---|---|
[entra][hr] | collector type, both entra and hr are supported. Typically using entra collector for IGA and hr for joiner-mover-leaver |
[entra][hr].tenant | tenant is self-defined tenant name, aka mycompany. This is the unique name of the specific collector type configuration. We may have one or more tenants configured for each type of collector. Name must correspond with nats.publisher.tenant and nats.server.auth.account.tenant. Note, tenant names are defined and automatically updated during initial startup/configuration procedure |
[entra][hr].tenant.auth | Entra using OAuth, HR may have other requirements, following are supported: OAuth OAuthSamlAssertion BearerToken Basic Auth General Headers |
hr.tenant.auth.base_urls | one or more (failover) endpoint urls. Not applicable for Entra that will use hardcoded url based on entra.tenant.general.entra.tenant_id |
[entra][hr].tenant.auth.oauth | oauth authentication configuration |
[entra][hr].tenant.auth.oauth.clients | array of one or more oauth clients. For Entra, auth client refer to Entra Application that require following api permissions defined: AuditLog.Read.All + Directory.Read.All + User.Read.All . Multiple clients can be defined to alleviate throttling or rate-limit constraints |
[entra][hr].tenant.auth.oauth.clients.client_id | client id, for Entra this is application client id |
[entra][hr].tenant.auth.oauth.clients.client_secret | client secret, will be encrypted on startup - for Entra this is application client secret, |
hr.tenant.auth.oauth_saml_assertion | OAuth SamlAssertion authentication, not applicable for Entra |
hr.tenant.auth.oauth_saml_assertion.audience | default “scim-stream”, value used to tag the SAML assertion |
hr.tenant.auth.oauth_saml_assertion.key_file | file name, file located in ./config/certs - containing the private key of the key pair used to sign the SAML assertion |
hr.tenant.auth.oauth_saml_assertion.cert_file | file name, file located in ./config/certs - containing public certificate corresponding to the key |
hr.tenant.auth.oauth_saml_assertion.client_id | client id, for SAP SuccessFactors this is also called as API key |
hr.tenant.auth.oauth_saml_assertion.company_id | custom SAML attribute used by SAP SuccessFactors representing id of company |
hr.tenant.auth.oauth_saml_assertion.issuer | default “scim-stream”, assertion is issued by scim-stream |
hr.tenant.auth.oauth_saml_assertion.name_id | user identifier to be used to access endpoint |
hr.tenant.auth.oauth_saml_assertion.token_url | the URL of the API server from which you request the OAuth token e.g., https://api2.successfactors.eu/oauth/token |
hr.tenant.auth.oauth_saml_assertion.user_identifier_format | default userName, it can be one of the three following types: userName, userId or email |
hr.tenant.auth.oauth_saml_assertion.life_time | default 3600, lifetime of the SAML Assertion in seconds |
hr.tenant.auth.basic | basic authentication, not applicable for Entra |
hr.tenant.auth.basic.username | username used for authentication |
hr.tenant.auth.basic.password | password used for authentication, will be encrypted on startup |
hr.tenant.auth.bearer_token | bearer authentication, not applicable for Entra |
hr.tenant.auth.bearer_token.token | token used for authentication, will be encrypted on startup |
hr.tenant.auth.header | optional headers to be included, not applicable for Entra |
hr.tenant.auth.header.xxx | xxx is the name of header and configuration defines the value to be set |
[entra][hr].tenant.general | general configuration |
[entra][hr].tenant.general.collect_interval | collect interval in seconds, minimum and default is 5. Also supporting crontab scheduler expression <min> <hour> <day> <month> <weekday> e.g., 15 2 * * 1-5 will run 02:15 Monday-Friday. Crontab expression typically used by HR collector |
[entra][hr].tenant.general.do_initial_load | optional, true or false, default false. true will collect and process all users in Entra/HR during startup or hot reload on configuration changes. Value true will automatically be reverted back to false |
entra.tenant.general.do_initial_load_filter | optional and preferred way of initial load for Entra Same as do_initial_load but instead using a single regular expression filter, like stream rules filter, for collect and process users. Filter will automatically be reverted back to blank/null after processing examples: do_initial_load_filter: (user.roles.25a131d0-af63-443b-976f-ac0076734627.value -match ".*") do_initial_load_filter: (user.roles.App1.value -match ".*") do_initial_load_filter: (user.groups.value -match "dc384382-b3b6-4d6b-a6da-e9801c4ba150") do_initial_load_filter: (user.groups.value -match "(dc384382-b3b6-4d6b-a6da-e9801c4ba150|5f2a9c65-5d65-4e66-a9d6-379dd92974ca)") do_initial_load_filter: (user.groups.display -match "Group1") do_initial_load_filter: (user.userName -match "[email protected]") do_initial_load_filter: (user.title -match "Consultant") do_initial_load_filter: (user.emails.work.value -match "*[email protected]") Note: Regular expressions .* and | are supported, but there are some limitations for intial load filter using .* :"xxx.*" - startsWith is OK, but not for user.groups".*xxx" - endsWith is OK only for attributes user.userName (userPrincipalName), user.emails.work.value (mail) and user.emails.other.value (otherMails)".*xxx.*" - contains is not supported |
[entra][hr].tenant.general.request_timeout | default 90 seconds, when timeout occures we will see a context deadline exceeded message in error log |
entra.tenant.general.entra | Entra specific settings |
entra.tenant.general.entra.tenant_id | Entra ID tenant ID (GUID) or domain name |
entra.hr.tenant.general.entra.include_guest_users | default false, true will include processing of guest users |
entra.tenant.general.entra.skip_upn_startswith | optional, array of upn prefix, users having upn prefix will not be processed e.g. svc will not process upn starting with svc |
entra.tenant.general.entra.use_original_group_id | default false, true will use Entra group id instead of group displayName as SCIM id |
hr.tenant.general.hr | HR specific settings |
hr.tenant.general.hr.collect_query_string | REST query used for collecting all users and needed information. Must correspond with hr mapper definitions |
hr.tenant.general.hr.collect_query_result_path | path relative to the result telling where users can be found (array/list). For SAP SuccessFacters path will be d.results . If path not defined, result should be root array/list of users |
hr.tenant.general.hr.pre_activate_days | default 0, number of days we want users to be created/activated before startDate |
[entra][hr].tenant.dynamics | optional, one or more rules for dynamic groups, roles and profile attributes giving ABAC/RBAC functionality |
[entra][hr].tenant.dynamics.dynamicId | self-defined uniqe id for dynamic rule - this id is also used in stream rules for including dynamic users e.g., (user.dynamicIds -match "dynamic-1") |
[entra][hr].tenant.dynamics.rules | one or more filter rules for dynamic assignments, several rules gives “OR” logic |
[entra][hr].tenant.dynamics.rules.filter | advanced regular expression supporting the -match operator related to user/group/role (SCIM-formatted) e.g:(user.title -match ".*Consultant.*") |
[entra][hr].tenant.dynamics.assignments | contains assignments definitions on what will be applied when rule(s) are fulfilled and also will be revoked when previous have been fulfilled. Note, assignment key always relates to “user.attribute” e.g. user.group , user.roles , user.addresses , user.name.givenName , … |
[entra][hr].tenant.dynamics.assignments.user.groups |
one or more groups to be assigned |
[entra][hr].tenant.dynamics.assignments.user.groups .display |
group display name |
[entra][hr].tenant.dynamics.assignments.user.groups .value |
group value (id) |
[entra][hr].tenant.dynamics.assignments.user.roles |
one or more scim roles to be assigned |
[entra][hr].tenant.dynamics.assignments.user.roles .type |
role type, e.g: App1 - note for Entra Application roles the type corresponds to Entra Application name |
[entra][hr].tenant.dynamics.assignments.user.roles .display |
display name that correspond to value e.g. “Admin access” |
[entra][hr].tenant.dynamics.assignments.user.roles .value |
value e.g. “Admin” |
[entra][hr].tenant.dynamics.assignments.user.xxx |
all user attrbutes may be used e.g. user.addresses, user.emails, user.title, user.name.givenName, … |
[entra][hr].tenant.streams | streams configuration |
[entra][hr].tenant.streams | one or more entra streams configuration |
[entra][hr].tenant.streams.channel | mandatory channel name used by SCIM Stream publisher. The dot-character . may be used to create a subject hierarchy e.g,:AD AD.OPERATOR AD.USER AD.USER.APP1 Subscriber, the SCIM Gateway plugin, subscribes to channel(s). Note, SCIM Stream will automatically prefix channel with collector type ENTRA or HR . Subscriber then use subject (channel) that must include this prefix e.g. HR.SF , ENTRA.AD , ENTRA.AD.USER.APP1 Wildcards may also be used for subscribing to a hierarchy ENTRA.> includes all ENTRA channels/subjects, ENTRA.AD.* includes single subjects under ENTRA.AD e.g. ENTRA.AD.OPERATOR and ENTRA.AD.USER, but not ENTRA.AD.USER.APP1 |
[entra][hr].tenant.streams.comment | optional |
[entra][hr].tenant.streams.rules | one or more filter rules for message publishing, several rules gives “OR” logic |
[entra][hr].tenant.streams.rules.filter | advanced regular expression supporting the -match operator related to user/group/role (SCIM-formatted) e.g:(user.groups.display -match "App1.*") and (user.title -match "(Dog Trainer|Project Manager)") other filter examples: (.*) includes all users/groups/applications(user.name.givenName -match ".*enr.*") (user.emails.work.value -match ".*@tenant.com") (user.addresses.work.postalCode -match "91608") (user.groups.value -match "9936d283-2ac4-4c78-a8a0-52631fb20344") (user.groups.value -match "9936d283-2ac4-4c78-a8a0-52631fb20344" -exclude) (user.groups.display -match "App1") (user.roles.a12f063b-1c8b-40e0-9988-01b46f478349.value -match ".*") (user.roles.Entra Application Test.value -match ".*") (user.roles.Entra Application Test.value -match "role-value1") (user.dynamicIds -match "dynamic-1") |
[entra][hr].tenant.mapper | optional for Entra, default Entra to SCIM attributes mapping is included. Configuration defined will override or extend defaults. For HR, mapper is mandatory |
[entra][hr].tenant.mapper.user | one or more user mapping (group mapping cannot be overridden) |
[entra][hr].tenant.mapper.user.attr | attr is the name of the Entra ID or HR attribute to be mapped, also supporting virtual attributes defined as functions e.g., Join([name.givenName], " ", [name.familyName]) .Following functions are supported: LowerCase(arg) UpperCase(arg) Join(arg1,arg2,…) Replace(arg, org, new) Normalize(arg) FirstN(arg, number) ElementNumber(arg, seperator, number) Increment(number, true/false) GetUniqueValue(arg) Functions can be nested. Below is an example of defining a unique upn; removing spaces from givenName and familyName, Join them both with “.”, normalize for replacing country special chracters e.g. å=>a , also join with Increment() that will add number specified and will be increased on next try when combined with GetUniqueValue() - using Increment(xx, true ) will include xx on first GetUniqueValue attemptGetUniqueValue(Normalize(Join(LowerCase(Replace([name.givenName]," “,”.")),".",LowerCase(Replace([name.familyName]," “,”.")),Increment(“01”,false),"@mycompany.com"))) Note, function like GetUniqueValue() could be used only when creating user. We then set a custom onCreate prefix in the map_to definitions e.g.- GetUniqueValue(…) map_to: onCreate .userPrincipalName |
[entra][hr].tenant.mapper.user.attr.map_to | defines the SCIM attribute name e.g.: userName , name.givenName , emails[type eq "work"].value , urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization for hr startDate and endDate must be included |
hr.tenant.mapper.rules | only applies to hr |
hr.tenant.mapper.rules.filter | supporting only one filter rule -most_current e.g.,- filter: (employmentNav.results -most_current “startDate”) purpose of this rule is to ensure all attribute mapper definitions related to employmentNav.results will use the most current employment in the list/array, a user might have several employments and we want to use the most current, excluding historical and future. startDate referes to SCIM startDate attribute that must be included in attribure mappings |
log | log configuration |
log.console | true or false, default false. true logs to stdout instead of file |
log.level | debug, error, info or disabled |
log.max_age | number of days to retain old log files, default 0 - disabled |
log.max_backups | number of logfiles to keep, default 10 |
log.max_size | max logfile size in Mega bytes before rollover to new file, default 10 |
email configuration | |
email.smtp.host | mailserver, e.g. smtp.office365.com |
email.smtp.port | port used by mailserver e.g. 587, 25 or 465 |
email.smtp.username | mail account username for authentication and also the sender of the email, e.g. [email protected] |
email.smtp.password | mail account password, will be automatically encrypted on startup. If not defined, user/password authentication not in use |
email.smtp.proxy | optional, mailproxy e.g. http://proxy-host:1234 |
email.smtp.proxy_username | proxy authentication username |
email.smtp.proxy.proxy_password | proxy authentication password, will be automatically encrypted on startup |
email.on_error | sending email notification when error occure |
email.on_error.enabled | true or false, true will enable email onerror notifications |
email.on_error.send_interval | default 15, email notifications are deferred until send_interval minutes have passed since the last notification |
email.on_error.subject | default SCIM Stream error - autogenerated email |
email.on_error.to | comma separated list of recipients email addresses e.g: [email protected] |
nats | NATS streaming configuration All configurations are automated through scim-stream startup arguments tenant names must correspond with entra.tenant and hr.tenant names See Downloads for details |
nats.server | NATS server configuration SCIM Stream have builtin NATS server, but also support using external |
nats.server.port | default 9012, port used by server |
nats.server.trace | true or false, default false |
nats.server.encryption_at_rest_key | optional, secret for data encryption at rest; NATS internal message storage will be encrypted. Provided clear text secret will become encrypted on startup |
nats.server.use_external_server | true or false, default false. If true, external NATS server will be used and nats.publisher.urls must be using external server. External server should also be using an exported configuration, see Downloads for details |
nats.server.certificate | optional, files will be autogenerated if not defined. File name defined must be located in ./config/certs |
nats.server.certificate.certfile | certificate file name, default autogenerated cert.pem |
nats.server.certificate.keyfile | private key file name, default autogenerated key.pem |
nats.server.certificate.cafile | CA file name, default autogenerated ca.pem . Note, subscribers needs a copy of this file |
nats.server.auth | NATS server authentication configuration |
nats.server.auth.operator | NATS operator configuration |
nats.server.auth.operator.jwt | operator JSON Web Token |
nats.server.auth.operator.secret | operator secret for jwt |
nats.server.auth.operator.sys_account_jwt | NATS system account jwt |
nats.server.auth.operator.sys_account_secret | NATS system account secret |
nats.server.auth.account | NATS Account configuration |
nats.server.auth.account.tenant | tenant is self-defined tenant name, aka mycompany |
nats.server.auth.account.tenant.jwt | tenant JSON Web Token |
nats.server.auth.account.tenant.secret | tenant secret for jwt |
nats.server.auth.account.tenant.signing | signing for pub/sub auth |
nats.server.auth.account.tenant.signing.pub_secret | secret for signing tenant SCIM Stream publisher |
nats.server.auth.account.tenant.signing.sub_entra_secret | secret for signing tenant entra subscribers |
nats.server.auth.account.tenant.signing.sub_hr_secret | secret for signing tenant hr subscribers |
nats.publisher | publisher configuration, SCIM-Stream is the only publisher of messages |
nats.publisher.tenant | tenant is self-defined tenant name, aka mycompany |
nats.publisher.tenant.jwt | publisher JSON Web Token |
nats.publisher.tenant.secret | publisher secret for signing jwt |
nats.publisher.tenant.urls | default nats://127.0.0.1:9012 using builtin internal NATS server, one or more nats urls (cluster servers), external NATS server could be used |
nats.publisher.tenant.max_age | default 14, number of days messages will remain in the stream before purged. 14 means subscriber can be maximum 14 days offline before starting loosing messages |
Environments
Configuration may also be set using environments or external file.
Syntax is variable names like above dotted notation, but starting with SCIM-STREAM_
, all uppercase, and underscore instead of dots. Attributes having underscore e.g. user_private_key must be used without underscore like USERPRIVATEKEY. Arrays like Entra using CLIENTS[index]
Example 1:
entra:
mycompany:
oauth:
clients:
- client_secret: my-client-secret
Becomes:
export SCIM-STREAM_ENTRA_MYCOMPANY_OAUTH_CLIENTS[0]_CLIENTSECRET=my-client-secret
Example 2:
nats:
publisher:
mycompany:
secret: generated-secret
Becomes:
export SCIM-STREAM_NATS_PUBLISHER_MYCOMPANY_SECRET=generated-secret
An external vault file may also be defined as environment. All configuration environments may be then be included in this file.
Example:
export SCIM-STREAM_VAULTFILE=/var/run/vault/.vaultfile
file "/var/run/vault/.vaultfile" having content:
SCIM-STREAM_ENTRA_MYCOMPANY_OAUTH_CLIENTS[0]_CLIENTSECRET=my-client-secret
SCIM-STREAM_NATS_PUBLISHER_MYCOMPANY_SECRET=generated-secret
Secrets defined in configuration file will automatically become encrypted on startup. Encrypted configuration file cannot be copied from one machine to another unless all secrets are reverted back to clear text. To overcome this, seed logic used for encryption may be overridden by your own seed defined as environment SEED
e.g.:
export SEED=SomeRandomCharacters:-)
SCIM Gateway
SCIM Gateway offers enhanced functionality with support for message subscription. Enabling one or more subscribers initiates seamless automated provisioning through your plugins
Configuration
SCIM Gateway requires a stream
configuration section to be found in the plugin configuration file.
Since streaming do not have any url and corresponding baseEntity, the baseEntity have to be configured at the subscriber:
scimgateway.stream.subscriber.entity.xxx
which normally corresponds with endpoint.entity.xxx
. If baseEntity for multi tenancy/endpoint support not being used, we define baseEntity as undefined: scimgateway.stream.subscriber.entity.undefined
.
Below is subscriber configuration example:
{
"scimgateway": {
"port": 0,
"scim": {
"usePutSoftSync": true,
...
}
...
"stream": {
"baseUrls": ["nats://<scim-stream-host>:9012"],
"certificate": {
"ca": "ca.pem"
},
"subscriber": {
"enabled": true,
"entity": {
"undefined": {
"nats": {
"tenant": "mycompany"
"subject": "ENTRA.APP1",
"jwt": "<...>",
"secret": <...>,
},
"deleteUserOnLastGroupRoleRemoval": false,
"skipConvertRolesToGroups": false,
"generateUserPassword": false,
"modifyOnly": false,
"replaceDomains": [{
"from": "@mycompany.onmicrosoft.com",
"to": "@mycompany.com"
}]
}
}
},
"publisher": null,
},
...
},
"endpoint": {
...
"entity": {
"undefined": {
...
}
}
...
}
}
Below is gateway publisher configuration example:
Note, subscribers must use same channel (subject) GW.APPX
as publisher and they must also have a valid jwt/secret for tenant being used.
{
"scimgateway": {
"port": 8880,
...
"stream": {
"baseUrls": ["nats://<scim-stream-host>:9012"],
"certificate": {
"ca": "ca.pem"
},
"subscriber": null,
"publisher": {
"enabled": true,
"entity": {
"undefined": {
"nats": {
"tenant": "mycompany"
"subject": "GW.APPX",
"jwt": "<...>",
"secret": <...>
},
"company1": {
"nats": {
"tenant": "company1"
"subject": "GW.APPX",
"jwt": "<...>",
"secret": <...>
}
}
}
}
},
...
},
"endpoint": {
<not used by publisher>
}
}
- stream - stream configuration for pub/sub - using streaming we only allow egress/outbound traffic
- stream.publisher - publisher configuration using
GW
prefixed subjects - one gateway may have several publishers for multi tenancy/endpoint. When enabled, standard incoming SCIM requests will routed and published to stream, and one of the gateways subscribing to channel used by publisher will handle the SCIM request and response back to publisher. Also note when gateway is running in publisher mode, plugin methods (getUsers, modifyUser, getApi,…) are not in use - everything will be handled by the subscriber gateway - stream.subscriber - subscriber configuration - one gateway may have several subscribers for multi tenancy/endpoint. For loadbalancing and failover, we may have several gateways running same plugins with same subscriber subject, and only one of them will be processing stream message.
- port - note, setting port to
0
will prevent standard inbound listener to be started, not mandatory but optional - scim.usePutSoftSync - must be set to
true
when subscribing toENTRA
orHR
prefixed subjects which is published by SCIM Stream and not the gateway (gateway is publishing usingGW
prefixed subjects). There will be an startup error if not set to true in case of ENTRA/HR - stream.baseUrls - one or more nats connection urls (cluster servers) e.g.
["nats://<scim-stream-host>:9012"]
- stream.certificate - certificate configuration
- stream.certificate.ca - CA filename or absolute path to the file that have been copied from the SCIM Stream server ref. nats.server.certificate.cafile. This file should be located in
./config/certs
unless absolute path being used - stream.subscriber/publisher - subscriber/publisher configuration
- stream.subscriber/publisher.enabled - true/false, true enables subscriber/publisher
- stream.subscriber/publisher.entity - entity definition
- stream.subscriber/publisher.entity.xxx - xxx is
baseEntity
, set toundefiend
means baseEntity not applicable.subscriber.entity.xxx
must correspond withendpoint.entity.xxx
if endpoint baseEntity being used. This ensures message subcriptions will be linked to correct baseEntity for multi tenancy/endpoints support. For gateway as publisher, baseEntity will be included in message and used by subscriber. - stream.subscriber/publisher.entity.xxx.nats - nats configuration for entity
- stream.subscriber/publisher.entity.xxx.nats.tenant - tenant name aka
mycompany
, must correspond with SCIM Stream tenant name. Not used by publisher, but should be included for information purpose - stream.subscriber/publisher.entity.xxx.nats.subject - syntax:
<collector-type>.<channel>
e.g.ENTRA.APP1
,ENTRA.AD
,HR.SF
. Subject is the stream channel having collector type prefix and is case sensitive.<collector-type>
is eitherENTRA
,HR
orGW
.<channel>
corresponds with SCIM Stream tenant.streams.channel configuration or theGW
type channel used by gateway publisher. The dot-character.
is used for subject hierarchy, see SCIM Stream[entra][hr].tenant.streams.channel
description. Subscriber is only authorized for subscribing to the tenant and collector-type (account) that have signed the jwt and generated the secret used by subscriber. Not allowing cross tenant nor cross ENTRA/HR/GW subscriptions. - stream.subscriber/publisher.entity.xxx.nats.jwt - user JSON Webtoken (JWT) generated by SCIM Stream NATS. JWT is linked to a specific NATS account/tenant allowing subscription to either ENTRA, HR or GW messages for that tenant. JWT will be signed by user secret with final verification/authorization by SCIM Stream NATS server.
- stream.subscriber/publisher.entity.xxx.nats.secret - user secret generated by SCIM Stream NATS for signing jwt, will become encrypted on startup
- stream.subscriber.entity.xxx.deleteUserOnLastGroupRoleRemoval - true/false, default false - true will delete user when the last group/role become removed
- stream.subscriber.entity.xxx.skipConvertRolesToGroups - true/false, default false - true keeps roles as-is
- stream.subscriber.entity.xxx.generateUserPassword - true/false, default false - true will generate a random user password on createUser operation
- stream.subscriber.entity.xxx.modifyOnly - true/false, default false - true will only allow modifyUser/modifyGroup and ignore createUser/deleteUser E.g., we do not want Entra messages to create users in AD when having HR messages for that purpose.
- stream.subscriber.entity.xxx.replaceDomains - optional, array of upn/email domains to be replaced (from/to objects)
- stream.subscriber.entity.xxx.replaceDomains[].from - domain name to be replaced, must start with “@” e.g. “@mycompany.onmicrosoft.com”
- stream.subscriber.entity.xxx.replaceDomains[].to - new domain name, must start with “@” e.g. “@mycompany.com”
Entra Application Roles
SCIM Gateway may retrieve all groups/accesses from target endpoints and create a result having these converted into Entra Application Roles. Result provided includes appRoles content supported by the Entra Application Manifest-appRoles definition. We may copy/paste this appRoles result into Entra Application Manifest and start administring all our existing target endpoint groups/accesses in Entra through Application roles, everything out-of-the-box and unique uuid’s preserved on future run that might include new accesses definitions.
Entra Application Roles can be generated by using GET /AppRoles
Example using defult loki-plugin:
http://localhost:8880/AppRoles
After having updated Entra Application Manifest appRoles[…] content, all we must do is configuring SCIM Stream to include this Entra Enterprise Application ObjectID to a stream channel rule:
streams:
- channel: App1
comment: Includes one application App1
rules:
- filter: (user.roles.<App1-EnterpriseAppObjectID>.value -match ".*")
And configure SCIM Gateway plugin to use this Entra collector stream channel ENTRA.App1
:
{
"scimgateway": {
...
"subscriber": {
...
"entity": {
"undefined": {
"nats": {
"subject": "ENTRA.App1",
...
},
"skipConvertRolesToGroups": false,
...
}
}
},
...
}
Note, by setting
"skipConvertRolesToGroups: true
, roles will be as-is and plugin needs logic for handling roles. This can also be tested using default plugin-loki
License
Downloads are exclusively available for licensed clients