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, GroupMember
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 IdentityMesh.Connectors.Okta.dll connector DLL in the engine’s Connectors/ folder (the installer drops it there automatically once installer wiring lands; manual install is described below).
    • 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, 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"
}
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 “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 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

Each (group, member) edge is emitted as a separate GroupMember object with a composite external ID <groupId>:<memberId>. The attributes carry:


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

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:

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