Okta Connector
The Okta connector imports users, groups, and group memberships from
an Okta org into IdentityMesh through the Okta REST API. It runs as a
standard IdentityMesh plug-in DLL — drop it into the engine’s
Connectors/ folder, give it your org domain and an API token, and
the engine handles scheduling, watermarking, and projection into the
mesh.
This runbook covers phase 1: import only. Write-back (creating users, modifying group membership, deactivating accounts) is on the roadmap as phase 2 and is called out explicitly at the end of this document.
If you are migrating from Okta to Entra ID (or running both side by side during a transition), this connector composes with the Entra ID connector — both project into the same mesh objects, and the merge behaviour is governed by the join rules configured in the Admin UI.
At a glance
| Property | Value |
|---|---|
| Connector name | Okta |
| Direction | Import (one-way, source → mesh) in phase 1 |
| Object types | User, Group, GroupMember |
| Authentication | App-only — Okta API token (SSWS scheme) in phase 1 |
| Watermark style | Per-object-type lastUpdated ISO 8601 timestamps |
| Throttling behaviour | Honours Retry-After on HTTP 429 / 503, up to 3 retries |
| Required permissions | API token must inherit a role with read access to users and groups (read-only admin is sufficient) |
Prerequisites
You will need:
-
An Okta org where you have administrator access. Standard, developer, and preview orgs all work — the API surface is identical. The org domain (e.g.
contoso.okta.com,contoso.oktapreview.com, or a custom domain) is required to configure the connector. -
An admin who can create API tokens. API tokens inherit the permissions of the user who created them, so the operator should already hold a read-only admin role to enforce least privilege. Avoid creating the token from a Super Admin account.
-
An IdentityMesh deployment with:
- The
IdentityMesh.Connectors.Okta.dllconnector DLL in the engine’sConnectors/folder (the installer drops it there automatically once installer wiring lands; manual install is described below). - Read access to the
IM_Secretstable (or to your Key Vault / Vault backend if you’ve migrated secrets persecrets-keyvault.md/secrets-vault.md).
- The
-
Outbound HTTPS to your Okta org domain from whichever host runs the import — the central sync engine, or a relay agent if you have routed this connector through one. No inbound firewall rules are needed.
Step 1 — Create an Okta API token
-
Sign in to your Okta admin console at
https://<your-org>.okta.com/adminas an org administrator. -
Navigate to Security → API → Tokens.
-
Click Create Token.
Field Value Name IdentityMesh-ImportScope (inherits the creator’s role) -
Copy the token VALUE immediately — Okta only displays it once. There is no way to retrieve a token after this dialog closes; if you miss it, delete the token and create a new one.
-
Calendar a 90-day rotation. Okta tokens auto-expire after 30 days of inactivity, and continue to work indefinitely while in active use; either way, rotate every 90 days to bound exposure if a token is ever leaked.
These tokens grant whatever permissions the creating admin holds. Phase 1 needs only read access to users and groups — assign the token-creator a read-only admin role rather than Super Admin.
Phase 2: OAuth2 service apps (private-key JWT)
Phase 2 of this connector will add support for OAuth2 service apps using private-key JWT bearer tokens. That mode is more enterprise- friendly than static API tokens (key-scoped, per-app audit, no token inactivity expiry) but adds setup complexity. Phase 1 ships static API tokens only; if you need OAuth2 service apps today, track the phase-2 work item or open a feature request.
Step 2 — Provision the secret in IdentityMesh
The connector reads its API token through IdentityMesh’s secret
store. Use a stable secret reference name like
secret://okta/<org-name>/apitoken.
The shipped helper script does this for you:
cd <connectors-source>\IdentityMesh.Connectors.Okta
.\sample.ps1 `
-ConnectionString "Server=.;Database=IdentityMesh;Trusted_Connection=True;" `
-OrgDomain "contoso.okta.com" `
-ApiToken "<paste-token-from-step-1>" `
-SecretRef "secret://okta/contoso/apitoken"
The script DPAPI-encrypts the token with the same scheme described
in secrets-and-dpapi.md, upserts it into
IM_Secrets, and prints a connector configuration block ready for
the next step.
If you have migrated to Azure Key Vault
(secrets-keyvault.md) or HashiCorp Vault
(secrets-vault.md) instead of DPAPI, store the
token in your vault under the same reference name. The
AuthMaterializer resolves the reference at run time — no connector
code changes are required.
Step 3 — Configure the connector
Create the connector through the Admin UI or by POSTing to
/api/connectors. The configuration is two JSON documents.
ConfigJson
{
"OrgDomain": "contoso.okta.com",
"UserSearchFilter": null,
"PageSize": 200,
"ImportUsers": true,
"ImportGroups": true,
"ImportMemberships": true,
"ExternalIdAttribute": "id"
}
| Field | Default | Notes |
|---|---|---|
OrgDomain | (req) | The Okta org domain, no scheme — e.g. contoso.okta.com |
UserSearchFilter | null | Optional Okta filter expression — see “Scoping the user set” |
PageSize | 200 | Okta caps users and groups at 200; lower values reduce per-page work |
ImportUsers | true | Set to false to skip the user list query entirely |
ImportGroups | true | Set to false to skip the group list query entirely |
ImportMemberships | true | Set to false for very large orgs — see “Scaling notes” |
ExternalIdAttribute | "id" | id (immutable Okta object ID) is recommended. login is allowed but the login can change |
AuthJson
{
"Mode": "ApiKey",
"ApiKey": "secret://okta/contoso/apitoken"
}
The ApiKey field is a secret reference, not the plaintext
token. The Auth Materializer resolves it at run time and only the
resulting SSWS header value is held in memory for the lifetime of
one import.
What gets synced
User attributes
The connector flattens the standard Okta profile sub-object onto
the mesh attribute store, plus a small set of top-level lifecycle
fields.
| Okta field | Mesh attribute | Notes |
|---|---|---|
id | ExternalId | Default external ID; immutable Okta ID |
status | status | ACTIVE, STAGED, PROVISIONED, RECOVERY, PASSWORD_EXPIRED, LOCKED_OUT, SUSPENDED, DEPROVISIONED |
| (derived) | accountEnabled | Boolean: true when status is ACTIVE, else false |
created | created | ISO 8601 UTC |
lastUpdated | lastUpdated | ISO 8601 UTC; drives the watermark |
lastLogin | lastLogin | May be null for accounts that haven’t signed in |
passwordChanged | passwordChanged | |
statusChanged | statusChanged | |
activated | activated | |
type | type | User type object (free-form) |
profile.firstName | firstName | |
profile.lastName | lastName | |
profile.displayName | displayName | |
profile.email | email | |
profile.secondEmail | secondEmail | |
profile.login | login | Sign-in name; can change |
profile.mobilePhone | mobilePhone | |
profile.primaryPhone | primaryPhone | |
profile.streetAddress | streetAddress | |
profile.city | city | |
profile.state | state | |
profile.zipCode | zipCode | |
profile.countryCode | countryCode | Two-letter country code |
profile.title | title | |
profile.department | department | |
profile.manager | manager | Manager display name |
profile.managerId | managerId | Manager Okta ID |
profile.costCenter | costCenter | |
profile.division | division | |
profile.employeeNumber | employeeNumber | |
profile.organization | organization | |
profile.userType | userType | |
profile.preferredLanguage | preferredLanguage | |
profile.locale | locale | |
profile.timezone | timezone |
Custom Okta profile attributes (the extensible profile schema), MFA factors, app assignments, and group rules are not imported in phase 1 — see “What’s deferred to phase 2”.
Group attributes
| Okta field | Mesh attribute | Notes |
|---|---|---|
id | ExternalId | |
type | type | OKTA_GROUP, APP_GROUP, BUILT_IN |
created | created | ISO 8601 UTC |
lastUpdated | lastUpdated | ISO 8601 UTC; drives the group watermark |
lastMembershipUpdated | lastMembershipUpdated | ISO 8601 UTC; updates whenever a member is added/removed |
objectClass | objectClass | Array → comma-joined string in the mesh |
profile.name | name | Group display name |
profile.description | description |
Group members
Each (group, member) edge is emitted as a separate GroupMember
object with a composite external ID <groupId>:<memberId>. The
attributes carry:
groupId— the Okta group IDmemberId— the Okta user IDmemberType— alwaysUser. Okta does not support nested groups natively, so unlike the Entra connector you will not seeGroupmember rows here.
Scoping the user set
A typical Okta org has more accounts than IdentityMesh needs to
manage — service accounts, deactivated former employees, partner
guests. Use UserSearchFilter to narrow the set.
Examples:
{ "UserSearchFilter": "status eq \"ACTIVE\"" }
Only currently active users. Skips STAGED, SUSPENDED,
DEPROVISIONED, and other non-active lifecycle states.
{ "UserSearchFilter": "profile.userType eq \"Employee\"" }
Only employee accounts (assuming you populate userType on the
profile schema).
{ "UserSearchFilter": "profile.department eq \"Engineering\"" }
Only users in a particular department.
The filter is an Okta search filter — see the Okta filter docs for the full grammar and supported operators.
Note that Okta does not offer an equivalent filter on the
/groups endpoint; if you need to scope groups, use a naming
convention (e.g. prefix groups intended for IdentityMesh with
IM-) and post-filter in the mesh.
Watermark behaviour
Okta does not provide a true delta endpoint. Instead the connector
uses the lastUpdated timestamp as a filter:
- Full mode (no watermark, or
Mode = Fullin the request): walks the full/api/v1/usersand/api/v1/groupslists. - Delta mode: filters by
lastUpdated gt "<stored timestamp>".
The connector encodes one high-water timestamp per object type into a tiny JSON wrapper so a single watermark string covers everything:
{ "u": "2026-04-22T08:15:30.000Z", "g": "2026-04-21T17:00:00.000Z" }
Memberships do not have their own cursor — they are recomputed from the current group set on every run.
Because the watermark is a plain ISO 8601 timestamp (not an opaque delta link), an operator can read it directly out of the database when investigating a stuck sync, and even hand-edit it to force a particular cutover. That is materially easier to debug than Graph’s deltaLink tokens.
Hard-delete detection (important gap)
The lastUpdated filter only fires when a user is modified.
Okta does not always raise a modification event when a user is
hard-deleted from the directory, so deletes — particularly the
DEPROVISIONED → purged flow — may not surface through delta runs.
The connector partially mitigates this: any user it sees with
status = DEPROVISIONED is emitted as a Delete change. But this
only catches deprovisioning that is observable through lastUpdated.
Operator action: schedule a periodic full sync — typically
weekly — so the connector walks the full user list end-to-end and
the mesh can detect users that have been hard-deleted from Okta
since the last full pass. Configure this through the Admin UI’s
schedule editor or by setting Mode = Full on a recurring run.
Phase 2 will close this gap properly by reading the Okta System Log
(/api/v1/logs) for user.lifecycle.delete events. Until then,
the periodic full sync is the supported workaround.
Throttling and rate limits
Okta enforces rate limits per-org and per-app. Bulk reads on large orgs can occasionally trip them — particularly the per-org concurrent request limit and the per-endpoint per-minute limits.
The connector handles Retry-After automatically:
- On HTTP 429 (Too Many Requests) or 503 (Service Unavailable), the
connector sleeps for the duration in the
Retry-Afterresponse header (or capped exponential backoff if the header is absent). - It retries up to 3 times before surfacing the error to the engine.
- The maximum back-off is 30 seconds — anything longer is treated as a control-plane issue worth surfacing.
If you see Retry-After log entries during normal operation:
- Lower
PageSizefrom 200 to 100. Fewer rows per request reduces the chance of long-running queries that count against the per- resource budget. - Spread connector schedules so the user, group, and membership passes don’t all fire at the same minute.
- Check the
X-Rate-Limit-*response headers exposed in the connector logs — they tell you exactly how close you are to each limit and when it resets.
For very large orgs (50,000+ users), consider:
- Splitting users and groups across two connectors (one with
ImportUsers=true, ImportGroups=falseand one inverse). - Disabling membership reads on the user/group connector and adding a third connector dedicated to memberships.
- Filing an Okta support ticket to request raised rate limits if your import schedule genuinely cannot fit within the standard per-org budget.
Security model
- App-only auth — no user impersonation, no consent prompts. The connector authenticates as the API token’s owning admin, so audit log entries in Okta attribute the reads to that admin account.
- Outbound only — the connector calls
https://<your-org>.okta.com. No inbound paths. - Read-only in phase 1 — the connector never issues a write request. The token’s permissions also constrain what’s possible even if a phase-2 build accidentally regressed.
- Token in transit — the API token never leaves the
IdentityMesh secret store unencrypted; the
SSWSheader value is composed in memory and discarded after each request. - Logging — API tokens, secret values, and bearer headers are never written to logs. The connector logs page counts, watermark advances, and any rate-limit warnings, but not authentication material.
What’s deferred to phase 2
The following are not in phase 1 and will be added in a follow-up phase:
- Export / write-back — creating users, updating profile
attributes, managing group membership, and deactivating accounts.
Phase 1 calls to
ExportAsyncthrowNotSupportedExceptionwith an explanatory message, so the engine surfaces the limitation cleanly rather than silently dropping changes. - OAuth2 service apps (private-key JWT) — replaces static API tokens with a key-scoped, per-app credential pattern. Better audit trail and no token-inactivity expiry.
- System Log driven hard-delete detection — read
/api/v1/logs?filter=eventType eq "user.lifecycle.delete"and emit Delete changes for users that have been purged from the directory. Closes the periodic-full-sync workaround documented above. - App assignments — import which Okta apps each user is assigned to, plus the assignment metadata. Useful for certification campaigns.
- MFA factors — read each user’s enrolled factors (SMS, push, WebAuthn, etc.) for compliance reporting.
- Custom profile attributes — the extensible Okta profile
schema. Phase 2 will add an opt-in flag to import every
profile.*field, including custom attributes the operator has added through the Okta admin console. - Group rules — the rule definitions that drive dynamic group membership. Useful for understanding why a user landed in a particular group.
Troubleshooting
401 Unauthorized on every page
- The API token has expired (30 days of inactivity) or been revoked. Generate a new token in Security → API → Tokens and update the secret store entry.
- The admin who created the token has been deactivated. Tokens inherit the creator’s lifecycle — recreate the token from an active admin account.
403 Forbidden on /api/v1/users or /api/v1/groups
- The token’s owning admin lacks read access to users or groups. Recreate the token from an admin role that holds the required read permissions (the built-in Read-Only Administrator role works).
Repeated Retry-After log entries
- See “Throttling and rate limits” above. Lower
PageSize, split the connector, or stagger schedules.
Slow imports
- Disable
ImportMembershipsif you don’t yet need group-membership data in the mesh. Group-member reads dominate runtime on orgs with thousands of groups. - Lower
PageSizeif the slowness coincides withRetry-Afterwarnings — the perceived slowness is throttling, not request latency.
Users that were hard-deleted from Okta still appear in the mesh
- This is the documented
lastUpdatedgap. Trigger a Full-mode run to walk the entire user list and let the mesh reconcile. - Schedule a recurring full sync (weekly is typical) so this reconciliation happens automatically.
Watermark “stuck” at an old timestamp
- The watermark column in the database stores the JSON wrapper
{"u":"...","g":"..."}. Inspect it directly in SQL to confirm the current high-water value. If it looks wrong, you can edit it by hand (or trigger a Full run, which ignores it).
Cross-references
secret-rotation.md— rotating the Okta API token on a 90-day cadence.secrets-and-dpapi.md— the default DPAPI-backed secret store used by the helper script.secrets-keyvault.md— pointing the connector at Azure Key Vault instead of DPAPI.secrets-vault.md— pointing the connector at HashiCorp Vault instead of DPAPI.connector-entra.md— the sister Entra ID connector. Run alongside Okta during an Okta → Entra migration; the merge behaviour in the mesh is governed by the join rules configured in the Admin UI.relay-agent.md— running this connector on a relay agent in a remote network instead of the central engine.