Simplifying Identity Automation: Building a SCIM Server for Okta On-Premises Provisioning (OPP) Agent
Introduction
In today’s digital landscape, organisations need to manage thousands of user accounts across multiple applications across cloud, on-premises, and hybrid infrastructures. Doing this manually is inefficient, error-prone, and risky from a security perspective. That’s where SCIM (System for Cross-domain Identity Management) comes in.
SCIM provides a standardised, REST-based protocol for provisioning and deprovisioning users and groups. By adopting it, companies can automate identity lifecycle management, reduce administrative overhead, and strengthen compliance.
For applications that live behind corporate firewalls, Okta’s On-Premises Provisioning (OPP) Agent makes it possible to extend SCIM-based provisioning securely without exposing those apps directly to the internet.
This guide will walk you through the process of building a SCIM 2.0 server and integrating it with Okta via the OPP Agent. Intended as a technical reference for implementation, this article assumes a foundational understanding of Okta, core provisioning concepts, and RESTful APIs.
What is SCIM and Why It Matters
SCIM (System for Cross-domain Identity Management) is an open standard designed to simplify the exchange of user identity information between Identity Providers (IdPs) and Service Providers (SPs). In simple terms, it provides a consistent, standardised way to create, update, and deactivate user accounts across multiple systems.
Traditionally, provisioning and deprovisioning users in different applications required custom scripts, APIs, or manual processes. These approaches were error-prone, time-consuming, and difficult to maintain at scale. SCIM addresses this by offering a standardised schema and RESTful APIs that both IdPs and SPs can implement, ensuring interoperability without reinventing the wheel.
For organisations, SCIM reduces operational overhead, strengthens security, and improves compliance by ensuring that access to systems is always aligned with the user’s lifecycle. For developers and vendors, adopting SCIM makes their applications easier to integrate with identity platforms like Okta, Microsoft Entra ID, and others - removing friction for customers and accelerating adoption.
In the context of Okta’s On-Premises Provisioning (OPP) Agent, building a SCIM 2.0 server enables seamless communication between Okta and internal applications, extending modern identity lifecycle management to systems that were never designed with identity standards in mind.
For more information on SCIM, see the SCIM 2.0 and 1.1 specifications.
Okta OPP Agent in a Nutshell
The Okta On-Premises Provisioning (OPP) Agent is a lightweight connector that bridges Okta’s cloud-based identity platform with applications and directories hosted inside your network or private environment.
Think of it as a translator: Okta communicates with the OPP Agent using secure channels, and the agent, in turn, talks to your internal systems using protocols and APIs they understand. This makes it possible to extend Okta’s modern identity lifecycle management capabilities - such as provisioning, deprovisioning, and profile updates - to legacy or custom applications that aren’t directly accessible from the cloud.
The real value of the OPP Agent lies in simplifying integration. Instead of building one-off connectors or exposing internal systems directly to the internet, the agent securely manages that interaction. By implementing a SCIM server behind the OPP Agent, you can give Okta a standards-based interface for managing users, while keeping your internal systems protected and under control.
In short, the OPP Agent is the key enabler for organisations that want to bring the benefits of automation, standardisation, and security to their on-premises or custom applications without compromising on infrastructure boundaries.
Should the target on-premises application already adhere to SCIM compliance (SCIM 1.1 or SCIM 2.0), the development of a distinct SCIM server becomes unnecessary. The agent will merely relay provisioning requests to the application's native SCIM endpoint.
The rest of this article focus on providing a guide to building a SCIM server for target systems that do not have a SCIM-compliant API. It explains how to create the necessary SCIM endpoints and translate SCIM requests into commands that your non-compliant system can understand.
Building a SCIM 2.0 Server
This section describes everything I personally used and implemented on the SCIM 2.0 server I built. The high-level steps should be the same irrespective of the chosen technology stack or hosting environment.
Please note that these instructions are provided at a high level and for reference purposes only. The actual implementation of a SCIM server, including the specific methods and functions, must be adapted to the business logic required to translate these instructions into the provisioning tasks of the target application.
For a production-grade solution, it's crucial to architect for resilience, security, and scalability. This includes ensuring your solution is highly available by deploying it across multiple availability zones. Security is paramount; never use a tunneling service like ngrok in production. Instead, expose your SCIM API through a managed API Gateway (e.g., AWS API Gateway) with robust authentication, rate limiting, and monitoring. You should also implement idempotency to handle duplicate requests gracefully, and use soft deletes for deprovisioning to maintain an audit trail and allow for easier user recovery.
Tech Stack
A programming language capable of building RESTful APIs (e.g., Python with Flask, Node.js with Express, Java with Spring Boot). In my case, I used Python with Flask, as for testing purposes running directly from PyCharm on my local machine.
Since I was developing and running my code locally, I decided to use ngrok to create a secure, public-facing tunnel making my SCIM server accessible from the internet and reachable from my Okta tenant.
While employing a tunneling service such as ngrok during local development may be acceptable, this practice is clearly unsuitable for a production-grade solution. For a production-grade solution, this approach lacks both security and scalability. Instead, a permanent API gateway like AWS API Gateway, Kong, or Apigee should be used to expose APIs to the internet.
Something that can be used for the data store where user information will be saved. This can be:
In my case, I used SQLAlchemy, which is a open-source toolkit for Python. The database is created locally and initiated when the program is executed. This is only for testing purposes, in real-life this would be the actual user store(s) used by the downstream application.
For hosting the Okta OPP Agent, I used Windows Server on AWS EC2 instance.
This proof-of-concept involved using AWS, the Okta Provisioning Platform (OPP) Agent, and ngrok. The primary goal was to simulate and test end-to-end connectivity between all these components., ignoring the fact that in this scenario the SCIM was exposed to the internet. The OPP Agent is crucial in scenarios where the SCIM server, which manages user identities, resides within a private network and cannot be directly exposed to the internet. As mentioned before, it acts as a secure bridge, allowing Okta to send provisioning requests to the on-premises SCIM server without requiring firewall changes. Conversely, in a production environment where a SCIM server is publicly accessible, the OPP Agent is unnecessary. In that case, you can simply configure one of Okta's pre-built SCIM template applications to connect directly to the SCIM server's public endpoint, streamlining the provisioning process and eliminating the need for an on-premise agent.
SCIM Endpoints
Depending on the specific requirements and desired functionality, the Okta On-Premises Provisioning (OPP) Agent requires the implementation of multiple SCIM endpoints, which are invoked during provisioning operations.
/Users - When an Okta admin assigns a user to an application that's integrated with the OPP Agent, Okta sends a SCIM request to the agent's /Users endpoint. This is how the agent knows to provision that user into the third-party application. The /Users endpoint is the key to enabling these provisioning actions:
Screenshot from the Provisioning to App options / features mentioned above.
Below you can find the list of routes, related to the /Users endpoint, which I created on my SCIM 2.0 Server implemented with Python + Flask.
# Get Users
@app.route("/scim/v2/Users", methods=["GET"])
# Get User
@app.route("/scim/v2/Users/<string:user_id>", methods=["GET"])
# Create User
@app.route("/scim/v2/Users", methods=["POST"])
# Update User
@app.route("/scim/v2/Users/<string:user_id>", methods=["PUT"])
/Groups - When an Okta selects a Push Group to an application that's integrated with the OPP Agent, Okta sends a SCIM request to the agent's /Groups endpoint. This is how the agent knows to provision that group into the third-party application. The /Groups endpoint is the key to enabling these provisioning actions:
Below you can find the list of routes, related to the /Groups endpoint, which I created on my SCIM 2.0 Server implemented with Python + Flask.
# Get Group
@app.route("/scim/v2/Groups/<string:group_id>", methods=["GET"])
# Create Group
@app.route("/scim/v2/Groups", methods=["POST"])
# Update Group
@app.route("/scim/v2/Groups/<string:group_id>", methods=["PUT"])
# Delete Group
@app.route("/scim/v2/Groups/<string:group_id>", methods=["DELETE"])
Note: During this exercise I've utilised an application created using the AIW (i.e, Application Integration Wizard). If an application from the Okta Integration Network (OIN) is used instead, some calls might be different. For example, when updating a specific group membership or group name, is the app is from OIN, the update is sent through a PATCH method request, if the app was created using the AIW, the update is sent through a PUT method request.
/ServiceProviderConfig - The ServiceProviderConfig endpoint is a mandatory, read-only endpoint in the SCIM 2.0 specification. It acts as a service discovery mechanism, allowing a SCIM client (like Okta) to understand the capabilities and features of the SCIM service provider without prior knowledge.
When a client makes a GET request to this endpoint, the server must respond with a JSON object that describes:
During the integration settings validation, Okta will show which provisioning features were detected to be supported by the SCIM server and only allow Okta administrators to configure and use such features.
Application route in the SCIM Server.
# Get Service Provider Configurations
@app.route("/scim/v2/ServiceProviderConfig", methods=["GET"])
Example response from this endpoint.
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig",
"urn:okta:schemas:scim:providerconfig:2.0"
],
"documentationUrl": "https://support.okta.com/scim-fake-page.html",
"patch": {
"supported": true
},
"bulk": {
"supported": false
},
"filter": {
"supported": true,
"maxResults": 100
},
"changePassword": {
"supported": true
},
"sort": {
"supported": false
},
"etag": {
"supported": false
},
"authenticationSchemes": [
{
"type": "oauthbearertoken",
"name": "OAuth Bearer Token",
"description": "Authentication scheme using the OAuth 2.0 Bearer Token Standard",
"specUri": "https://datatracker.ietf.org/doc/html/rfc6750",
"documentationUri": "https://example.com/help/scim/oauth",
"primary": true
}
],
"resourceTypes": [
{
"id": "User",
"name": "User",
"endpoint": "/Users",
"schema": "urn:ietf:params:scim:schemas:core:2.0:User"
},
{
"id": "Group",
"name": "Group",
"endpoint": "/Groups",
"schema": "urn:ietf:params:scim:schemas:core:2.0:Group"
}
],
"urn:okta:schemas:scim:providerconfig:2.0": {
"userManagementCapabilities": [
"GROUP_PUSH",
"IMPORT_NEW_USERS",
"IMPORT_PROFILE_UPDATES",
"PUSH_NEW_USERS",
"PUSH_PASSWORD_UPDATES",
"PUSH_PENDING_USERS",
"PUSH_PROFILE_UPDATES",
"PUSH_USER_DEACTIVATION",
"REACTIVATE_USERS",
"IMPORT_USER_SCHEMA",
"OPP_SCIM_INCREMENTAL_IMPORTS"
]
}
}
Application route in the SCIM Server.
# Get Resource Types
@app.route("/scim/v2/ResourceTypes", methods=["GET"])
Example response from the SCIM server.
{
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
],
"totalResults": 2,
"startIndex": 1,
"itemsPerPage": 2,
"Resources": [
{
"id": "User",
"name": "User",
"endpoint": "/scim/v2/Users",
"description": "User Schema",
"schema": "urn:ietf:params:scim:schemas:core:2.0:User",
"meta": {
"location": "/scim/v2/ResourceTypes/User",
"resourceType": "ResourceType"
}
},
{
"id": "Group",
"name": "Group",
"endpoint": "/scim/v2/Groups",
"description": "Group Schema",
"schema": "urn:ietf:params:scim:schemas:core:2.0:Group",
"meta": {
"location": "/scim/v2/ResourceTypes/Group",
"resourceType": "ResourceType"
}
}
]
}
Application route in the SCIM Server.
# Get Schemas
@app.route("/scim/v2/Schemas", methods=["GET"])
Example response from the SCIM server.
{
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
],
"totalResults": 2,
"startIndex": 1,
"itemsPerPage": 2,
"Resources": [
{
"id": "urn:ietf:params:scim:schemas:core:2.0:User",
"name": "User",
"description": "User Schema",
"attributes": [
{
"name": "userName",
"type": "string",
"multiValued": false,
"description": "Unique username for the user",
"required": true,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "server"
},
{
"name": "name",
"type": "complex",
"multiValued": false,
"description": "The components of the user's real name",
"required": false,
"mutability": "readWrite",
"returned": "default",
"subAttributes": [
{
"name": "givenName",
"type": "string",
"multiValued": false,
"mutability": "readWrite",
"returned": "default",
"required": false
},
{
"name": "middleName",
"type": "string",
"multiValued": false,
"mutability": "readWrite",
"returned": "default",
"required": false
},
{
"name": "familyName",
"type": "string",
"multiValued": false,
"mutability": "readWrite",
"returned": "default",
"required": false
}
]
},
{
"name": "emails",
"type": "complex",
"multiValued": true,
"description": "Email addresses for the user",
"required": false,
"mutability": "readWrite",
"returned": "default",
"subAttributes": [
{
"name": "value",
"type": "string",
"multiValued": false,
"mutability": "readWrite",
"returned": "default",
"required": false
},
{
"name": "type",
"type": "string",
"multiValued": false,
"mutability": "readWrite",
"returned": "default",
"required": false
},
{
"name": "primary",
"type": "boolean",
"multiValued": false,
"mutability": "readWrite",
"returned": "default",
"required": false
}
]
},
{
"name": "active",
"type": "boolean",
"multiValued": false,
"description": "Whether the user is active",
"required": false,
"mutability": "readWrite",
"returned": "default"
},
{
"name": "displayName",
"type": "string",
"multiValued": false,
"description": "The display name of the user",
"required": false,
"mutability": "readWrite",
"returned": "default"
}
],
"meta": {
"resourceType": "Schema",
"location": "/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:User"
}
},
{
"id": "urn:ietf:params:scim:schemas:core:2.0:Group",
"name": "Group",
"description": "Group Schema",
"attributes": [
{
"name": "displayName",
"type": "string",
"multiValued": false,
"description": "A human-readable name for the Group.",
"required": true,
"mutability": "readWrite",
"returned": "default"
}
],
"meta": {
"resourceType": "Schema",
"location": "/scim/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:Group"
}
}
]
}
Authentication
In my testing environment, a static Bearer Token (e.g., 123456789) has been implemented in the Authorization header for authentication purposes.
def auth_required(func):
@wraps(func)
def check_auth(*args, **kwargs):
try:
if request.headers["Authorization"].split("Bearer ")[1] == "123456789":
return func(*args, **kwargs)
else:
return make_response(jsonify({"error": "Unauthorized"}), 403)
except Exception as e:
return make_response(jsonify({"error": "Unauthorized"}), 403)
return check_auth
The SCIM protocol doesn't define a SCIM-specific scheme for authentication and authorisation. SCIM depends on the use of Transport Layer Security (TLS) and/or standard HTTP authentication and authorisation schemes as per RFC7235. For example, the following methodologies could be used, among others:
Filtering & Pagination
Filtering and pagination are important capabilities of a SCIM server, as they enable clients to efficiently query and retrieve subsets of resources rather than processing large datasets in a single request.
Filtering allows the Okta OPP Agent to locate specific users or groups based on attributes (for example, userName eq "test.user@example.com"), while pagination ensures responses are broken into manageable sizes using parameters such as startIndex and count. These capabilities must be declared in the /ServiceProviderConfig endpoint, where the SCIM server communicates whether filtering and pagination are supported and specifies any limits (e.g., maximum results). This ensures the provisioning client can optimize its requests and interact with the service provider reliably.
The JSON below is a snippet of the sample response from the /ServiceProviderConfig endpoint. In this case, it tells Okta (i.e., the SCIM Client) that it can issue filter queries and return up to 100 results. Please refer to the section regarding the /ServiceProviderConfig for the full response.
...
"filter": {
"maxResults": 100,
"supported": true
}
...
End-to-End Configuration and Testing
Connectivity - The Okta Provisioning Platform (OPP) Agent has two primary connectivity requirements.
Okta On-Premises Provisioning (OPP) Agent - At least one Okta OPP Agent must be installed and configured within the Okta tenant. Instruction in installing the Okta OPP Agent can be found here.
To verify that at least one Okta OPP Agent is configured, running and ready to be used in an application, an Okta administrator can access the Okta Admin Console and navigate to Dashboard > Agents > On-premise and verify that the agent's status is operational.
Okta SCIM Application - To establish a connection between Okta and the SCIM server, you must first create a SCIM application instance within the Okta tenant. This instance serves as the logical representation of the downstream application you want to provision to. To create the application, an Okta administrator needs to access the Okta Admin Console and navigate to Applications > Applications > Create App Integration. In my particular case, since this application's purpose was provisioning only, I used a SWA - Secure Web Authentication application.
Once the application instance is created, you need to select On-Premises Provisioning, under the General tab, for the Provisioning option.
Then, under the Provisioning tab, under the Integration section, you need to provide all the details to establish the connection with the SCIM server:
Once all the details are provided, you click on Test Connector Configuration to validate the connection. Okta will then execute a connectivity test, and if successful, will indicate which features are supported, and unsupported, by the SCIM server.
Finally, you hit Close and Save. Once the connector configuration is validated and saved, Okta refreshes the application instance to display the supported features. In this case, you will need to navigate to the Provisioning tab and then 'To App'. Under 'Provisioning to App' you enable the following features:
Attribute Mappings - After you've successfully connected the Okta application instance to the SCIM server, the next critical step is to configure attribute mappings. This process ensures that user attributes from the Okta Universal Directory are correctly translated and sent to the downstream application. You'll map standard attributes like userName and displayName as well as any custom attributes that are unique to the application. This mapping is essential because it tells Okta exactly which Okta user profile fields correspond to which fields in the SCIM schema. Without this configuration, the provisioning agent would not know what user data to send, leading to failed provisioning attempts.
The mappings are configured under Directory > Profile Editor. From the list of User Profile, you need to select the one corresponding the our application.
The User Profile for the application will be pre-configured with some Base attributes. You will need to either manually create custom Okta user attributes to match the names and data types of the attributes defined in the SCIM schema. Alternatively, if the SCIM server supports the User Import Schema capability, Okta can automatically discover and import the schema. This second, more efficient method saves time and reduces the risk of manual configuration errors by directly ingesting the schema metadata from your SCIM service.
Once the User Profile for the SCIM Server contains all the required attributes, you can configure the attribute mappings between Okta and the application.
Testing - Once all components - the SCIM server, the Okta Provisioning Platform (OPP) Agent, and the Okta application instance - are fully configured, the final step is to perform comprehensive end-to-end testing.
Assign a New User - Assign the new user to the application in Okta under the Assignment tab, by clicking on Assign > Assign to People.
Okta will first run a GET with a userName filter query parameter to see if the user already exists in the external app.
GET /scim/v2/Users?filter=userName%20eq%20%22Test.SCIM04%40fabiosantos.co.uk%22&startIndex=1&count=200 HTTP/1.1
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.13.0
Date: Fri, 19 Sep 2025 21:50:55 GMT
Content-Type: application/json
Content-Length: 160
Connection: close
{
"Resources": [],
"itemsPerPage": 0,
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
],
"startIndex": 1,
"totalResults": 0
}
Since no users were returned as part of the initial search, Okta will send a HTTP POST to create the new user in the target system.
POST /scim/v2/Users HTTP/1.1
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "Test.SCIM04@fabiosantos.co.uk",
"name": {
"formatted": "Test SCIM 04",
"familyName": "SCIM 04",
"givenName": "Test"
},
"active": true,
"password": "9h!@2YsN",
"emails": [
{
"value": "Test.SCIM04@fabiosantos.co.uk",
"type": "primary",
"primary": true
}
],
"externalId": "00uq84luw5296rJJa1d7",
"displayName": "Test SCIM 04"
}
HTTP/1.1 201 CREATED
Server: Werkzeug/3.1.3 Python/3.13.0
Date: Fri, 19 Sep 2025 21:50:56 GMT
Content-Type: application/json
Content-Length: 589
Connection: close
{
"active": true,
"displayName": "Test SCIM 04",
"emails": [
{
"primary": true,
"type": "primary",
"value": "Test.SCIM04@fabiosantos.co.uk"
}
],
"externalId": "00uq84luw5296rJJa1d7",
"groups": [],
"id": "e82aa505-ff44-4c26-9617-3f513eaea003",
"locale": null,
"meta": {
"resourceType": "User"
},
"name": {
"familyName": "SCIM 04",
"givenName": "Test",
"middleName": null
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:scim:schemas:core:1.0"
],
"userName": "Test.SCIM04@fabiosantos.co.uk"
}
Assign an Existing User - Assign the existing user, in the target system, to the application in Okta under the Assignment tab, by clicking on Assign > Assign to People.
Okta will first run a GET with a userName filter query parameter to see if the user already exists in the external app.
GET /scim/v2/Users?filter=userName%20eq%20%22Test.SCIM04%40fabiosantos.co.uk%22&startIndex=1&count=200 HTTP/1.1
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.13.0
Date: Fri, 19 Sep 2025 22:03:03 GMT
Content-Type: application/json
Content-Length: 864
Connection: close
{
"Resources": [
{
"active": true,
"displayName": "Test SCIM 04",
"emails": [
{
"primary": true,
"type": "primary",
"value": "Test.SCIM04@fabiosantos.co.uk"
}
],
"externalId": "00uq84luw5296rJJa1d7",
"groups": [],
"id": "e82aa505-ff44-4c26-9617-3f513eaea003",
"locale": null,
"meta": {
"resourceType": "User"
},
"name": {
"familyName": "SCIM 04",
"givenName": "Test",
"middleName": null
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:scim:schemas:core:1.0"
],
"userName": "Test.SCIM04@fabiosantos.co.uk"
}
],
"itemsPerPage": 1,
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
],
"startIndex": 1,
"totalResults": 1
}
Since a user was returned as part of the initial search, Okta will send a HTTP PUT to update the existing user in the target system.
PUT /scim/v2/Users/e82aa505-ff44-4c26-9617-3f513eaea003 HTTP/1.1
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"id": "e82aa505-ff44-4c26-9617-3f513eaea003",
"userName": "Test.SCIM04@fabiosantos.co.uk",
"name": {
"formatted": "Test SCIM 04",
"familyName": "SCIM 04",
"givenName": "Test"
},
"active": true,
"emails": [
{
"value": "Test.SCIM04@fabiosantos.co.uk",
"type": "primary",
"primary": true
}
],
"externalId": "00uq84luw5296rJJa1d7",
"displayName": "Test SCIM 04"
}
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.13.0
Date: Fri, 19 Sep 2025 22:03:03 GMT
Content-Type: application/json
Content-Length: 589
Connection: close
{
"active": true,
"displayName": "Test SCIM 04",
"emails": [
{
"primary": true,
"type": "primary",
"value": "Test.SCIM04@fabiosantos.co.uk"
}
],
"externalId": "00uq84luw5296rJJa1d7",
"groups": [],
"id": "e82aa505-ff44-4c26-9617-3f513eaea003",
"locale": null,
"meta": {
"resourceType": "User"
},
"name": {
"familyName": "SCIM 04",
"givenName": "Test",
"middleName": null
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:scim:schemas:core:1.0"
],
"userName": "Test.SCIM04@fabiosantos.co.uk"
}
Update a User - Update the user's profile in Okta (e.g., change the user's last name).
Okta will detect the update and propagate the change to the downstream systems. In the SCIM server's case, it will send a HTTP PUT request to update the user's profile in the SCIM server.
PUT /scim/v2/Users/e82aa505-ff44-4c26-9617-3f513eaea003 HTTP/1.1
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"id": "e82aa505-ff44-4c26-9617-3f513eaea003",
"userName": "Test.SCIM04@fabiosantos.co.uk",
"name": {
"formatted": "Test SCIM 004",
"familyName": "SCIM 004",
"givenName": "Test"
},
"active": true,
"emails": [
{
"value": "Test.SCIM04@fabiosantos.co.uk",
"type": "primary",
"primary": true
}
],
"externalId": "00uq84luw5296rJJa1d7",
"displayName": "Test SCIM 004"
}
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.13.0
Date: Fri, 19 Sep 2025 22:07:35 GMT
Content-Type: application/json
Content-Length: 591
Connection: close
{
"active": true,
"displayName": "Test SCIM 004",
"emails": [
{
"primary": true,
"type": "primary",
"value": "Test.SCIM04@fabiosantos.co.uk"
}
],
"externalId": "00uq84luw5296rJJa1d7",
"groups": [],
"id": "e82aa505-ff44-4c26-9617-3f513eaea003",
"locale": null,
"meta": {
"resourceType": "User"
},
"name": {
"familyName": "SCIM 004",
"givenName": "Test",
"middleName": null
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:scim:schemas:core:1.0"
],
"userName": "Test.SCIM04@fabiosantos.co.uk"
}
Unassign a User - Unassign a user from the application in Okta under the Assignment tab and either remove the user from the respective application assignment group, or if the user was assigned individually, remove the user from the app by click on the X next to the user.
Okta will detect the un-assignment and propagate the change to the downstream systems. In the SCIM server's case, it will send a HTTP PUT request to update the user's profile in the SCIM server. The SCIM server will then perform the required operation to deactivate the user (e.g., deactivate, delete, remove roles and licenses, etc..).
PUT /scim/v2/Users/e82aa505-ff44-4c26-9617-3f513eaea003 HTTP/1.1
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"id": "e82aa505-ff44-4c26-9617-3f513eaea003",
"userName": "Test.SCIM04@fabiosantos.co.uk",
"name": {
"formatted": "Test SCIM 004",
"familyName": "SCIM 004",
"givenName": "Test"
},
"active": false,
"emails": [
{
"value": "Test.SCIM04@fabiosantos.co.uk",
"type": "primary",
"primary": true
}
],
"externalId": "00uq84luw5296rJJa1d7",
"displayName": "Test SCIM 004"
}
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.13.0
Date: Fri, 19 Sep 2025 22:12:53 GMT
Content-Type: application/json
Content-Length: 592
Connection: close
{
"active": false,
"displayName": "Test SCIM 004",
"emails": [
{
"primary": true,
"type": "primary",
"value": "Test.SCIM04@fabiosantos.co.uk"
}
],
"externalId": "00uq84luw5296rJJa1d7",
"groups": [],
"id": "e82aa505-ff44-4c26-9617-3f513eaea003",
"locale": null,
"meta": {
"resourceType": "User"
},
"name": {
"familyName": "SCIM 004",
"givenName": "Test",
"middleName": null
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:scim:schemas:core:1.0"
],
"userName": "Test.SCIM04@fabiosantos.co.uk"
}
Assign a Push Group - Assign the new Okta Group to the application in Okta under the Push Groups tab, by clicking on Push Groups > Find groups by name or Push Groups > Find groups by role and leave the "Push group memberships immediately".
Okta will first perform a POST to create the group in the target system and then perform a PUT to update its group memberships.
POST /scim/v2/Groups HTTP/1.1
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
],
"displayName": "SCIM-Group-02",
"members": [
{
"value": "a16ff245-c582-482a-9040-943281e50b8d",
"display": "test.scim02@fabiosantos.co.uk"
}
]
}
HTTP/1.1 201 CREATED
Server: Werkzeug/3.1.3 Python/3.13.0
Date: Tue, 23 Sep 2025 10:17:13 GMT
Content-Type: application/json
Content-Length: 336
Connection: close
{
"displayName": "SCIM-Group-02",
"id": "4a3e0dad-7e9a-4804-a101-7de2cea49135",
"members": [
{
"display": "test.scim02@fabiosantos.co.uk",
"value": "a16ff245-c582-482a-9040-943281e50b8d"
}
],
"meta": {
"resourceType": "Group"
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
]
}
PUT /scim/v2/Groups/4a3e0dad-7e9a-4804-a101-7de2cea49135 HTTP/1.1
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
],
"id": "4a3e0dad-7e9a-4804-a101-7de2cea49135",
"displayName": "SCIM-Group-02",
"members": [
{
"value": "a16ff245-c582-482a-9040-943281e50b8d",
"display": "test.scim02@fabiosantos.co.uk"
}
]
}
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.13.0
Date: Tue, 23 Sep 2025 10:17:14 GMT
Content-Type: application/json
Content-Length: 336
Connection: close
{
"displayName": "SCIM-Group-02",
"id": "4a3e0dad-7e9a-4804-a101-7de2cea49135",
"members": [
{
"display": "test.scim02@fabiosantos.co.uk",
"value": "a16ff245-c582-482a-9040-943281e50b8d"
}
],
"meta": {
"resourceType": "Group"
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
]
}
Add a User to a Group / Remove a User from a Group - Assign a user to an Okta Group, or remove a user from an Okta Group, under Directory > Groups, search and select the target group and then assign user(s) or remove user(s).
In this case, Okta will send a HTTP PUT to perform the updates in the target system accordingly.
PUT /scim/v2/Groups/4a3e0dad-7e9a-4804-a101-7de2cea49135 HTTP/1.1
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
],
"id": "4a3e0dad-7e9a-4804-a101-7de2cea49135",
"displayName": "SCIM-Group-02",
"members": [
{
"value": "a16ff245-c582-482a-9040-943281e50b8d",
"display": "test.scim02@fabiosantos.co.uk"
}
]
}
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.13.0
Date: Tue, 23 Sep 2025 10:21:22 GMT
Content-Type: application/json
Content-Length: 336
Connection: close
{
"displayName": "SCIM-Group-02",
"id": "4a3e0dad-7e9a-4804-a101-7de2cea49135",
"members": [
{
"display": "test.scim02@fabiosantos.co.uk",
"value": "a16ff245-c582-482a-9040-943281e50b8d"
}
],
"meta": {
"resourceType": "Group"
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
]
}
Delete a Push Group - Unlink and delete a Push Group under the application Push Groups tab, then under the Push Status click on the dropdown and select Unlick pushed group > Delete the group in the target app (recommended).
Okta will send a HTTP DELETE to the SCIM server to delete the group from the target system.
DELETE /scim/v2/Groups/4a3e0dad-7e9a-4804-a101-7de2cea49135 HTTP/1.1
HTTP/1.1 204 NO CONTENT
Server: Werkzeug/3.1.3 Python/3.13.0
Date: Tue, 23 Sep 2025 10:25:26 GMT
Content-Type: text/html; charset=utf-8
Connection: close
References
During the implementation of the SCIM server, multiple sources of information were consulted to ensure compliance with the standard and compatibility with provisioning clients. These included the official SCIM RFC specifications (RFC7643 and RFC7644), vendor documentation such as Okta’s SCIM and OPP integration guide, Okta developer blog posts (e.g., How to Build a Flask SCIM Server Configured for Use with Okta), as well as community resources, technical blogs, and code samples. Leveraging these diverse references made it possible to validate design choices, address interoperability considerations, and build a functional prototype suitable for testing.
Conclusion
In conclusion, building a SCIM 2.0 server provided me with a deeper understanding of how the different components - Okta, the Okta OPP Agent, and the SCIM server itself - interact during the provisioning process. In today’s landscape, many solutions are delivered off-the-shelf, and platforms like Okta often rely on configuration rather than custom implementation, which means that the inner workings behind the scenes are frequently opaque. By implementing this project in a development environment, I was able to gain hands-on insights into the end-to-end flow, error handling, and protocol intricacies - knowledge that is rarely acquired when working solely with pre-packaged integrations. Such exercises are invaluable for building a deeper understanding of identity provisioning and for improving our ability to troubleshoot, optimise, and architect future solutions.
Disclaimer
All implementations, configurations, and examples described herein were created solely for testing and learning purposes. The content reflects the author’s personal experiences and opinions, and should not be interpreted as representing the views, positions, or endorsements of the author’s employer or of any third-party products, platforms, or companies mentioned. Readers should conduct their own due diligence before applying any of the information in a production environment.
Senior Software Engineer at Okta
1moThank you for the post. I understand that SCIM 2.0 supports entitlements; could you provide an example of that?