Simplifying Identity Automation: Building a SCIM Server for Okta On-Premises Provisioning (OPP) Agent

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.

Article content
Okta OPP Architecture

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.

Article content
Okta OPP Architecture for SCIM-compliant on-premises applications

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.

Article content
Okta OPP Architecture for non SCIM-compliant on-premises applications

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.

Article content
SCIM 2.0 Server - Dev Architecture

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:

  • A relational database like PostgreSQL or MySQL.
  • A non-relational database like MongoDB.
  • An LDAP directory like Microsoft Active Directory or OpenLDAP.
  • A custom data store or legacy application with its own API.

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:

  • Create User: When a user is assigned to the application in Okta, and the Create User option is enabled in the app, Okta sends two messages. The first one determines whether the user exists (e.g., GET /scim/v2/Users?filter=userName%20eq%20"john.doe@domain.co.uk"&startIndex=1&count=200 HTTP/1.1) in the on-premises app. If the user doesn't exist in the on-premises app, Okta sends another message to create the user (e.g., POST /scim/v2/Users HTTP/1.1 - with the respective request payload to create the user). If the user already exists in the on-premise app, Okta send another message to update the user (e.g., PUT /scim/v2/Users/9e180e52-ca3f-4af2-b19b-c48dae79cdb7 HTTP/1.1 - with the respective request payload to update the existing user's attributes).
  • Update User: When a user's profile in Okta changes and Update User Attributes is enabled in the app, Okta sends the request to the SCIM server with instructions to update the user (e.g., PUT /scim/v2/Users/9e180e52-ca3f-4af2-b19b-c48dae79cdb7 HTTP/1.1 -with the respective request payload to update the user's attributes).
  • Deactivate User: When a use is unassigned from the application in Okta and the Deactivate Users is enabled in the app, Okta sends the request to the SCIM server with instructions to deactivate the user (e.g., PUT /scim/v2/Users/9e180e52-ca3f-4af2-b19b-c48dae79cdb7 HTTP/1.1 -with the respective request payload to deactivate the user).
  • Get Users: To import users from the downstream application into Okta, the SCIM server needs to implement a function to retrieve users (e.g., GET /scim/v2/Users HTTP/1.1).
  • Get User: The SCIM server also needs to implement a function to retrieve a single user's profile (e.g., GET /scim/v2/Users/9e180e52-ca3f-4af2-b19b-c48dae79cdb7 HTTP/1.1).

Screenshot from the Provisioning to App options / features mentioned above.

Article content
Provisioning to App: Features

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:

  • Create Group: When a group is selected as Push Group to the application in Okta, unlike users, Okta will not perform a search operation to determine if the group already exists in the target application or not. In this case, it's up to the SCIM server to handle this situation. Okta will send a message to create the group (e.g., POST /scim/v2/Groups HTTP/1.1 - with the respective request payload to create the group). It's recommended to create the logic inside the function, that handles the user creation, to search for an existing group, and if a group with the same name already exists, return the same to Okta. After the creation of the group is complete, Okta will send another message to update the group's members (e.g., PUT /scim/v2/Groups/4e00b3bb-fc9a-4094-9752-ab0eadd92ceb HTTP/1.1 - with the respective request payload to update the group's attributes and members).
  • Update Group: When a pushed group's profile or memberships in Okta changes, Okta sends the request to the SCIM server with instructions to update the group accordingly (e.g., PUT /scim/v2/Groups/4e00b3bb-fc9a-4094-9752-ab0eadd92ceb HTTP/1.1 - with the respective request payload to update the group's attributes and members).
  • Get Group: The SCIM server also needs to implement a function to retrieve a single group's profile (e.g., GET /scim/v2/Groups/4e00b3bb-fc9a-4094-9752-ab0eadd92ceb HTTP/1.1).
  • Delete Group: When unlinking a pushed group, Okta gives the administrator the option to either 'Delete the group in the target app' or 'Leave the group in the target app'. If the deletion option is selected, Okta will send a HTTP DELETE (e.g., DELETE /scim/v2/Groups/7bda689b-6794-4049-8445-fd0f606eb75c HTTP/1.1) to the SCIM server.

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:

  • Authentication: Which authentication schemes it accepts (e.g., OAuth Bearer Tokens).
  • Limitations: The maximum number of results it can return in a single request or the maximum size of a bulk payload.
  • User Management Capabilities: The JSON payload also needs to provide the list of features / capabilities supported by the SCIM server. This is used by Okta to enable or disable features for a given application's provisioning settings. The table indicates the list of Provisioning Features and the corresponding values in the response from the Service

