Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,14 @@ Two tokens can be active at a time, so you can rotate without downtime: issue th
<Tabs items={['Okta', 'Microsoft Entra ID', 'OneLogin', 'JumpCloud']}>
<Tab value="Okta">

For a step-by-step walkthrough, see [Okta provisioning](/platform/enterprise/scim/okta).

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

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

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

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.

Expand Down
5 changes: 5 additions & 0 deletions apps/docs/content/docs/platform/enterprise/scim/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "Directory provisioning",
"pages": ["index", "okta"],
"defaultOpen": false
}
108 changes: 108 additions & 0 deletions apps/docs/content/docs/platform/enterprise/scim/okta.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
title: Okta provisioning
description: Connect a private Okta SCIM integration to Sim and verify user and group provisioning
---

import { Callout } from 'fumadocs-ui/components/callout'
import { Step, Steps } from 'fumadocs-ui/components/steps'

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.

## Before you start

- An Enterprise organization in Sim, with an owner or administrator who can manage provisioning.
- Each user email domain [verified in Sim](/platform/enterprise/verified-domains).
- An Okta administrator account with access to application provisioning.
- 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).

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.

## Connect Okta to Sim

<Steps>

<Step>
### Enable provisioning in Sim

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.
</Step>

<Step>
### Add the Okta integration

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

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.

<Callout type="info">
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.
</Callout>
</Step>

<Step>
### Configure API integration

In the new Okta app, open **Provisioning → Integration → Configure API Integration** and select **Enable API integration**.

| Field | Value |
| --- | --- |
| Base URL | The URL copied from Sim, ending in `/api/scim/v2` |
| API Token | `Bearer <your-sim-token>` — include `Bearer` and a space before the token |

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.

The Header Auth template sends this field as the complete `Authorization` header. Entering only the token produces **A bearer token is required**.

![Okta API integration with a successful credential test; the test deployment URL is redacted](/static/enterprise/okta/api-integration.png)
</Step>

<Step>
### Enable provisioning actions

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.

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.

![Okta provisioning actions with Create Users, Update User Attributes, and Deactivate Users enabled, and Sync Password disabled](/static/enterprise/okta/provisioning-actions.png)
</Step>

</Steps>

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.

## Verify a user lifecycle

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.

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

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.

## Push groups and map access

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.

![Okta Group Push selecting an engineering group and creating its downstream group in Sim](/static/enterprise/okta/group-push.png)

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.

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.

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.

## Rotate the token

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.

## Troubleshooting

| Symptom | Check |
| --- | --- |
| 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. |
| User creation fails | Confirm the email domain is verified, a seat is available, and the account does not belong to another Sim organization. |
| User exists but has no workspace access | Push the directory group and add a Sim workspace mapping. Assignment alone creates organization membership. |
| Name or membership changes do not arrive | Check the app assignment, provisioning actions, attribute mappings, and Group Push status in Okta. |
| 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). |
| A deactivated member still uses a seat | Deactivation suspends access. Removing organization membership is a separate offboarding action. |

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.
2 changes: 2 additions & 0 deletions apps/docs/content/docs/platform/enterprise/sso.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ For a saved connection, open **Sign-in** and select **Edit**. The Provider ID re

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

For Okta, follow the [provisioning walkthrough](/platform/enterprise/scim/okta) to connect a separate SCIM app and verify user and group synchronization.

## Provider Guides

<Tabs items={['Okta', 'Microsoft Entra ID', 'Microsoft Entra ID (SAML)', 'Google Workspace', 'ADFS']}>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions apps/sim/ee/scim/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ timeout.
filters, account email drift, and filters reflecting manual suspension.
- Entra-shaped complex name and extension PATCH, case-insensitive core-qualified
password fields, atomic failure, and partial complex attribute selection.
- Okta display-name updates with an echoed stale formatted name, actual account
name drift repair, stable retries, optional display-name removal, and group
member labels falling back to the formatted name.
- Group membership idempotency and membership-only replacement timestamps.
- Workspace, organization-role, and permission-group mappings, reconciliation,
drift repair, withdrawal, and preservation of pre-existing manual workspace access.
Expand Down Expand Up @@ -101,6 +104,22 @@ which verifies that SCIM's `disableJit` setting blocks fresh SSO membership,
preserves existing membership, and permits JIT when disabled. These checks run
the admission operation and Enterprise entitlement reads through PostgreSQL.

