Skip to content

Okta Sensor ​

What it ingests ​

The directory from the Okta Management API: who works here, which groups they're in, and who they report to.

Okta objectOntology entitySignalEdges
GroupOrganizationgroupPerson MEMBER_OF the group, for each member
UserPerson (identity)userPerson REPORTS_TO Person, when the manager resolves

A group is an org unit, like a Rippling team. A user is identity. Each one resolves to a Person keyed on their Okta user id (okta:00u…). That key is a reserved identity key, so renames and email changes stay on the same Person, and two employees with the same name stay separate. The user's work email and display name are added as aliases too. That's how an Okta user joins up with the same person's Slack messages, GitHub commits, and calendar invites.

REPORTS_TO runs from the report to their manager. It's only drawn when the sensor can resolve the user's managerId to another Okta user (see Reporting lines). Okta's display-only manager string is kept on the event as evidence, but never becomes an edge on its own, because a bare name could match anyone with that name.

Deprovisioned users still resolve, so their past activity stays attributed to them, but they get no MEMBER_OF or REPORTS_TO edges. That applies to both ends: a user whose managerId still names a deprovisioned manager gets no REPORTS_TO either. The built-in Everyone group is ingested as an Organization without its members, since everyone is in it.

Read-only and poll-based. Only these profile attributes are read: login, email, first/last/display name, title, department, division, organization, user type, employee number, managerId, and manager. Phone numbers, postal addresses, and secondary email are never decoded, so they never reach an event.

This is Okta alone. Microsoft Entra ID is a separate source.

Credential ​

The sensor takes one of two credentials. Set exactly one: saving a sensor with neither or both is rejected with invalid_credential_choice.

References an okta_service_app credential by id. The secret is a JSON blob:

json
{
  "client_id": "0oa1b2c3d4e5f6g7h8i9",
  "key_id": "the kid shown next to the public key in Okta (optional)",
  "private_key": "-----BEGIN PRIVATE KEY-----\n…\n-----END PRIVATE KEY-----"
}

private_key is the PEM RSA private key (PKCS#8 BEGIN PRIVATE KEY or PKCS#1 BEGIN RSA PRIVATE KEY) whose public half is registered on the app. The sensor signs a short-lived RS256 client assertion with it, exchanges it at https://<org>.okta.com/oauth2/v1/token, and requests only okta.users.read okta.groups.read. A client_secret is refused, because this client authenticates with the key. JWK-format keys aren't accepted, so download or convert to PEM first.

bash
curl -sS -X POST https://your-corveil-host/api/credentials \
  -H "Authorization: Bearer <admin>" -H "Content-Type: application/json" \
  -d '{"type":"okta_service_app","name":"acme-okta","secret":"{\"client_id\":\"0oa…\",\"private_key\":\"-----BEGIN PRIVATE KEY-----\\n…\"}"}'

SSWS API token ​

References an okta_api_token credential by id. The secret is an Okta API token, sent as Authorization: SSWS <token>. Store only the token value, not SSWS …. The token inherits its creator's admin role, so create it as a Read-Only Administrator.

bash
curl -sS -X POST https://your-corveil-host/api/credentials \
  -H "Authorization: Bearer <admin>" -H "Content-Type: application/json" \
  -d '{"type":"okta_api_token","name":"acme-okta","secret":"<api-token>"}'

Okta recommends service apps over API tokens. A token expires after 30 days without use and acts with its creator's full admin role, not with scopes.

Config fields ​

KeyRequiredNotes
org_urlyesYour org's URL: https://<org>.okta.com, .oktapreview.com, .okta-emea.com, .okta-gov.com, or .okta.mil. The admin console URL (<org>-admin.okta.com) is accepted and normalized. Custom domains and any other host are rejected; the Management API is also served on the okta.com URL.
enabled_objectsnoComma-separated: groups, users. Blank polls both. Unknown names are rejected at configure time.

There's no default_entity_type. Groups are Organization and users are identity, so a single override would assign the wrong type to one of them.

Provider-side setup ​

Service app ​

  1. In the Okta Admin Console, go to Applications → Applications → Create App Integration and choose API Services. Name it (for example, Corveil directory sensor).
  2. On the app's General tab, under Client Credentials, click Edit and set Client authentication to Public key / Private key.
  3. Under Public keys, click Add key → Generate new key. Switch the download format to PEM and copy the private key. Okta shows it once. Save the key, and note the kid.
  4. Turn off Require Demonstrating Proof of Possession (DPoP) header in token requests, which Okta enables by default on new API Services apps. This sensor doesn't send DPoP proofs, so the token request fails with invalid_dpop_proof while it's on.
  5. On the Okta API Scopes tab, grant okta.users.read and okta.groups.read. Grant nothing else.
  6. On the Admin roles tab, assign Read-Only Administrator. Without an admin role, the scoped token can authenticate but still gets 403 on the directory.
  7. Store {"client_id","private_key","key_id"} as an okta_service_app credential.

API token ​

  1. Sign in as a user who holds only the Read-Only Administrator role.
  2. Go to Security → API → Tokens → Create token, name it, and copy the value. Okta shows it once.
  3. Store the token as an okta_api_token credential.

Wire-up ​

POST /api/sensors

json
{
  "sensor_type": "okta",
  "name": "Acme Okta",
  "config": {
    "org_url": "https://acme.okta.com",
    "enabled_objects": "groups, users"
  },
  "credentials": { "service_app": "<okta_service_app credential id>" }
}

For an API token, use "credentials": { "api_token": "<okta_api_token credential id>" } instead.

Verify ​

Run Test connection. It reads one page of each enabled object, so a bad key, a missing scope, or a missing admin role fails at save time rather than on the first poll. If a service app was granted okta.users.read but not okta.groups.read, the groups probe is the one that fails. Failures carry Okta's errorCode (for example E0000006 for a missing permission, or invalid_client for a key that doesn't match the app).