Article content
Okta OPP Provisioning Features

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.

Article content
Test Connector Configuration: Provisioning 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"
    ]
  }
}        

  • /ResourceTypes - This endpoint is required by the Okta OPP Agent to allow clients to discover the types of SCIM resources supported by the service provider, such as Users and Groups. This endpoint provides metadata about each resource, including its schema and endpoint location, enabling the OPP Agent to correctly interpret and interact with the SCIM API during provisioning tasks.

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"
      }
    }
  ]
}        

  • /Schemas - This endpoint is required by the Okta OPP Agent to provide detailed definitions of the attributes supported for each SCIM resource, such as Users and Groups. This metadata enables the OPP Agent to understand the structure, data types, mutability, and constraints of each attribute, ensuring that provisioning requests are properly formatted and interpreted by the SCIM service provider.

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:

  • TLS Client Authentication
  • Bearer Tokens
  • Basic Authentication

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.

  1. The OPP Agent must be able to connect to Okta (e.g., https://yourorg.okta.com) over the internet to receive user and group lifecycle management events. This connection is outbound and uses HTTPS.
  2. The OPP Agent must be able to connect to the on-premises SCIM server it is configured to manage. This communication typically occurs over a local network connection, allowing the agent to securely translate Okta's provisioning requests into actions on the SCIM server, all without exposing the private SCIM endpoint to the public internet.

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.

Article content
Okta On-premise Agents

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.

Article content
AIW - Application Integration Wizard

Once the application instance is created, you need to select On-Premises Provisioning, under the General tab, for the Provisioning option.

Article content
Provisioning: On-Premises Provisioning

Then, under the Provisioning tab, under the Integration section, you need to provide all the details to establish the connection with the SCIM server:

  • SCIM connector based URL (e.g., https://domain.com/scim/v2/)
  • Authorization type (e.g., HTTP Header)
  • HTTP header name and value (e.g. Bearer 123456789)
  • Unique user field name (e.g. userName)
  • Connect to these agent: Select the agent that you want the requests to go through

Article content
Integration Connector Configuration

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.

Article content
Test Connector Configuration

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:

  • Create Users
  • Update User Attributes
  • Deactivate Users

Article content
Provisioning to App 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.

Article content
SCIM Server: User Profile Schema

Once the User Profile for the SCIM Server contains all the required attributes, you can configure the attribute mappings between Okta and the application.

Article content
SCIM Server: Attribute mappings from the Okta User to the SCIM Server

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.

  • HTTP GET Request

GET /scim/v2/Users?filter=userName%20eq%20%22Test.SCIM04%40fabiosantos.co.uk%22&startIndex=1&count=200 HTTP/1.1        

  • HTTP GET Response

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.

  • HTTP POST Request

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 POST Response

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.

  • HTTP GET Request

GET /scim/v2/Users?filter=userName%20eq%20%22Test.SCIM04%40fabiosantos.co.uk%22&startIndex=1&count=200 HTTP/1.1        

  • HTTP GET Response

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.

  • HTTP PUT Request

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 PUT Response

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.

  • HTTP PUT Request

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 PUT Response

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..).

  • HTTP PUT Request

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 PUT Response

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.

  • HTTP POST Request

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 POST Response

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"
  ]
}        

  • HTTP PUT Request

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 PUT Response

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.

  • HTTP PUT Request

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 PUT Response

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.

  • HTTP DELETE Request

DELETE /scim/v2/Groups/4a3e0dad-7e9a-4804-a101-7de2cea49135 HTTP/1.1        

  • HTTP DELETE Response

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.


shahid shan

Senior Software Engineer at Okta

1mo

Thank you for the post. I understand that SCIM 2.0 supports entitlements; could you provide an example of that?

To view or add a comment, sign in

More articles by Fabio Santos

Others also viewed

Explore content categories