## Live Okta verification

The private **SCIM 2.0 Test App (Header Auth)** has also been exercised through
the Okta Admin Console against an isolated test organization over HTTPS. The
walkthrough and redacted screenshots are in the
[Okta provisioning guide](../../../docs/content/docs/platform/enterprise/scim/okta.mdx).
The run verified credential testing, user assignment and creation, profile
updates, deactivation and reactivation of the same account, group push, workspace
grant and withdrawal, group rename, readdition, repeated push, reconciliation,
token rotation, and downstream group deletion. Resulting names, memberships,
and grants were checked in PostgreSQL as well as the admin interfaces.

This used synthetic users and a seeded verified test domain. It did not exercise
real DNS ownership verification, an end-user SSO login, or a Microsoft Entra
tenant. It is a provider interoperability check, not OIN certification.

## Remaining provider verification

Before claiming a provider integration has been validated, use an actual Okta
Expand Down
34 changes: 34 additions & 0 deletions apps/sim/ee/scim/lib/application/users/provision-user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ function attributes(overrides: Partial<ScimUserAttributes> = {}): ScimUserAttrib
externalId: 'ext-1',
active: true,
displayName: 'Ada Lovelace',
displayNameSource: 'provider',
name: { formatted: 'Ada Lovelace', givenName: 'Ada', familyName: 'Lovelace' },
emails: [{ value: 'ada@acme.test', type: 'work', primary: true }],
...overrides,
Expand Down Expand Up @@ -256,6 +257,39 @@ describe('provisionScimUser', () => {
})
})

it.each(['create', 'link'] as const)(
'uses the explicit display name when the identity action is %s',
async (action) => {
stageConnection()
if (action === 'link') {
mocks.resolveIdentity.mockResolvedValue({
action: 'link',
userId: 'u-old',
via: 'tombstone',
})
}
const stored = attributes({ displayName: 'Countess Lovelace' })
stageReadBack(action === 'link' ? 'u-old' : 'u-new', stored, null)
await run(stored)
if (action === 'create') {
expect(mocks.createUser).toHaveBeenCalledWith({
body: {
email: 'ada@acme.test',
name: 'Countess Lovelace',
data: { emailVerified: false },
},
})
} else {
expect(mocks.syncIdentity).toHaveBeenCalledWith(db, {
userId: 'u-old',
email: 'ada@acme.test',
name: 'Countess Lovelace',
})
}
expect(mocks.insertScimUser.mock.calls[0][1].attributes.name.formatted).toBe('Ada Lovelace')
}
)

it('runs the post-commit effects against the subscription admission validated', async () => {
stageConnection()
await run(attributes())
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/ee/scim/lib/application/users/provision-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
resolveProvisionedIdentity,
} from '@/ee/scim/lib/identity/resolve-user'
import { reconcileUserProjection } from '@/ee/scim/lib/projection/reconcile-user'
import { primaryEmail } from '@/ee/scim/lib/protocol/canonical'
import { accountName, primaryEmail } from '@/ee/scim/lib/protocol/canonical'
import { ScimError, uniqueness } from '@/ee/scim/lib/protocol/errors'
import { toUserResource } from '@/ee/scim/lib/protocol/resources'
import {
Expand Down Expand Up @@ -128,7 +128,7 @@ export const provisionScimUser = defineAuthorizedScimUseCase({
const created = await auth.api.createUser({
body: {
email,
name: attributes.name.formatted,
name: accountName(attributes),
data: { emailVerified: false },
},
})
Expand Down Expand Up @@ -182,7 +182,7 @@ export const provisionScimUser = defineAuthorizedScimUseCase({
emailChanged = await syncAccountIdentityTx(tx, {
userId,
email,
name: attributes.name.formatted,
name: accountName(attributes),
})
if (emailChanged && attributes.active) {
await revokeUserSessionsTx(tx, { userId, organizationId: context.organizationId })
Expand Down
Loading
Loading