Building a Custom SCIM Server in Okta
A friendly, ground-up guide to creating your own SCIM service so that Okta can create, update, and remove people and groups inside an application that has no ready-made connector. You will learn what SCIM is, what Okta sends, what your server must answer, and how to design, secure, test, and run it.
Imagine you run a large library, and every September hundreds of new students join. A clerk at the front desk writes each name in a book, hands out a card, and gives access to the right rooms. In June, when students leave, the same clerk crosses their names out and takes the cards back. Now imagine you have fifty libraries in different towns, each with its own book, its own card style, and its own way of crossing names out. The clerk would need fifty different habits. Life would be easier if every library agreed on one standard form for “add a student,” “change a student,” and “remove a student.”
SCIM is that standard form for the software world. Okta is the clerk at the front desk of your company. Each business application is one library. When a new employee joins, Okta wants to tell every app, in one common language, “Please create this person.” When the employee leaves, Okta wants to say, “Please switch this person off.” Many popular apps already speak this language and have a ready-made connector. Many others, especially your own in-house applications, do not. For those, you build a small service that listens for Okta’s requests and does the right thing inside your app. That service is a custom SCIM server.
This guide teaches you how to build one, in words and pictures, with no code. You do not need any prior knowledge. Every new term is explained when it first appears, and every big idea comes with an everyday comparison. By the end, you will understand what Okta sends, what your server must answer, how to store and match people, how to keep the service safe and fast, and how to find the cause when something goes wrong.
This guide describes ideas and typical behavior in plain words rather than exact code or exact screen names. Okta changes its products over time, and details such as which requests are sent, which authentication choices exist, and which settings appear can vary by release, license, and integration type. Always confirm exact requirements in the official Okta provisioning documentation and in the SCIM standards before you build or go live.
1Why Build a Custom SCIM Server at All?
Before learning how, it helps to understand why anyone would take on this work.
Every company has a long list of people who should have access to its applications. That list changes every day. People join, change teams, change names, take leave, and leave for good. If someone updates each application by hand, three bad things happen. Work is slow, mistakes creep in, and former employees keep access long after they should have lost it. That last problem is a real security risk.
The problem a custom server solves
Okta can keep applications up to date automatically. This is called provisioning. For popular products such as large email and chat tools, Okta already has a built-in connector in its catalog. You switch it on and it works. But your company may also have a homegrown claims system, an old internal portal, a partner extranet, or a new product you are building. Okta has no ready-made connector for these, because Okta has never seen them.
You have two choices. You can keep updating the app by hand, or you can give the app a front door that speaks the standard language Okta already understands. Building that front door is what this guide is about.
Why the standard language is a gift
If you invented your own private language, you would have to teach it to every tool that needs to talk to your app. Because SCIM is a public standard, you only teach your app once. Okta speaks it, and many other identity tools do too. The same server that works for Okta today could work with another identity product tomorrow.
Think of the electric plug in your wall. Every lamp, fan, and phone charger uses the same plug shape, so you can move them from room to room. A custom SCIM server is a plug socket you install on your own app so that Okta’s standard plug fits. You do not rebuild the lamp. You only fit the socket.
When it is the wrong answer
A custom server is not always the best choice. If the app already has a catalog connector, use it. If the app has a different supported way to receive users, such as a file import or a vendor-provided connector, that may be simpler. If only five people ever use the app and they rarely change, hand updates may be cheaper than building and running a service. Later chapters give a decision guide to help you choose.
Ask one question first: “Does Okta already have a way to talk to this app?” Only when the answer is no does a custom server become worth its cost.
2Core Concepts You Need First
A small set of words appears again and again in this topic. Learn them once and the rest reads easily.
Identity
An identity is the digital record of a person or a thing that needs access. It holds facts such as a name, an email address, a department, and a status. Think of an identity as a name tag with extra details written on the back.
Provisioning and deprovisioning
Provisioning means creating and keeping accounts up to date in applications. Deprovisioning means switching accounts off or removing them when they are no longer needed. Together they are the “joiner, mover, leaver” story of every employee. Joiner means a new person arrives. Mover means they change roles. Leaver means they depart.
Identity provider and service provider
In this story, Okta is the identity provider. It is the trusted source of truth about people. Your application is the service provider. It consumes accounts and uses them to decide what each person can do. In SCIM language, Okta plays the role of the client, which sends requests, and your server plays the role of the service provider, which answers them.
SCIM
SCIM stands for System for Cross-domain Identity Management. It is a pair of public standards that describe how to represent users and groups, and how to create, read, change, and delete them over the web. It is built on ordinary web calls and a common data format called JSON, which is simply a tidy way of writing facts as labeled pairs. There are two versions, 1.1 and 2.0, and 2.0 is the one most modern systems use.
Resource
A resource is a thing SCIM can manage. The two everyday resources are users and groups. Each resource has a unique identifier, a set of attributes, and some bookkeeping information such as when it was created.
Schema
A schema is a description of what a resource looks like: its allowed attributes, the type of each one, and whether it is required. Think of a schema as the printed layout of a form. It says which boxes exist and what kind of thing belongs in each.
Endpoint
An endpoint is a web address where your server listens for one kind of request. There is typically one for users, one for groups, and a few that describe the server itself.
Idempotent
An action is idempotent when doing it twice has the same result as doing it once. Switching a lamp to “off” is idempotent. Pressing a “toggle” button is not. Good provisioning servers aim to be idempotent wherever possible, because networks can repeat requests.
| Term | Plain meaning | Everyday comparison |
|---|---|---|
| Provisioning | Creating and updating accounts | Handing out and updating library cards |
| Deprovisioning | Switching off or removing accounts | Collecting the card at the end of the year |
| SCIM | Standard way to manage users and groups | A common form every library accepts |
| Resource | A managed thing such as a user | One page in the book |
| Schema | The layout of a resource | The printed boxes on the form |
| Endpoint | Address that handles one request type | A desk for one kind of task |
| Idempotent | Safe to repeat | Setting a switch to off |
Think of a hotel. The booking company is Okta, keeping the master list of guests. The hotel is your app, which must actually make rooms ready. SCIM is the standard booking form both sides agree on, so any booking company can reserve a room at any hotel.
3How Okta Fits In: The Client and the Server
The most important idea in this guide is who talks and who listens. Okta talks. Your server listens and answers.
Okta is the caller
Your server never decides on its own to create a person. It waits. When something happens in Okta, such as assigning a person to an application, Okta sends a request to your server. Your server does the work and replies with the result. This means you do not have to teach your server about Okta’s inner workings. You only have to teach it to answer standard SCIM requests correctly.
Where the connection is configured
In Okta’s Admin Console, you create an application integration for your app, then switch on provisioning for it. You tell Okta the base address of your SCIM server, how to authenticate to it, and which actions you want Okta to perform, such as creating users, updating attributes, and deactivating users. The exact path through the console, and the options offered, depend on your Okta release and license, so confirm them in the documentation.
The cast of characters
Okta Universal Directory
Holds the master record of every person and their facts.
Okta provisioning engine
Notices changes and sends SCIM requests for them.
Secure web connection
Carries requests over encrypted web traffic.
Your SCIM service
Receives requests, checks them, and talks to your app.
Your application
Owns the real accounts and decides what people can do.
Logs on both sides
Show what was asked, what was answered, and when.
flowchart LR
HR["HR system
source of truth"] --> OKTA["Okta
Universal Directory"]
OKTA -->|"Assignment or change"| ENG["Okta provisioning engine"]
ENG -->|"SCIM requests over HTTPS"| SRV["Your custom SCIM server"]
SRV -->|"Create, update, switch off"| APP["Your application and its data"]
SRV -->|"Replies with results"| ENG
Think of a translator at an international meeting. The speaker (Okta) talks in a standard language. The translator (your SCIM server) turns it into the local language of the listener (your app) and carries the answer back. The translator never starts the conversation. They only respond to what is said.
Your server’s job is translation and honesty. Translate the standard request into your app’s actions, and report truthfully what happened.
4The SCIM Protocol in Plain Words
SCIM looks big in its standards, but its daily use is a small set of ordinary web actions.
Four everyday actions
SCIM is built on the ordinary web. Each request has a verb that says what the caller wants and an address that says which thing it is about. The common verbs are these.
- Get asks to read something, such as one user or a list of users.
- Post asks to create something new.
- Put asks to replace a whole record with a new version.
- Patch asks to change only some parts of a record.
- Delete asks to remove a record.
The common addresses
A SCIM server has a base address. After it, standard names identify what is being managed. One name leads to users, another to groups. Additional names describe the server itself: what it supports, what resource types it offers, and what attribute layouts it uses. Those descriptive endpoints let a caller discover capabilities without guessing.
The shape of the data
Facts travel as JSON, a plain-text format of labeled values. A user record has an identifier chosen by your server, a user name that people sign in with, a name block, email addresses, an active flag, and more. Every record also carries a list of schema labels that announce which layouts it follows, and a meta block with created and modified times.
How replies communicate
Every reply carries a status number. Numbers beginning with two mean success. For example, one number means “found and here it is,” another means “created,” and another means “done, nothing to return.” Numbers beginning with four mean the caller made a mistake, and numbers beginning with five mean your server failed. Chapter 12 goes through the important ones.
sequenceDiagram
participant O as Okta
participant S as Your SCIM server
participant A as Your application
O->>S: Ask for the user with this user name
S->>A: Look up the account
A-->>S: Not found
S-->>O: Empty list of results
O->>S: Create this user
S->>A: Make the account
A-->>S: Account created with an identifier
S-->>O: Created, here is the user with its identifier
Think of ordering at a counter. You say what you want (the verb), you point at the menu item (the address), you hand over details (the data), and the cook replies with “ready,” “sold out,” or “sorry, the kitchen is on fire” (the status). SCIM is just a polite, standard way of placing the order.
If you can read a menu and understand a receipt, you can understand SCIM. Do not be put off by the standards documents. Most of your work uses a small slice of them.
5The User Resource: What a Person Looks Like
The user record is where most of your effort goes. Understanding its parts will save you hours later.
The identifier
Each user has an identifier created by your server, not by Okta. It must be unique, stable, and never reused. Okta stores it and uses it in later requests to find the same person again. Think of it as the number on the library card. A person’s name might change, but the card number stays.
The user name
The user name is the human-friendly handle, often an email address. It must be unique in your app, and Okta relies on it to check whether a person already exists. Because the first question Okta usually asks is “does this user name already exist,” your server must search by user name quickly and accurately.
Names, emails, and the active flag
The name block holds given name, family name, and sometimes a display name. Emails are a list, where each entry has a value, a type such as work, and an optional primary marker. The active flag is a simple true or false that says whether the account is enabled. This tiny flag matters enormously: it is how Okta switches a leaver off without destroying their history.
Schemas and extensions
The core user layout covers common facts. Real companies also need department, manager, cost center, and so on. The standard provides an enterprise extension for many of these. For facts unique to your app, such as a clearance level, you can define a custom extension with its own schema label. Okta can map its own attributes to these, with your choices made in the application’s provisioning settings.
Meta information
Every record carries a meta block: the resource type, when it was created, when it was last modified, and a version tag. Good servers fill these in correctly. They help callers tell whether their copy is out of date.
| Part of a user | Who sets it | Why it matters |
|---|---|---|
| Identifier | Your server | Lets Okta find the same person again |
| User name | Okta, from its mapping | Used to check whether a person already exists |
| Name and emails | Okta, from its mapping | Shown to people and used for contact |
| Active flag | Okta | Enables or disables the account |
| Extension facts | Okta, if mapped | Carry department, manager, and app-specific data |
| Meta block | Your server | Tracks creation, change, and version |
flowchart TD
U["User resource"] --> ID["Identifier
set by your server"]
U --> UN["User name
unique handle"]
U --> NM["Name block"]
U --> EM["Emails list"]
U --> AC["Active flag"]
U --> EX["Extensions
department, manager, custom"]
U --> MT["Meta
times and version"]
Think of a passport. It has a number that never changes (identifier), a name you are known by (user name), personal details, a stamp saying whether it is valid (active flag), and extra pages for visas (extensions). Each part has a distinct job.
Never use a person’s email address as the identifier. People change emails. The identifier must outlive every other fact about the person.
6The Group Resource: Collections of People
Groups let Okta manage access for many people at once.
What a group is
A group is a named collection with an identifier, a display name, and a list of members. Each member entry points to a user by identifier. If your application uses groups, teams, or roles to decide what people can do, Okta can send them to your server so that membership changes in Okta flow into your app.
Why groups are optional
Not every app needs group support. If your app only needs to know “is this person allowed in,” user provisioning is enough. If your app has roles such as viewer, editor, and administrator, group support lets Okta assign people to roles without anyone touching your app. Okta’s feature for sending groups is commonly called pushing groups, and it is usually enabled separately from user provisioning. Confirm the availability for your integration type.
Membership changes
Group updates usually arrive as changes to the member list: add these people, remove those people. These are typically sent as patch requests that touch only the members, so your server must understand partial changes instead of expecting a whole replacement.
Groups as roles in your app
A good design decision is to treat each group as a role in your application. The group display name maps to a role name, and membership maps to who holds that role. Keeping this mapping simple makes both the server and the Okta configuration easy to understand.
Think of a school class list. Rather than telling the cafeteria about each child one by one, you hand over “Class 5B” and say who is in it. When a child joins or leaves, you only update the list. Groups do the same for access.
Start with users only. Add groups later, once the user flow is stable. Each feature you add is one more thing to test and secure.
7The Account Lifecycle: What Okta Asks Your Server to Do
Every account in your app goes through a life: it is born, it changes, it rests, and it is retired. Okta can drive each stage.
Create
When a person is assigned to your application in Okta, Okta decides your app needs an account for them. It typically first asks whether the user name already exists. If not, it sends a request to create the user. If your server says the user already exists, Okta may link to the existing account instead, depending on how the integration is configured. This “look first, then create” habit protects you from duplicates.
Update
When facts change in Okta, such as a new department or a changed surname, Okta sends an update so your app stays in step. Updates may be sent as whole replacements or as partial changes. Your server should be ready for both, because Okta’s behavior can depend on the SCIM version and the settings of the integration.
Deactivate
When a person is unassigned from the app, or their Okta account is suspended or deactivated, Okta tells your server to switch the account off. This usually means setting the active flag to false. It does not normally mean deleting the record. Keeping the record preserves history, ownership of documents, and audit trails.
Reactivate
If the person returns, Okta can switch the account back on by setting the active flag to true again. Your server must support this cleanly, restoring the same account rather than creating a new one.
Import
Sometimes the flow runs the other way. Okta can read the list of users that already exist in your application so it can match them with people in Okta. This helps when an app has years of accounts that were created before Okta was introduced. Import requires your server to list users with paging, covered in Chapter 9.
Delete
Okta commonly prefers switching accounts off over deleting them. Some integrations can be set up to delete, and a server should still handle a delete request gracefully, but you should plan on the active flag being the main off-switch. Confirm in the Okta documentation how your integration treats removal.
Assign
An administrator or a rule assigns the person to your app in Okta.
Look up
Okta asks your server if this user name already exists.
Create or link
If new, your server creates the account. If it exists, Okta links to it.
Keep in step
Changes in Okta are sent as updates and applied by your server.
Switch off
When access ends, Okta sets the active flag to false.
Bring back
If the person returns, the same account is switched on again.
stateDiagram-v2
[*] --> NotInApp
NotInApp --> Active : "Create"
Active --> Active : "Update facts"
Active --> Inactive : "Deactivate"
Inactive --> Active : "Reactivate"
Inactive --> Removed : "Delete, if used"
Removed --> [*]
Think of a phone line. A new person gets a number (create). They may change their plan (update). When they travel abroad for a year, the line is paused (deactivate), and it can be resumed later (reactivate). Only rarely is the number given away for good (delete). Okta treats accounts the same careful way.
The “switch off” path is the most important one for security. A server that creates beautifully but fails to disable correctly leaves former employees with working accounts.
8The Endpoints Your Server Should Offer
Think of your server as a building with a few desks. Each desk handles one kind of request.
The users desk
This is the busiest desk. It must support searching for a user by user name, creating a user, reading one user by identifier, replacing or partially changing a user, and, ideally, removing one. It must also be able to list users with paging for imports.
The groups desk
Only needed if you support groups. It handles creating groups, reading them, changing their members, and removing them. Members are referenced by user identifier.
The descriptive desks
The standard also describes endpoints that tell callers about the server. One lists the features you support, such as whether you accept partial changes, filtering, and paging, and which sign-in methods you accept. Another lists the types of resource you offer. Another lists the attribute layouts. Not every caller reads these, and Okta’s behavior around them can depend on the integration, but providing them makes your server easier to test and adopt, and is part of being a good SCIM citizen.
| Desk | What it handles | Needed for Okta? |
|---|---|---|
| Users: search | Find by user name or list with paging | Yes, for existence checks and imports |
| Users: create | Make a new account | Yes, for onboarding |
| Users: read one | Return an account by identifier | Yes, commonly |
| Users: update | Replace or partly change an account | Yes, for keeping in step |
| Users: switch off | Set the active flag to false | Yes, essential |
| Groups | Create groups and change members | Only if you push groups |
| Service description | Declare supported features | Recommended |
| Resource types and schemas | Declare layouts | Recommended |
flowchart TD
BASE["SCIM base address"] --> USERS["Users desk"]
BASE --> GROUPS["Groups desk"]
BASE --> DESC["Descriptive desks"]
USERS --> U1["Search"]
USERS --> U2["Create"]
USERS --> U3["Read"]
USERS --> U4["Update"]
GROUPS --> G1["Create"]
GROUPS --> G2["Change members"]
DESC --> D1["Service features"]
DESC --> D2["Resource types"]
DESC --> D3["Schemas"]
Think of a post office with separate windows for parcels, letters, and enquiries. A visitor who goes to the right window gets quick service. A visitor who finds only one window for everything gets confused. Clear, separate desks make your server predictable.
Build the users desk first and test it thoroughly. Add the descriptive desks next, because they are cheap and helpful. Add groups last.
9Searching, Filtering, and Paging
Finding the right person quickly, and listing thousands of people politely, are two of the most important skills of a SCIM server.
Why searching matters
Before creating anybody, Okta typically asks whether a user with a given user name already exists. This question is asked with a filter, a short condition saying “user name equals this value”. If your server cannot answer it correctly, Okta cannot protect you from duplicates. At the least, support the “equals” test on the user name, and ideally on a few other attributes.
Case, spaces, and exactness
User names are often email addresses, and email addresses are usually treated without regard to capital letters. Decide whether your user name comparison is case sensitive, and be consistent. The standard says that for the user name, comparison is case insensitive. A search for “Asha@example.com” and “asha@example.com” should find the same person. Remember to trim stray spaces before comparing as well.
What a search reply looks like
A search returns a list wrapper: a total count of matches, the starting position, the number of items in this page, and the list of resources. An empty result is not an error. It is a successful reply with a total of zero. Okta reads that as “no such person, go ahead and create.”
Why paging exists
If your app has fifty thousand users, you cannot return them in one reply. The reply would be huge and slow, and could crash the caller. Paging solves this. The caller asks for a number of items starting from a position, and your server returns just that slice. The caller then asks for the next slice, and so on. SCIM numbers positions starting at one, not zero. This difference between the standard and many programming habits is a frequent source of off-by-one mistakes.
Keeping order stable
Paging works only if the order of results does not change between requests. If new users are inserted while someone is paging, items can be skipped or repeated. Sort by a stable key such as creation order or identifier, so each slice picks up exactly where the last one ended.
Protecting yourself
Set a maximum page size. If a caller asks for ten million items at once, return your maximum and tell them how many items you actually returned. Also be careful with filters: accept only the forms you support and reject the rest with a clear error, rather than risking slow or unsafe searches.
sequenceDiagram
participant O as Okta
participant S as Your SCIM server
O->>S: List users, start at 1, up to 100
S-->>O: 100 users, total 250
O->>S: List users, start at 101, up to 100
S-->>O: 100 users, total 250
O->>S: List users, start at 201, up to 100
S-->>O: 50 users, total 250
Note over O,S: The caller stops when it has seen the total
Think of reading a big book through a bookmark. You read a few pages, place the bookmark, close the book, and later open exactly where you stopped. Paging is the bookmark. If someone tears out pages between your visits, the bookmark no longer points to the right place, which is why stable order matters.
SCIM positions start at one. If your server treats the first item as position zero, every page will be shifted and the first person on each page will be missed.
10Replace or Patch: Two Ways to Update
Updating a record can mean “here is the whole new version” or “change just these parts”. Your server must understand both ideas.
Replace
A replace request sends the complete user. Your server overwrites what it had with what it received. Anything the caller left out is treated as removed or reset. This is simple, but dangerous if the caller does not know about attributes that your server keeps. For example, if your app stores a local preference that Okta never sends, a naive replace could erase it. Decide in advance which attributes belong to Okta and which belong to your app, and leave the second group alone.
Patch
A patch request lists operations: add this value, remove that value, replace this attribute. It touches only what it names. This is efficient and safer, but it requires your server to understand the standard way of pointing at parts of a record, including paths into lists such as emails or group members.
What Okta tends to send
Okta can use replace for general profile updates and patch for specific tasks such as switching an account off or changing group membership, though exact behavior depends on SCIM version and integration. The safest approach is to support both on your users desk. A server that accepts only one will break in unexpected situations.
Idempotent updates
Whichever you support, an update should be safe to repeat. Setting the active flag to false twice must not fail the second time. Adding a member who is already in the group should not create a duplicate entry, and removing a member who is not in the group should not raise an alarm.
Reading what you wrote
After a successful update, return the full current record. Okta can use it to confirm what your server actually stored. If your server silently ignores an attribute it does not support, the returned record will show it, and the mismatch becomes visible rather than hidden.
Whole new version
Simple to understand. Risky if your app holds extra facts.
Targeted changes
Efficient and safer, but needs path handling.
Support both
Okta may use either, depending on task and settings.
Repeatable
Applying the same change twice leaves the same result.
Think of editing a document. Replace is handing over a brand new copy and throwing away the old one. Patch is writing “change the date on page two” on a sticky note. If you only hand over new copies, careful notes your colleague added in the margin of the old one are lost.
Write down which attributes Okta owns. Replace and patch should change only those. Everything else in your app belongs to your app.
11Authentication: Proving Okta Is Really Okta
Your SCIM server can create and disable accounts. That makes it one of the most sensitive doors in your system. It must never open for strangers.
Why this is so serious
Anyone who can talk to your SCIM server as if they were Okta can create an administrator account, or disable everyone. Protecting this door is not optional. Every request must prove who it comes from before anything happens.
The common choices
Okta can authenticate to a SCIM server in several ways, and the choices on offer depend on the integration type and release. Typically they include a secret token sent in a request header, sign-in with a username and password, and OAuth 2.0 where Okta first obtains a short-lived access token. Check the current list in your Okta console and documentation.
| Method | How it works | Strength | Weakness |
|---|---|---|---|
| Static bearer token | A long secret is sent with each request | Simple to set up | If leaked, valid until changed |
| Username and password | Credentials are sent with each request | Familiar | Passwords are often weak or reused |
| OAuth 2.0 token | A short-lived token is obtained and renewed | Limited lifetime, better control | More to build and operate |
Building a safe check
Your server should reject any request without valid credentials, using the standard “not authenticated” status, before doing any work. Compare secrets in a way that does not reveal how close a wrong guess was. Keep the secret out of logs. Store it in a proper secret vault, not in a file next to the code.
Rotation
Secrets should change on a schedule, and immediately if you suspect a leak. Design for rotation from the beginning: allow two valid secrets at once during a handover window, so you can change one side first without breaking provisioning.
Transport security
Always use encrypted web traffic. Okta expects a secure address for your SCIM base. Use a certificate from a trusted authority, and keep it renewed. An expired certificate can silently stop all provisioning.
sequenceDiagram
participant O as Okta
participant S as Your SCIM server
participant V as Secret vault
O->>S: Request with credentials
S->>V: Fetch the expected secret
V-->>S: Secret value
alt Credentials match
S-->>O: Process request and reply
else Missing or wrong
S-->>O: Not authenticated
end
Think of a night guard at the back door of a bank. The guard asks for the passcode before opening, even for people who say they are cleaners. The passcode is changed regularly, and the guard never writes it on the door. Your SCIM server must be that guard.
Do not leave the SCIM address open to the whole internet without authentication “just for testing”. Test systems get forgotten and become real attack doors.
12Status Codes and Error Replies
When something goes wrong, your reply must tell Okta exactly what happened, so Okta can retry, give up, or show an administrator a useful message.
The success family
A successful read returns a “found” status with the record. A successful create returns a “created” status with the new record and its identifier. A successful removal returns “done with nothing to show”. Using the right success status matters, because Okta reads it to decide what to do next.
The caller-mistake family
When the request is wrong, reply with a status beginning with four. Bad data in the request means “invalid request”. A missing or wrong secret means “not authenticated”. Trying to create something that already exists means “conflict”. Asking for something that is not there means “not found”. Asking too often means “too many requests”. Each one tells the caller something different about what to do.
The server-mistake family
When your server itself fails, reply with a status beginning with five. This tells Okta the fault is on your side and that retrying later might help. Never use a server-error status for a caller mistake, and never use a caller-mistake status for your own crash. The caller will make the wrong decision.
The error body
SCIM defines a standard error body with a schema label, a status, and a human-readable detail message. Okta can show this detail to administrators in its logs. Write the detail message so that a tired administrator at midnight can understand it without opening your source code. Say which attribute was wrong and what was expected.
| Situation | Reply meaning | What Okta should do |
|---|---|---|
| User found | Success with record | Use the record |
| User created | Created with record | Remember the identifier |
| No such user in search | Success with empty list | Proceed to create |
| Duplicate user name | Conflict | Link or report an administrator |
| Bad attribute value | Invalid request | Report; retry will not help |
| Wrong or missing secret | Not authenticated | Report a connection problem |
| Too many requests | Slow down | Wait and retry |
| Your server crashed | Server error | Retry later |
flowchart TD
R["Request arrives"] --> AU{"Authenticated?"}
AU -->|"No"| E1["Reply: not authenticated"]
AU -->|"Yes"| VA{"Request valid?"}
VA -->|"No"| E2["Reply: invalid request with detail"]
VA -->|"Yes"| EX{"Conflicts or missing?"}
EX -->|"Duplicate"| E3["Reply: conflict"]
EX -->|"Not there"| E4["Reply: not found"]
EX -->|"Fine"| DO["Do the work"]
DO --> OK{"Worked?"}
OK -->|"Yes"| S1["Reply: success"]
OK -->|"No"| E5["Reply: server error"]
Think of a vending machine. “Insert more coins” tells you what you did wrong. “Sold out” tells you the item is not there. “Out of service” tells you the machine is broken. Each message leads you to a different action. Your status codes should be as clear as those messages.
Include the attribute name and the reason in your error detail, such as “the department value is longer than the limit of fifty characters”. It will save hours of guessing.
13Architecture and Components of a SCIM Server
A good server is built in layers, each with one job. This makes it easier to test, fix, and grow.
The front door layer
This layer receives web requests. It checks the secure connection, confirms the caller’s credentials, and applies limits on how fast a caller may ask. It does not know anything about users. It only decides whether a request may come in.
The protocol layer
This layer understands SCIM. It reads the verb and address, decodes the data, checks that the request follows the standard layout, and builds standard replies and error bodies. It translates between the outside world’s SCIM language and your inside world’s plain operations.
The business layer
Here live your rules. Which user names are allowed? What happens when someone is switched off? Which attributes does Okta own? Which roles does a group map to? Keeping these rules in one place means you can change them without touching the protocol code.
The data layer
This layer talks to the database or to your application’s own interfaces. It creates, finds, updates, and disables accounts. It also stores the mapping between SCIM identifiers and your internal record numbers, if they differ.
The supporting parts
Around these layers sit logging, which records what happened; metrics, which count and time things; configuration, which holds settings and secrets; and a queue or retry helper, if some work has to happen later or in the background.
flowchart TD
OK["Okta"] -->|"HTTPS"| FD["Front door
connection, credentials, limits"]
FD --> PL["Protocol layer
SCIM parsing and replies"]
PL --> BL["Business layer
rules and mapping"]
BL --> DL["Data layer
database or app interfaces"]
DL --> APP["Your application data"]
FD -.-> LOG["Logging and metrics"]
PL -.-> LOG
BL -.-> LOG
DL -.-> LOG
CFG["Configuration and secrets"] -.-> FD
CFG -.-> BL
Front door
Decides who may enter and how fast.
Protocol
Speaks SCIM: parse, validate, reply.
Business
Holds your rules and attribute ownership.
Data
Reads and writes real accounts.
Think of a restaurant. The host at the door decides who comes in (front door). The waiter understands the menu language and writes the order (protocol). The head chef decides how dishes are made (business). The kitchen hands do the cooking (data). Each person has a job, and the restaurant runs smoothly because no one does everyone else’s job.
When the standard changes, you edit the protocol layer. When your app changes, you edit the data layer. When your rules change, you edit the business layer. Each change stays small.
14Designing the Data Model and Storage
Your server needs a reliable memory. How it stores people decides how fast and how safe it is.
Reuse your application’s accounts
Often the best design is for the SCIM server to read and write your application’s own user table, not to keep a second copy. A second copy can drift out of date and lead to confusing disagreements. If you must keep extra data, such as the identifier Okta knows, store it next to the account in the same place.
Three things worth storing
- A stable external identifier that you hand to Okta. It never changes and is never reused, even after deletion.
- A normalized user name used for fast, case-insensitive searches, kept in a column or field with a unique rule.
- Timestamps and a version tag for created and last-modified, so callers can tell when a record changed.
Uniqueness belongs in the database
Do not rely only on your code to prevent duplicates. Two requests arriving at the same moment can both pass a “does it exist” check and then both create the person. A unique rule enforced by the database blocks the second one. Your code should catch that failure and answer with a conflict reply.
Soft states over deletion
Prefer a simple “enabled or disabled” state over removing rows. Disabled accounts keep their history. Deletion destroys evidence and can orphan data owned by that person. If privacy laws require erasure, handle it as a deliberate, separate process, not as a side effect of deprovisioning.
Groups and membership
If you support groups, store the group, the members, and the link between them. A link table that holds one row per member per group is simple and works well. Make sure removing a user also tidies the links, so no ghost memberships remain.
flowchart LR
USR["User account
internal id, status"] --- XID["External identifier
given to Okta"]
USR --- UNM["Normalized user name
unique"]
USR --- TS["Created, modified, version"]
USR --- LNK["Membership links"]
LNK --- GRP["Group or role"]
Think of a school register. Each child has an admission number that never changes, even if they move class or change their surname. The register never throws away a name when a child leaves. It marks them as left. The same careful habits protect your accounts.
Two identical create requests can arrive almost together. Only a database-level unique rule reliably stops the second one.
15Mapping Okta Attributes to Your Application
Okta and your application describe people differently. Mapping is the agreement that connects the two descriptions.
Two directions
In Okta, each application has a profile, a list of attributes the app can receive. Administrators decide which Okta fact feeds each app attribute. For example, the app’s user name can come from the Okta email address, and the app’s given name from the Okta first name. This is the Okta-to-app direction. If you also import, there is an app-to-Okta direction.
Choose a small set first
It is tempting to map everything. Resist. Every extra attribute is another thing to validate, secure, and keep in step. Start with the minimum your app truly needs: user name, name, email, and active. Add others only when a real feature needs them.
Declare your app’s attributes
When you set up the integration in Okta, you can define the attributes your app accepts, with their types. Align this with the layouts your server describes. If your server announces an attribute that Okta does not know, administrators cannot map it. If Okta sends an attribute your server does not understand, your server must decide whether to ignore it politely or reject the request. Ignoring unknown optional facts, while returning only what you stored, is usually friendlier.
Handle differences gracefully
- Lengths: your app may allow fewer characters than Okta does. Validate and answer with a clear error.
- Formats: dates, phone numbers, and country names come in many styles. Agree on one.
- Multi-valued facts: Okta may send several emails or phone numbers. Decide which one your app uses, and store the primary.
- Missing values: decide whether an empty attribute means “leave as is” or “clear it”.
| Your app needs | Typically fed from | Watch out for |
|---|---|---|
| Sign-in name | Okta email or login | Case and trailing spaces |
| First and last name | Okta first and last name | Accents, long or empty names |
| Okta primary email | Several emails, type labels | |
| Department | Okta department | Spelling differences, length limit |
| Manager | Okta manager | Manager must exist before the report |
| Role | Group membership | Role names must match exactly |
flowchart LR
UD["Okta profile
first name, email, department"] --> MAP["Mapping rules
set by administrator"]
MAP --> OUT["Outgoing SCIM user"]
OUT --> SRV["Your server"]
SRV --> VAL["Validate lengths and formats"]
VAL --> STO["Store in your app"]
Think of filling in a visa form using details from your passport. The passport says “Surname,” the form says “Family name.” You copy carefully from one to the other. Mapping is that careful copying, written down once so it happens the same way every time.
Maintain a simple table of every attribute: where it comes from, where it goes, its limits, and who owns it. It will answer most support questions.
16Retries, Duplicates, and Concurrency
Networks drop messages and callers repeat themselves. A strong server stays correct anyway.
Why repeats happen
Suppose your server creates an account but the reply gets lost on the way back. From Okta’s view, nothing happened, so it tries again later. If your server answers the second create by making another account, you have a duplicate. If it answers with a clear conflict, or returns the existing account, Okta can recover. Design every operation so that repeating it is harmless.
Idempotency in practice
- Create: check by user name first. If it exists, reply with a conflict so Okta can link.
- Update: applying the same change twice should leave the same state.
- Deactivate: switching off an already-off account is a success, not an error.
- Group membership: adding an existing member or removing an absent member is a quiet success.
Concurrency
Many requests can arrive at once, for different people and sometimes for the same person. Two updates to the same person can overlap and one can overwrite the other. Use database transactions so that each request sees a consistent view. Where possible, use version tags so that an update based on old data is detected.
Ordering problems
Requests can arrive out of order. A group update that mentions a user might arrive before the user is created. Decide how to handle this: either answer with a clear “not found” so Okta retries later, or hold the work briefly. Do not silently drop it.
Rate limits and back-pressure
If thousands of people are assigned at once, Okta may send many requests in a short time. Your application may not handle that. Set a sensible limit and answer with the “too many requests” status, including how long to wait. Okta backs off and tries again. This protects your database from being swamped and is much better than crashing.
sequenceDiagram
participant O as Okta
participant S as Your SCIM server
O->>S: Create user A
S-->>O: Created, but the reply is lost
Note over O: No reply seen, so retry later
O->>S: Create user A again
S-->>O: Conflict, user already exists
O->>S: Look up user A
S-->>O: Here is user A with its identifier
Think of pressing the lift button twice. A good lift does not call two lifts. The second press changes nothing. Your server should treat repeated requests the same polite way.
An error that is clear is a gift. A request that appears to succeed but did nothing is a trap that surfaces weeks later in an audit.
17Matching, Importing, and Reconciliation
Most real apps already have users before Okta arrives. Matching them correctly avoids duplicates and surprises.
The matching question
Suppose your app has a person named Asha with a certain email address. Okta also has an Asha with the same email. Are they the same? If you match them, Okta takes control of the existing account. If you do not, Okta may create a second Asha, and one person ends up with two accounts. Matching needs a reliable unique fact, usually the user name or email, chosen on purpose.
Importing existing accounts
Okta can read your list of users through your server and compare it with its own. For this to work, your listing must be complete, stable in order, and fast enough to page through. Before turning on import, clean the data: remove test accounts, fix obviously wrong emails, and merge duplicates. Importing messy data only spreads the mess.
Reconciling differences
Over time, accounts in your app may drift away from Okta, through manual edits or failed updates. A reconciliation check compares both sides and lists differences: people in Okta but missing in the app, people in the app but unknown to Okta, and people whose facts differ. Run it regularly and decide how to fix each type of difference. Do not auto-fix everything. Disabled accounts that should be enabled, or the reverse, deserve a human look.
Orphaned accounts
An orphan is an app account with no matching person in Okta. Orphans often belong to former employees whose offboarding was missed. They are a security risk. A reconciliation report that names orphans is one of the most valuable outputs of a SCIM project.
flowchart TD
A["Okta people list"] --> C{"Compare by user name"}
B["App accounts list"] --> C
C -->|"In both and equal"| OK["Fine"]
C -->|"In both but different"| DF["Update the differing facts"]
C -->|"Only in Okta"| CR["Create in the app"]
C -->|"Only in the app"| OR["Orphan: review before action"]
Think of merging two address books. You match people by phone number, join the entries that describe the same person, add the missing ones, and flag the entries that you cannot explain. Rushing leaves you with duplicates and wrong numbers.
Before the first real sync, compare lists and review the proposed changes. A surprise during the first live sync is far more costly than an hour of checking beforehand.
18Connecting Okta to Your Server
Once your server speaks SCIM, you tell Okta where it is and how to use it.
Create the application integration
In the Okta Admin Console you create an application integration for your app. Depending on your release, you may do this by creating a custom integration, or by using an integration wizard, or by publishing to the Okta Integration Network for others to use. The right path depends on whether the app is for your organization alone or for many customers. Check the current documentation for the steps available to you.
Enter the connection details
In the provisioning settings you give the base address of your SCIM server, choose how Okta should sign in to it, and supply the secret. Many consoles offer a test button that makes a simple call and reports success or failure. Use it first, and read any message carefully.
Choose which actions Okta may perform
Okta usually lets you switch on creating users, updating attributes, deactivating users, and, where available, pushing groups and importing users. Turn on only what your server supports and you have tested. Start with create and deactivate, then add the rest.
Map attributes
Choose which Okta facts feed each app attribute, using the mapping sheet from earlier. Include the user name rule. Think about whether the user name should be built from the email, and check how the mapping handles unusual characters.
Assign a test person
Create a test user in Okta, assign them to the integration, and watch your server’s logs. Confirm the lookup, the create, and a follow-up update. Then unassign the person and confirm the account is switched off. Only after these tests pass should real people be assigned.
Roll out in waves
Assign a small pilot group first, such as the project team. Look for errors over a few days. Then widen gradually. A staged rollout means a mistake affects ten people, not ten thousand.
Build and test the server alone
Prove it answers correctly before involving Okta.
Create the integration
Add the application integration in the Admin Console.
Enter address and secret
Press the test button and read the result.
Map attributes
Connect Okta facts to your app attributes.
Test with one person
Create, update, deactivate, and reactivate.
Pilot, then widen
Roll out to a small group, then more.
flowchart LR
S1["Server tested alone"] --> S2["Integration created"]
S2 --> S3["Connection test passes"]
S3 --> S4["Mappings set"]
S4 --> S5["One test person works"]
S5 --> S6["Pilot group"]
S6 --> S7["Everyone"]
Think of opening a new restaurant. You test the kitchen before inviting guests, run a soft opening for friends, and only then open to the public. Each stage reveals problems when they are still small.
Okta’s screens and wording change over time. Use this chapter as a checklist of ideas and follow the current Okta documentation for the exact clicks.
19Testing Your SCIM Server
A server that seems to work is not the same as a server that has been tested. Testing finds the surprises before your users do.
Test the server on its own first
Before Okta is involved, you can talk to your server with a general-purpose web testing tool. You send a request, read the reply, and compare it with what the standard and Okta expect. This isolates problems: if something fails here, the cause is in your server, not in Okta’s configuration.
The scenarios to cover
- Search for a user that does not exist and confirm an empty, successful reply.
- Search for a user that exists, with different capitalization, and confirm it is found.
- Create a user with only required facts, then with all facts.
- Create the same user twice and confirm a clear conflict.
- Read, replace, and patch a user, and confirm the stored result.
- Deactivate twice and confirm both succeed.
- Reactivate and confirm the same account returns.
- Page through a long list and confirm nothing is skipped or repeated.
- Send bad data and confirm a helpful error.
- Send no credentials and wrong credentials and confirm rejection.
Use the vendor’s checks
Okta provides guidance and sometimes test suites for SCIM integrations, particularly for those intended for its public catalog. If your server is for internal use only, you can still follow the same checks. Look in the current documentation for the recommended test steps.
Test with the real thing
After standalone tests, connect a non-production Okta organization to a non-production copy of your app. Assign test people with awkward data: long names, accents, missing fields, and many emails. Real data is always messier than imagined.
Automate
Turn your checks into an automatic test collection that runs every time you change the server. Regression is the quiet killer of integrations. A change that fixes one thing often breaks another, and automated tests catch it at once.
| Test level | Purpose | Who runs it |
|---|---|---|
| Unit tests | Check small pieces such as filter parsing | Developers, on every change |
| Protocol tests | Check replies against the standard | Developers, automatically |
| Integration tests | Check server with the real app database | Developers and testers |
| End-to-end tests | Check Okta to server to app | Identity team, in a test organization |
| Load tests | Check behavior with many requests | Operations, before big rollouts |
flowchart TD
UT["Unit tests"] --> PT["Protocol tests"]
PT --> IT["Integration tests"]
IT --> E2E["End-to-end with test Okta org"]
E2E --> LT["Load tests"]
LT --> PILOT["Pilot group in production"]
Think of a new bridge. Engineers test each bolt, then each beam, then the whole span with trucks driving over it, before the public is allowed to cross. Testing a SCIM server works the same way, from small parts to the full load.
Maintain a standing set of test users with unusual names, missing fields, and several emails. Reuse them every time you change anything.
20Hosting, Networking, and Reaching Private Apps
Your server must be reachable by Okta, and yet safe from everyone else.
Where it runs
A SCIM server is a small web service. It can run in a cloud platform, in a container, as a serverless function, or on a machine in your own data center. The best home is wherever your application’s data is easiest and safest to reach. A server that runs next to the database is faster and exposes fewer connections.
The public address
Okta runs on the public internet, so a standard integration needs a secure public web address for your server. Use an address you control, with a certificate from a trusted authority. Place a gateway or load balancer in front of the server to handle encryption, limits, and request logging.
Limiting who can call
Beyond credentials, consider restricting by network. Okta publishes lists of the addresses its services use, and these can change over time. If you allow only those addresses, strangers cannot even knock on the door. Treat this as an extra layer, not a replacement for authentication, and plan how you will keep the list current.
When the app is private
Many internal apps live behind a firewall with no public address. Okta offers an on-premises provisioning agent for some cases. This is a small program installed inside your network that opens an outbound connection to Okta and passes requests to a SCIM server that stays private. Availability and details depend on your license and release, so check the documentation before you rely on it.
High availability
If your server is down, no one is provisioned. Okta retries for some time, but long outages create backlogs and errors. Run more than one copy behind a load balancer, keep them stateless, and place them in separate failure zones so a single failure does not stop provisioning.
flowchart LR
OK["Okta"] -->|"HTTPS"| GW["Gateway or load balancer
certificate, limits, logs"]
GW --> S1["SCIM server copy 1"]
GW --> S2["SCIM server copy 2"]
S1 --> DB["Application database"]
S2 --> DB
OK2["Okta"] -->|"Outbound only"| AG["On-premises agent"]
AG --> PS["Private SCIM server"]
PS --> PDB["Private application"]
Think of a school with a front office. Parcels are delivered to a staffed front desk that signs for them and sends them on, instead of letting couriers wander the corridors. The gateway is the front desk, and it also lets you run two assistants so that the desk never closes.
An expired certificate quietly stops all provisioning. Automate renewal and set an alert that warns weeks before the date.
21Security Considerations
A SCIM server can create and disable accounts, so attackers value it. Defend it in layers.
Authentication and secrets
Every request must be authenticated. Keep secrets in a vault, never in code or in a shared document. Rotate them. Give the secret no more power than needed: the SCIM service account in your app should be able to manage users and nothing else.
Authorization inside your server
Even a properly authenticated caller should be limited. Decide what the SCIM service may and may not do. Should it be able to create administrator accounts? Perhaps role assignment for highly privileged roles should require a separate approval path, so that a stolen secret cannot mint a super-admin.
Validate everything
Treat every incoming value as untrusted. Check lengths, formats, and allowed characters. Reject unexpected attributes or ignore them deliberately. Protect against injection, where cleverly crafted text tries to trick your database or your filter parser into doing something unintended. Use safe database methods that separate data from commands, and parse filters strictly.
Protect personal data
User records are personal data. Send them only over encrypted connections, store them with suitable protection, and limit the facts you accept to what the app needs. Do not log whole records. A log file full of names and emails is a data leak waiting to happen.
Limit abuse
Apply rate limits and size limits. A caller should not be able to send gigantic payloads or request enormous pages. Limit the number of group members accepted in one call.
Protect the off-switch
Make sure that deactivation really disables sign-in, including active sessions and long-lived tokens inside your app. An account marked inactive that can still use an old session is not truly off.
Audit and review
Record who did what and when for every change. Review who has access to the secret and to the server, and review the list of privileged roles that SCIM can grant.
| Threat | Example | Defence |
|---|---|---|
| Stolen secret | Attacker creates an admin account | Vault, rotation, limited rights, alerts |
| Injection | Crafted filter text reaches the database | Strict parsing and safe queries |
| Data leakage | Full records in logs | Log identifiers, not personal facts |
| Abuse | Huge pages or repeated calls | Size and rate limits |
| Incomplete offboarding | Disabled account keeps a live session | End sessions and tokens on deactivate |
| Expired certificate | Provisioning stops quietly | Automated renewal and alerts |
flowchart TD
A["Incoming request"] --> B["Encrypted connection"]
B --> C["Authenticate caller"]
C --> D["Rate and size limits"]
D --> E["Validate every value"]
E --> F["Authorize the action"]
F --> G["Safe database access"]
G --> H["Audit record written"]
Think of a bank vault with a guarded gate, a locked door, a keypad, and a camera. No single defence is perfect, but a thief must beat them all. Layered security makes an attack costly and noisy.
Give the SCIM service the smallest rights that make it work. If it is ever misused, the damage is limited.
22Monitoring, Logging, and Troubleshooting
When provisioning fails, you want to know before the help desk does, and you want to find the cause fast.
What to log on your server
Log each request with a time, a unique request number, the verb, the address, the caller, the outcome, and how long it took. Log errors with the reason. Avoid personal data in logs. Include a request number in every reply header so that you can match Okta’s records with yours.
What to watch in Okta
Okta keeps a system log of events, including provisioning successes and failures. Administrators can also see tasks that need attention, such as users that failed to provision, with an error message. Read these regularly. The error text often points straight at the attribute or the status code that caused trouble.
Metrics that matter
- Request count by operation, to see volume and patterns.
- Error rate by status, to see when failures begin.
- Response time, especially the slowest cases.
- Queue or backlog size, if you process some work later.
- Certificate and secret expiry dates.
- Reconciliation differences, counted over time.
Alerts
Alert on sudden changes: a jump in errors, a drop to zero requests when you expect traffic, slow replies, and approaching expiry dates. Alert on any deactivate failures separately, because they are the dangerous ones.
A troubleshooting routine
Start from the person. Find them in Okta, read the provisioning error, and note the time. Find the matching request in your server logs. Read the status and the detail. Reproduce with the testing tool using the same data. Decide whether the cause is the data, the mapping, the credentials, the network, or a bug. Fix it, then retry the failed task in Okta.
flowchart TD
P["Person failed to provision"] --> O["Read the Okta error and time"]
O --> L["Find the request in server logs"]
L --> R["Read status and detail"]
R --> T["Reproduce with a test request"]
T --> C{"Cause?"}
C -->|"Data"| D1["Fix source data or validation"]
C -->|"Mapping"| D2["Fix the attribute mapping"]
C -->|"Credentials or network"| D3["Fix secret, certificate, or address"]
C -->|"Bug"| D4["Fix the code and add a test"]
D1 --> RT["Retry the task"]
D2 --> RT
D3 --> RT
D4 --> RT
Think of a doctor following a patient’s story: what happened, when, what the tests say. The doctor does not guess. Logs and metrics are the tests that let you diagnose by evidence.
The best troubleshooting tool is an error message that names the attribute and the problem. Spend time making yours precise.
23Performance and Scalability
Provisioning is usually quiet, but it can arrive in bursts. A well-built server stays calm during a burst.
Where bursts come from
On most days only a trickle of changes arrives. But when a new application is rolled out to thousands of people, or a large reorganization changes departments for everyone, requests can pile up in minutes. A server designed only for the quiet days may fall over on the busy one.
Make the common calls cheap
The lookup by user name is called constantly. Put an index on the normalized user name so it is a quick lookup rather than a scan of the whole table. Return only the facts needed. Avoid heavy work, such as sending emails or calling slow systems, inside the request itself.
Do slow work later
If creating an account triggers slow steps, such as setting up mailboxes or sending welcome notes, queue those steps and complete the SCIM request quickly. Okta can wait only so long for a reply. Be careful, though: if a user is reported as created before the slow work is done, make sure your app handles the half-ready state gracefully.
Scale out, not just up
A stateless server can run as several identical copies behind a load balancer. Add copies when load rises. Make sure your database can cope too: it is often the real limit. Watch connection counts and slow queries.
Respect the limits on both sides
Okta applies its own rate limits to its services. Your server should apply sensible limits too. When you cannot keep up, reply with the “too many requests” status and a wait time, and Okta will slow down. This is a far better outcome than timeouts and half-finished work.
Plan with real numbers
Estimate your largest burst: how many people might be assigned at once, and how many requests each one needs (usually a lookup, a create, and perhaps a few updates). Multiply, test with that load, and add a safety margin.
flowchart LR
BURST["Burst of requests"] --> LB["Load balancer"]
LB --> A["Server copy A"]
LB --> B["Server copy B"]
LB --> C["Server copy C"]
A --> DB["Indexed database"]
B --> DB
C --> DB
A -.-> Q["Queue for slow tasks"]
B -.-> Q
C -.-> Q
Think of a supermarket at festival time. More checkout lanes open, shelves are stocked in advance, and the bakery bakes cakes in the back instead of at the till. Quick lanes plus background work keep queues short.
24Advantages, Disadvantages, and Trade-offs
A custom server is powerful, but it is also a piece of software you must own for years.
Advantages
- Joiners get access automatically and quickly.
- Leavers lose access promptly, reducing risk.
- Fewer manual tasks and fewer typing errors.
- Standard language that other tools can reuse.
- Clear audit trail of who was created, changed, and removed.
- Works for apps that have no catalog connector.
Disadvantages
- You must build, host, secure, and maintain a service.
- Needs skills in the protocol and in your own app.
- Errors can silently leave wrong access.
- Standards and Okta behavior evolve over time.
- Messy source data causes visible failures.
- The service becomes a sensitive target for attackers.
Choosing between options
A custom server is only one way to reach an application. The right choice depends on size, risk, and the skills you have.
| Option | Best when | Cost | Risk |
|---|---|---|---|
| Catalog connector | The app is a known product | Low | Low |
| Custom SCIM server | Your app has users and changes often | Medium to high | Medium, depends on quality |
| On-premises agent with private server | The app is private behind a firewall | Medium to high | Medium |
| Scheduled file import | The app can only read files | Low to medium | Slower, less precise |
| Manual updates | Very few users and rare changes | Low at first | High as it grows |
| Single sign-on only, accounts created on first use | Provisioning is not required | Low | Leavers keep stale accounts |
flowchart TD
N["App needs user lifecycle automation"] --> Q1{"Catalog connector exists?"}
Q1 -->|"Yes"| U1["Use the connector"]
Q1 -->|"No"| Q2{"App can expose a SCIM front door?"}
Q2 -->|"Yes, reachable"| U2["Build a custom SCIM server"]
Q2 -->|"Yes, but private"| U3["Server plus on-premises agent"]
Q2 -->|"No"| Q3{"Few users and rare change?"}
Q3 -->|"Yes"| U4["Manual or file-based process"]
Q3 -->|"No"| U5["Consider changing or wrapping the app"]
Think of getting to work. A direct bus is cheapest when it exists. Building your own road is a large job that makes sense only when many people will use it for many years. The custom server is the road, and you should build it with open eyes.
25Design Patterns for SCIM Servers
A pattern is a proven answer to a problem that keeps coming back. These patterns make a SCIM server calmer and safer.
Pattern: Look first, then create
Before creating a person, search by user name. If the person exists, reply with a conflict or return them. This one habit prevents most duplicates, and it matches how Okta itself behaves.
Pattern: Soft off-switch
Deactivate by flipping an enabled flag and ending sessions, not by deleting rows. History stays intact and reactivation is trivial.
Pattern: Stable external identifier
Hand Okta an identifier that never changes and is never reused. Keep it separate from the user name and the email address, which can both change.
Pattern: Thin protocol, thick rules
Keep the SCIM-specific code thin. It parses and replies, nothing more. Put your decisions in a separate business layer. When the standard evolves, the rules stay untouched.
Pattern: Attribute ownership table
Write down which attributes Okta owns and which your app owns. The server updates only the first group. This protects local settings from being erased by a replace request.
Pattern: Idempotent by design
Make every operation safe to repeat. Duplicate deactivate, duplicate add-member, and duplicate create all end in the same correct state.
Pattern: Fail loudly, with detail
When you cannot do something, say so with the right status and a message that names the attribute and the reason. A loud failure is fixed in minutes. A silent one lives for months.
Pattern: Reconcile on a schedule
Do not assume both sides stay in agreement forever. Compare them regularly, report differences, and fix on purpose.
Look first, then create
Search before making anyone.
Soft off-switch
Disable, do not delete.
Stable identifier
Never reuse, never change.
Thin protocol layer
Rules live elsewhere.
Attribute table
Okta fields versus app fields.
Reconcile regularly
Find drift before auditors do.
flowchart TD
REQ["Create request"] --> LK{"User name exists?"}
LK -->|"Yes"| CF["Reply conflict or return existing"]
LK -->|"No"| VAL["Validate and apply ownership rules"]
VAL --> DB["Insert with unique rule"]
DB --> OK{"Insert succeeded?"}
OK -->|"Yes"| CR["Reply created with identifier"]
OK -->|"Duplicate raced"| CF
Think of a careful librarian. Before issuing a new card, she checks the register. She never throws away old records. She gives each reader a number that never changes. She keeps her own notes separate from the registry. Patterns are simply the good habits of a careful professional.
26Anti-patterns to Avoid
An anti-pattern is a habit that seems sensible but causes trouble later. Each card below shows one, why it hurts, and a better way.
What it looks like
A SCIM address with weak or no authentication, left open because “it is only internal”.
Why it hurts
Anyone who finds it can create or disable accounts, including administrators.
Better approach
Require strong credentials on every call, restrict the network, and keep secrets in a vault.
What it looks like
Removing the account row whenever a person is deprovisioned.
Why it hurts
History vanishes, owned data is orphaned, and returning staff get a new, empty account.
Better approach
Disable the account and end sessions. Delete only through a deliberate, separate process.
What it looks like
Using the email address as the permanent identifier handed to Okta.
Why it hurts
When the email changes after a name change, the link breaks and duplicates appear.
Better approach
Use a separate, stable identifier that never changes.
What it looks like
Replying “success” even when an attribute was ignored or the work failed.
Why it hurts
Okta believes the app is correct, while the app has wrong or missing data.
Better approach
Return the stored record, and use clear errors when something cannot be done.
What it looks like
Treating the first position in a list as zero instead of one.
Why it hurts
One person is skipped at every page boundary, and the loss is hard to notice.
Better approach
Follow the standard’s one-based positions and test with lists longer than a page.
What it looks like
Mapping every possible Okta attribute into the app “just in case”.
Why it hurts
More data to validate, secure, and keep correct, and more ways for requests to fail.
Better approach
Map only what a real feature needs, and add more later with a reason.
What it looks like
Writing complete user records into log files for debugging convenience.
Why it hurts
Logs spread widely and become an uncontrolled copy of personal data.
Better approach
Log request numbers and identifiers, and keep personal facts out.
27Best Practices and Common Mistakes
This chapter gathers habits that separate calm teams from frantic ones.
Best practices
Start small
Support users, search, create, update, and deactivate before anything else.
Follow the standard
Use the documented layouts, statuses, and one-based paging.
Secure from day one
Encrypted traffic, strong credentials, vault, rotation, least privilege.
Make it repeatable
Every operation safe to run twice.
Test automatically
A growing test collection, run on every change.
Observe
Logs, metrics, and alerts, including expiry dates.
Roll out in waves
Pilot group first, then widen gradually.
Reconcile and review
Compare both sides regularly and review access to the service itself.
Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
| Case-sensitive user name search | Duplicates with different capital letters | Normalize and compare without case |
| Empty search treated as error | Okta cannot create new people | Return success with an empty list |
| Only supporting replace | Group and status changes fail | Support both replace and patch |
| Not handling repeated requests | Duplicates after retries | Make operations idempotent |
| Ignoring rate limits | Database overloaded in a rollout | Limit and answer with a wait time |
| Forgetting session cleanup | Deactivated users stay signed in | End sessions on deactivate |
| Testing only perfect data | Real users fail on awkward names | Keep awkward test people |
| No ownership of the service | Nobody fixes it when it breaks | Name an owner and an on-call route |
| Letting certificates expire | All provisioning stops quietly | Automate renewal, alert early |
Think of a pilot’s checklist. Experienced pilots still use one, not because they forget how to fly, but because small slips matter. These practices are the checklist for your server.
Before going live, ask three things: what happens when this is repeated, what happens when it fails, and who will notice.
28Real-World Scenarios
These short stories use made-up companies to show how the ideas appear in daily work. They are teaching examples, not descriptions of any real organization.
Scenario 1: The claims portal nobody could automate
Marlowe Insurance ran an internal claims portal built years earlier. New adjusters waited days for accounts, and leavers sometimes kept access for weeks. The team put a thin SCIM layer in front of the portal’s user table, supporting search, create, update, and deactivate. They piloted with one department, fixed two mapping issues with long surnames, and widened over a month. Account setup dropped from days to minutes, and an audit later showed no active accounts for people who had left.
Scenario 2: Duplicate Ashas
Kestrel Logistics enabled provisioning to a partner extranet that already had accounts. Okta created a second account for many people because the extranet compared user names with case sensitivity, and many existing names had capitals. The team changed the search to ignore case, merged the duplicates, and ran a dry-run comparison before re-enabling. A reconciliation report now runs weekly.
Scenario 3: The Monday burst
Northgate Retail assigned a new scheduling app to eight thousand store staff on one Monday morning. The SCIM server answered quickly at first, then the database saturated. The team added an index on the normalized user name, enabled a rate limit that returned a wait time, and queued welcome emails for later. Okta slowed its pace and finished assigning in under two hours with no failures. The change was added to their rollout checklist.
Scenario 4: The expired certificate
Cedarline Health found on a Friday that no new nurses had been provisioned for six days. The cause was an expired certificate on the SCIM gateway. The team automated renewal, added an alert thirty days before expiry, and added a monitor that warns when requests drop to zero during working hours.
What the scenarios share
Each story was solved by the same habits: look first before creating, handle repeats, make failures visible, test with real data, roll out in stages, and watch for quiet breakage. None needed anything exotic. They needed discipline.
Most provisioning incidents come from data surprises and silent failures, not from clever attacks. Boring care beats clever tricks.
29Frequently Asked Questions
Short answers to common questions. Where details depend on your license or release, confirm them in your own console and the official documentation.
30Summary and Key Takeaways
Here is the whole story in a few lines you can carry with you.
A custom SCIM server is a translator that sits in front of your application. Okta speaks the standard language of identity: look up this person, create them, change their facts, switch them off. Your server turns each request into the right action inside your app and replies honestly with the correct status. Built in clear layers, protected by strong authentication, tested with awkward data, watched with logs and alerts, and rolled out in stages, it keeps accounts in step with people automatically.
The craft lies in small habits: search before creating, never delete when you can disable, use stable identifiers, make every operation safe to repeat, and fail loudly with clear detail. With these, the service becomes a quiet, dependable part of your access design instead of a hidden source of surprises.
Key Takeaways
- Okta asks, your server answers. Your job is to translate standard SCIM requests into actions in your app and report truthfully.
- Start with the essentials. Authenticate, search by user name, create, update, and deactivate. Add groups and import later.
- Be safe to repeat. Look before creating, treat repeated deactivation as success, and use database rules to stop duplicates.
- Use stable identifiers and soft deactivation. Never use the email as the identifier, and disable rather than delete.
- Secure the door. Encrypted traffic, strong credentials in a vault, least privilege, strict validation, and no personal data in logs.
- Test, observe, and roll out in waves. Automate tests, watch logs, metrics, and expiry dates, and widen from a pilot group.
- Confirm the details. Okta behavior and options change, so verify specifics in the official documentation and the SCIM standards.