Skip to content

Commit f3e3e54

Browse files
committed
fix(scim): sync provider display names and document Okta setup
1 parent 8cd51d4 commit f3e3e54

23 files changed

Lines changed: 441 additions & 30 deletions

File tree

apps/docs/content/docs/platform/enterprise/scim.mdx renamed to apps/docs/content/docs/platform/enterprise/scim/index.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,14 @@ Two tokens can be active at a time, so you can rotate without downtime: issue th
6666
<Tabs items={['Okta', 'Microsoft Entra ID', 'OneLogin', 'JumpCloud']}>
6767
<Tab value="Okta">
6868

69+
For a step-by-step walkthrough, see [Okta provisioning](/platform/enterprise/scim/okta).
70+
6971
If you use OIDC for sign-in, create a separate provisioning integration: Okta cannot add SCIM to a custom OIDC app. In the Okta Integration Network catalog, add **SCIM 2.0 Test App (Header Auth)** for a private integration. See [Okta's setup guide](https://developer.okta.com/docs/guides/scim-provisioning-integration-connect/main/).
7072

7173
In that app, open **Provisioning → Integration → Configure API Integration**, enable API integration, and enter:
7274

73-
- **SCIM 2.0 Base Url**: `https://<your-sim-domain>/api/scim/v2`
74-
- **API Token**: your Sim token
75+
- **Base URL**: `https://<your-sim-domain>/api/scim/v2`
76+
- **API Token**: `Bearer <your-sim-token>` (include `Bearer` and a space)
7577

7678
Select **Test API Credentials**, then save. Under **Provisioning → To App**, enable Create Users, Update User Attributes, and Deactivate Users. Assign a test user first, then use **Push Groups** for groups you want to map in Sim. Keep assignment groups separate from groups you push, as required by Okta.
7779

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"title": "Directory provisioning",
3+
"pages": ["index", "okta"],
4+
"defaultOpen": false
5+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
---
2+
title: Okta provisioning
3+
description: Connect a private Okta SCIM integration to Sim and verify user and group provisioning
4+
---
5+
6+
import { Callout } from 'fumadocs-ui/components/callout'
7+
import { Step, Steps } from 'fumadocs-ui/components/steps'
8+
9+
Use an Okta SCIM integration to create, update, and deactivate Sim members. This guide covers provisioning; configure [single sign-on](/platform/enterprise/sso) separately for authentication.
10+
11+
## Before you start
12+
13+
- An Enterprise organization in Sim, with an owner or administrator who can manage provisioning.
14+
- Each user email domain [verified in Sim](/platform/enterprise/verified-domains).
15+
- An Okta administrator account with access to application provisioning.
16+
- A Sim deployment reachable by Okta over HTTPS. For self-hosted deployments, use its public URL and apply the [SCIM rollout requirements](/platform/enterprise/scim#deployment-and-upgrades).
17+
18+
Start with a test user and a dedicated group before assigning your organization. Provisioned members use a seat; they receive workspace access only through a mapping or an existing grant.
19+
20+
## Connect Okta to Sim
21+
22+
<Steps>
23+
24+
<Step>
25+
### Enable provisioning in Sim
26+
27+
Open **Settings → Organization → Single sign-on → Provisioning** and turn on **Enable directory provisioning**. Copy the **Base URL** from **Connection**. Under **Tokens**, choose an expiry and select **Issue token**. Copy the token before closing its dialog; Sim shows it once.
28+
</Step>
29+
30+
<Step>
31+
### Add the Okta integration
32+
33+
In the Okta Admin Console, open **Applications and Resources → Applications → Browse App Catalog**. Search for **SCIM 2.0 Test App (Header Auth)** and select **Add Integration**. Give the app a recognizable name, such as **Sim provisioning**.
34+
35+
For a provisioning-only app, hide its application icon from users and turn off automatic sign-in from the landing page. On **Sign-On Options**, select **Secure Web Authentication**, use your Sim HTTPS login URL, and set **Application username format** to **Email**. Complete the app setup. These template settings do not configure Sim SSO.
36+
37+
<Callout type="info">
38+
Okta cannot add SCIM to a custom OIDC app. Keep this provisioning app alongside your OIDC sign-in app. For an existing custom SAML or SWA app, use [Okta's custom-app SCIM instructions](https://help.okta.com/en-us/Content/Topics/apps/apps_app_integration_wizard_scim.htm); those connection fields differ from the catalog template below.
39+
</Callout>
40+
</Step>
41+
42+
<Step>
43+
### Configure API integration
44+
45+
In the new Okta app, open **Provisioning → Integration → Configure API Integration** and select **Enable API integration**.
46+
47+
| Field | Value |
48+
| --- | --- |
49+
| Base URL | The URL copied from Sim, ending in `/api/scim/v2` |
50+
| API Token | `Bearer <your-sim-token>` — include `Bearer` and a space before the token |
51+
52+
Select **Test API Credentials** and save after the test succeeds. A successful connection test checks connectivity and authentication; continue with a test assignment to verify provisioning.
53+
54+
The Header Auth template sends this field as the complete `Authorization` header. Entering only the token produces **A bearer token is required**.
55+
56+
![Okta API integration with a successful credential test; the test deployment URL is redacted](/static/enterprise/okta/api-integration.png)
57+
</Step>
58+
59+
<Step>
60+
### Enable provisioning actions
61+
62+
Under **Provisioning → To App**, select **Edit**, enable **Create Users**, **Update User Attributes**, and **Deactivate Users**, then save. Leave **Sync Password** disabled: Sim does not synchronize passwords through SCIM.
63+
64+
Use an email address from a verified Sim domain for the application username and primary email. Review the attribute mappings if your Okta usernames differ from users' email addresses.
65+
66+
![Okta provisioning actions with Create Users, Update User Attributes, and Deactivate Users enabled, and Sync Password disabled](/static/enterprise/okta/provisioning-actions.png)
67+
</Step>
68+
69+
</Steps>
70+
71+
This uses Okta's [private integration template](https://developer.okta.com/docs/guides/scim-provisioning-integration-connect/main/). It does not make the integration a published or certified Okta Integration Network application.
72+
73+
## Verify a user lifecycle
74+
75+
Assign one test user to the app from **Assignments**. In Sim, confirm that the account appears in the organization and that **Provisioning → Activity** shows successful requests. Update the user's **Display name** in Okta and confirm it in Sim. Sim uses the mapped `displayName`, falling back to the structured name when it is omitted.
76+
77+
Unassign the test user and check that Sim marks them as deactivated. Reassign the same user and confirm that the existing member becomes active again. If the user belongs to a pushed group, remove them from that group and confirm the downstream removal **before** unassigning the app; see [Okta's offboarding order](https://help.okta.com/en-us/Content/Topics/users-groups-profiles/app-assignments-group-push.htm).
78+
79+
Okta deactivates users over SCIM; it does not send a SCIM DELETE. Sim suspends access while retaining the user's organization membership, ownership, and seat. See [deactivation behavior](/platform/enterprise/scim#what-it-does) before using unassignment for offboarding.
80+
81+
## Push groups and map access
82+
83+
Use separate groups for app assignment and Group Push. Okta does not support using the same group for both purposes. Assign the users to the app first. Under **Push Groups → Find groups by name**, select the group, leave **Push group memberships immediately** enabled, and save with **Create Group** selected for a new downstream group.
84+
85+
![Okta Group Push selecting an engineering group and creating its downstream group in Sim](/static/enterprise/okta/group-push.png)
86+
87+
Once the group appears under **Single sign-on → Provisioning → Group mappings** in Sim, map it to a workspace, permission group, or the organization admin role. Workspace access requires a workspace mapping. If name matching is enabled, Sim can automatically map matching permission groups.
88+
89+
Use a workspace mapping for the first test. Confirm that the member gains access, then remove them from the pushed group while keeping their app assignment. Confirm that the directory grant is withdrawn. Add them back and verify access is restored without creating another account.
90+
91+
With **Lock managed membership** enabled, manage access for provisioned members through directory groups; manual invitations and grants are blocked. Read [how access is withdrawn](/platform/enterprise/scim#how-access-is-withdrawn) before mapping groups that overlap with existing manual access.
92+
93+
## Rotate the token
94+
95+
Issue a replacement token in Sim while the current token remains active. In Okta, edit **Provisioning → Integration** and replace **API Token** with `Bearer <new-token>`. Test the credentials, save, and confirm a successful provisioning request before revoking the old token in Sim. Sim allows two active tokens so rotation can overlap.
96+
97+
## Troubleshooting
98+
99+
| Symptom | Check |
100+
| --- | --- |
101+
| Credential test fails | Include `Bearer ` before the token. Use the complete HTTPS base URL and a current Sim token, and confirm provisioning is enabled and reachable. |
102+
| User creation fails | Confirm the email domain is verified, a seat is available, and the account does not belong to another Sim organization. |
103+
| User exists but has no workspace access | Push the directory group and add a Sim workspace mapping. Assignment alone creates organization membership. |
104+
| Name or membership changes do not arrive | Check the app assignment, provisioning actions, attribute mappings, and Group Push status in Okta. |
105+
| A pushed group is missing members | Confirm those users are active in Okta and successfully assigned to the app. After activation, repush the group; see [Okta's Group Push troubleshooting](https://help.okta.com/en-us/Content/Topics/users-groups-profiles/usgp-group-push-troubleshoot.htm). |
106+
| A deactivated member still uses a seat | Deactivation suspends access. Removing organization membership is a separate offboarding action. |
107+
108+
Use **View Logs** in the Okta app and **Provisioning → Activity** in Sim to inspect failures. Requests with an invalid or revoked token appear only in Okta because Sim cannot associate them with a connection.

apps/docs/content/docs/platform/enterprise/sso.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ For a saved connection, open **Sign-in** and select **Edit**. The Provider ID re
105105

106106
SCIM settings save immediately in the **Provisioning** tab. Its **Disable just-in-time provisioning** rule overrides Automatic first-sign-in membership while the connection is active and entitled. Existing members can still sign in. See [directory provisioning](/platform/enterprise/scim#provisioning-and-sso-together).
107107

108+
For Okta, follow the [provisioning walkthrough](/platform/enterprise/scim/okta) to connect a separate SCIM app and verify user and group synchronization.
109+
108110
## Provider Guides
109111

110112
<Tabs items={['Okta', 'Microsoft Entra ID', 'Microsoft Entra ID (SAML)', 'Google Workspace', 'ADFS']}>
105 KB
Loading
161 KB
Loading
219 KB
Loading

apps/sim/ee/scim/TESTING.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ timeout.
5959
filters, account email drift, and filters reflecting manual suspension.
6060
- Entra-shaped complex name and extension PATCH, case-insensitive core-qualified
6161
password fields, atomic failure, and partial complex attribute selection.
62+
- Okta display-name updates with an echoed stale formatted name, actual account
63+
name drift repair, stable retries, optional display-name removal, and group
64+
member labels falling back to the formatted name.
6265
- Group membership idempotency and membership-only replacement timestamps.
6366
- Workspace, organization-role, and permission-group mappings, reconciliation,
6467
drift repair, withdrawal, and preservation of pre-existing manual workspace access.
@@ -101,6 +104,22 @@ which verifies that SCIM's `disableJit` setting blocks fresh SSO membership,
101104
preserves existing membership, and permits JIT when disabled. These checks run
102105
the admission operation and Enterprise entitlement reads through PostgreSQL.
103106

107+
## Live Okta verification
108+
109+
The private **SCIM 2.0 Test App (Header Auth)** has also been exercised through
110+
the Okta Admin Console against an isolated test organization over HTTPS. The
111+
walkthrough and redacted screenshots are in the
112+
[Okta provisioning guide](../../../docs/content/docs/platform/enterprise/scim/okta.mdx).
113+
The run verified credential testing, user assignment and creation, profile
114+
updates, deactivation and reactivation of the same account, group push, workspace
115+
grant and withdrawal, group rename, readdition, repeated push, reconciliation,
116+
token rotation, and downstream group deletion. Resulting names, memberships,
117+
and grants were checked in PostgreSQL as well as the admin interfaces.
118+
119+
This used synthetic users and a seeded verified test domain. It did not exercise
120+
real DNS ownership verification, an end-user SSO login, or a Microsoft Entra
121+
tenant. It is a provider interoperability check, not OIN certification.
122+
104123
## Remaining provider verification
105124

106125
Before claiming a provider integration has been validated, use an actual Okta

apps/sim/ee/scim/lib/application/users/provision-user.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ function attributes(overrides: Partial<ScimUserAttributes> = {}): ScimUserAttrib
123123
externalId: 'ext-1',
124124
active: true,
125125
displayName: 'Ada Lovelace',
126+
displayNameSource: 'provider',
126127
name: { formatted: 'Ada Lovelace', givenName: 'Ada', familyName: 'Lovelace' },
127128
emails: [{ value: 'ada@acme.test', type: 'work', primary: true }],
128129
...overrides,
@@ -256,6 +257,39 @@ describe('provisionScimUser', () => {
256257
})
257258
})
258259

260+
it.each(['create', 'link'] as const)(
261+
'uses the explicit display name when the identity action is %s',
262+
async (action) => {
263+
stageConnection()
264+
if (action === 'link') {
265+
mocks.resolveIdentity.mockResolvedValue({
266+
action: 'link',
267+
userId: 'u-old',
268+
via: 'tombstone',
269+
})
270+
}
271+
const stored = attributes({ displayName: 'Countess Lovelace' })
272+
stageReadBack(action === 'link' ? 'u-old' : 'u-new', stored, null)
273+
await run(stored)
274+
if (action === 'create') {
275+
expect(mocks.createUser).toHaveBeenCalledWith({
276+
body: {
277+
email: 'ada@acme.test',
278+
name: 'Countess Lovelace',
279+
data: { emailVerified: false },
280+
},
281+
})
282+
} else {
283+
expect(mocks.syncIdentity).toHaveBeenCalledWith(db, {
284+
userId: 'u-old',
285+
email: 'ada@acme.test',
286+
name: 'Countess Lovelace',
287+
})
288+
}
289+
expect(mocks.insertScimUser.mock.calls[0][1].attributes.name.formatted).toBe('Ada Lovelace')
290+
}
291+
)
292+
259293
it('runs the post-commit effects against the subscription admission validated', async () => {
260294
stageConnection()
261295
await run(attributes())

apps/sim/ee/scim/lib/application/users/provision-user.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
resolveProvisionedIdentity,
3333
} from '@/ee/scim/lib/identity/resolve-user'
3434
import { reconcileUserProjection } from '@/ee/scim/lib/projection/reconcile-user'
35-
import { primaryEmail } from '@/ee/scim/lib/protocol/canonical'
35+
import { accountName, primaryEmail } from '@/ee/scim/lib/protocol/canonical'
3636
import { ScimError, uniqueness } from '@/ee/scim/lib/protocol/errors'
3737
import { toUserResource } from '@/ee/scim/lib/protocol/resources'
3838
import {
@@ -128,7 +128,7 @@ export const provisionScimUser = defineAuthorizedScimUseCase({
128128
const created = await auth.api.createUser({
129129
body: {
130130
email,
131-
name: attributes.name.formatted,
131+
name: accountName(attributes),
132132
data: { emailVerified: false },
133133
},
134134
})
@@ -182,7 +182,7 @@ export const provisionScimUser = defineAuthorizedScimUseCase({
182182
emailChanged = await syncAccountIdentityTx(tx, {
183183
userId,
184184
email,
185-
name: attributes.name.formatted,
185+
name: accountName(attributes),
186186
})
187187
if (emailChanged && attributes.active) {
188188
await revokeUserSessionsTx(tx, { userId, organizationId: context.organizationId })

0 commit comments

Comments
 (0)