Runbooks/Connector Okta

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

PropertyValue
Connector nameOkta
DirectionImport (one-way, source → mesh) in phase 1
Object typesUser, Group (membership imports as the group’s member attribute)
AuthenticationApp-only — Okta API token (SSWS scheme) in phase 1
Watermark stylePer-object-type lastUpdated ISO 8601 timestamps
Throttling behaviourHonours Retry-After on HTTP 429 / 503, up to 3 retries
Required permissionsAPI token must inherit a role with read access to users and groups (read-only admin is sufficient)

Prerequisites

You will need:

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

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

  3. An IdentityMesh deployment with:

    • The Okta connector DLL in the host’s Connectors/ folder. Both the Sync Engine MSI and the Relay Agent MSI install it; nothing to copy (see relay-agent.md).
    • Read access to the IM_Secrets table (or to your Key Vault / Vault backend if you’ve migrated secrets per secrets-keyvault.md / secrets-vault.md).
  4. 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

  1. Sign in to your Okta admin console at https://<your-org>.okta.com/admin as an org administrator.

  2. Navigate to Security → API → Tokens.

  3. Click Create Token.

    FieldValue
    NameIdentityMesh-Import
    Scope(inherits the creator’s role)
  4. 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.

  5. 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, 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.

Store it with the secretscli tool that ships with the Sync Engine, run on the engine host:

secretscli set secret://okta/contoso/apitoken "<paste-token-from-step-1>"

The tool DPAPI-encrypts the token with the scheme described in secrets-and-dpapi.md and stores it in IM_Secrets. Confirm it with secretscli exists <ref>.

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. IdentityMesh resolves the reference at run time — the connector configuration is the same either way.


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"
}
FieldDefaultNotes
OrgDomain(req)The Okta org domain, no scheme — e.g. contoso.okta.com
UserSearchFilternullOptional Okta filter expression — see “Scoping the user set”
PageSize200Okta caps users and groups at 200; lower values reduce per-page work
ImportUserstrueSet to false to skip the user list query entirely
ImportGroupstrueSet to false to skip the group list query entirely
ImportMembershipstrueSet to false for very large orgs — see “Throttling and rate limits”
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. IdentityMesh 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 fieldMesh attributeNotes
idExternalIdDefault external ID; immutable Okta ID
statusstatusACTIVE, STAGED, PROVISIONED, RECOVERY, PASSWORD_EXPIRED, LOCKED_OUT, SUSPENDED, DEPROVISIONED
(derived)accountEnabledBoolean: true when status is ACTIVE, else false
createdcreatedISO 8601 UTC
lastUpdatedlastUpdatedISO 8601 UTC; drives the watermark
lastLoginlastLoginMay be null for accounts that haven’t signed in
passwordChangedpasswordChanged
statusChangedstatusChanged
activatedactivated
typetypeUser type object (free-form)
profile.firstNamefirstName
profile.lastNamelastName
profile.displayNamedisplayName
profile.emailemail
profile.secondEmailsecondEmail
profile.loginloginSign-in name; can change
profile.mobilePhonemobilePhone
profile.primaryPhoneprimaryPhone
profile.streetAddressstreetAddress
profile.citycity
profile.statestate
profile.zipCodezipCode
profile.countryCodecountryCodeTwo-letter country code
profile.titletitle
profile.departmentdepartment
profile.managermanagerManager display name
profile.managerIdmanagerIdManager Okta ID
profile.costCentercostCenter
profile.divisiondivision
profile.employeeNumberemployeeNumber
profile.organizationorganization
profile.userTypeuserType
profile.preferredLanguagepreferredLanguage
profile.localelocale
profile.timezonetimezone

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 fieldMesh attributeNotes
idExternalId
typetypeOKTA_GROUP, APP_GROUP, BUILT_IN
createdcreatedISO 8601 UTC
lastUpdatedlastUpdatedISO 8601 UTC; drives the group watermark
lastMembershipUpdatedlastMembershipUpdatedISO 8601 UTC; updates whenever a member is added/removed
objectClassobjectClassArray → comma-joined string in the mesh
profile.namenameGroup display name
profile.descriptiondescription

Group members

Membership is imported on the group, as a member attribute holding the members’ Okta user IDs joined with ; — the same shape as an Active Directory group’s member. A group with no members has no member attribute at all. Okta does not nest groups natively, so the list only ever holds user IDs.

Membership is deliberately not imported as objects of its own. A standalone membership row can never become a mesh relationship: the join engine only joins an object to a mesh object of the same type, and an inbound flow holds one value per attribute. Edges come from a multi-valued reference attribute on the group, so that is what the connector imports.

To turn the list into mesh membership you need three things:

  1. The member / Group schema row: reference, multi-valued. The installer’s database step creates it on every install and upgrade, so an upgraded system already has it. A database updated outside the installer needs the database step run (see Running the database step yourself) before the services start.
  2. An inbound attribute flow on this connector for the Group type: member → member. Inbound flows are rule-driven; without the rule the attribute never reaches the mesh.
  3. Join rules for User and Group that anchor on id. The engine matches each member ID against the mesh anchor, so users anchored on login never resolve.

The engine then writes one member relationship from the group to each resolved member, prunes the ones no longer listed on the next import, and skips members with no mesh object yet (they resolve on a later run).


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:

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

The group mark advances over the later of the two group timestamps, so a membership-only change is read once and not on every run after. Memberships have no cursor of their own: every group the delta returns has its full member list re-read, and its member attribute always carries the whole list.

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:

If you see Retry-After log entries during normal operation:

For very large orgs (50,000+ users), consider:


Security model


What’s deferred to phase 2

The following are not in phase 1 and will be added in a follow-up phase:


Troubleshooting

401 Unauthorized on every page

403 Forbidden on /api/v1/users or /api/v1/groups

Repeated Retry-After log entries

Slow imports

Users that were hard-deleted from Okta still appear in the mesh

Watermark “stuck” at an old timestamp


Cross-references