Once polling, the per-poll summary log reports groups_collected, users_collected, and manager_lookups.

How incremental sync works ​

The sensor polls on the worker's regular tick, every 5 minutes.

  • The first poll is a full sync. It walks the whole directory from the beginning, so the identity backbone is complete without a separate backfill. A large org takes several ticks (see the per-tick budgets below), and each tick resumes where the last stopped.
  • Users are searched with lastUpdated gt "<watermark>", oldest first. search (unlike filter) also returns deprovisioned users, so a deactivation reaches the graph. A test pins the literal so a typo can't silently re-read the whole directory every tick.
  • Groups are searched with lastUpdated or lastMembershipUpdated. Adding someone to a group changes only lastMembershipUpdated, so a membership change re-reads that group and its current members.
  • The watermark is the start of the last finished walk, minus a 5-minute overlap. Okta's search index is eventually consistent, so a change made during a walk is always inside the next walk's window. Rows read twice are absorbed by event-id dedup.
  • Edits re-ingest. Event ids carry the row's version (okta:user:<id>@<lastUpdated>), while the ontology entity keeps the stable okta:group:<id>. A renamed group updates its one Organization instead of creating a new one.
  • Per-tick budgets. Up to 10 pages of 200 users, up to 300 requests on the groups walk (groups are read 20 per page, each with up to 5 pages of 200 members), and about 400 manager lookups (checked between pages). When a budget runs out, the cursor saves Okta's after link and the next tick resumes there.
  • Rate limits are shared with every other integration in your Okta org. The sensor spaces requests 120 ms apart, and when X-Rate-Limit-Remaining reaches zero it waits for X-Rate-Limit-Reset (up to 30 seconds). A 429 ends the tick with the cursor unchanged, and the next tick retries the same window.
  • Pagination links are host-pinned. Okta's Link: rel="next" URL is absolute and this client sends Authorization on every request, so a link pointing at any other host is refused.

Reporting lines ​

Okta's managerId attribute is free-form. Depending on how your HR import maps it, it holds the manager's Okta user id, their login, or an HR employee number. The sensor tries each in turn:

  1. A user it has already seen this tick, by id, login, or employeeNumber. This costs no request.
  2. GET /api/v1/users/{managerId}, which accepts an id or a login.
  3. A search on profile.employeeNumber eq "<managerId>".

A managerId that matches nobody gets no edge. Its result is cached for the tick, so a dangling value costs one lookup, not one per report. A user whose managerId points at themselves gets no edge.

Known limits ​

  • Removed edges stay. When someone leaves a group or changes manager, the new edge is added, but the old MEMBER_OF or REPORTS_TO edge isn't retracted. The group or user properties do update.
  • Deletes are invisible. A deleted group or user (as opposed to deactivated) disappears from search results, and the sensor doesn't treat that as a deletion.
  • Very large groups are truncated at 1,000 members, with members_truncated: true on the group. These are usually broad groups pushed from AD or an app, and MEMBER_OF edges to them add little.
  • DPoP isn't supported. Turn off the DPoP requirement on the service app (setup step 4).
  • Custom domains aren't accepted as org_url. Use the org's okta.com URL.

Not included ​

  • Applications and app assignments, factors, sessions, and devices. Not read.
  • The System Log and event hooks. v1 polls the directory instead. Event hooks need an inbound endpoint registered in the Okta org, which isn't provisioned at install time.
  • Group rules and nested group sources. Okta groups aren't nested. Groups pushed from AD/LDAP are read like any other group.
  • Microsoft Entra ID. A separate source.