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
1 change: 1 addition & 0 deletions docs/5-integrations/extensions/third-party/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Extensions built by third-party developers that integrate external tools and ser
- [Hayabusa](hayabusa.md) - Windows event log analysis
- [Microsoft Response](microsoft-response.md) - Entra ID, Intune, Defender XDR, and Defender for Endpoint investigation and response actions
- [NIMS](nims.md) - NIMS integration
- [Okta](okta.md) - Okta identity investigation and response (suspend, MFA/password reset, session & token revocation)
- [OTX](otx.md) - AlienVault OTX threat intelligence
- [PagerDuty](pagerduty.md) - Incident management notifications
- [Plaso](plaso.md) - Timeline analysis with Plaso
Expand Down
174 changes: 174 additions & 0 deletions docs/5-integrations/extensions/third-party/okta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Okta

The Okta LimaCharlie Extension exposes the incident-response and investigation surface of an Okta org (the **Okta management API**) to D&R rules and AI agents. It enables automated investigation and containment of an Okta account compromise directly from detections — suspend a user, reset their MFA, kill their sessions **and** OAuth tokens, revoke their app grants, quarantine them into a group, and pull the System Log for triage.

The extension provides two layers:

- **Typed actions** for the common containment, credential, MFA, session, and triage workflows, with friendly parameter names and built-in safety rails.
- A generic **`api_call`** passthrough for any Okta management endpoint not covered by a typed action.

Authentication is either a user-owned **SSWS API token** or an **OAuth 2.0 "API Services" app** (client credentials) — no user interaction, no delegated tokens. Okta's **DPoP** (sender-constrained token) requirement is handled transparently.

## Setup

Pick **one** authentication mode.

### Option A — SSWS API token (simplest)

1. In the Okta Admin Console, go to **Security → API → Tokens → Create Token** and copy the token value (shown only once).
2. Create the token from a **dedicated service admin account**, not a person's account: an SSWS token inherits the creator's privilege level, and it is revoked after 30 days of inactivity or if the creating account is deactivated.

### Option B — OAuth API Services app (recommended)

1. In the Admin Console, go to **Applications → Create App Integration → API Services**.
2. On the app's **General** tab, switch client authentication to **Public key / Private key** and generate (or add) a key. Save the private key — a PEM or the private JWK Okta hands out.
3. On the app's **Okta API Scopes** tab, grant the `okta.*` scopes for the actions you will use (least-privilege set below), then admin-consent them.

> **Okta requires `private_key_jwt` for management scopes.** The org authorization server rejects a plain `client_id` + `client_secret` for `okta.*` scopes — use a private key. (A `client_secret` field is accepted by the extension for completeness but expect Okta to reject it for these scopes.) New API Services apps also have **DPoP** locked on; the extension detects and satisfies this automatically.

Least-privilege scopes:

| Capability | Scope |
| --- | --- |
| Read users / lists | `okta.users.read` |
| User lifecycle / password / factor writes | `okta.users.manage` |
| Clear sessions / revoke tokens | `okta.sessions.manage` |
| Read groups | `okta.groups.read` |
| Group membership changes | `okta.groups.manage` |
| System Log | `okta.logs.read` |

> The org authorization server **silently drops** requested-but-ungranted scopes from the minted token; a missing grant then surfaces as that endpoint returning **403** (`insufficient_scope`), not a mint failure. Grant only what you use — every action degrades independently.

### Subscribe to the extension

Subscribe to `ext-okta` from the LimaCharlie **Marketplace** (Extensions → Add-Ons).

### Store the secret

In **Secrets Manager**, create a secret (for example `okta-api-token` or `okta-private-key`) and paste the SSWS token or the private key as its value.

### Configure the extension

In **Extensions → ext-okta → Configuration**, fill in `org_url` plus the fields for your chosen auth mode:

| Field | Required | Value |
| --- | --- | --- |
| `org_url` | yes | Okta org URL or host, e.g. `https://acme.okta.com` (admin / `-admin` URLs are normalized). |
| `api_token` | Option A | Reference to the SSWS-token secret, e.g. `hive://secret/okta-api-token`. |
| `client_id` | Option B | The API Services app client ID. |
| `private_key` | Option B | Reference to the private-key secret (PEM or private RSA JWK), e.g. `hive://secret/okta-private-key`. |
| `key_id` | no | `kid` for a PEM private key (JWKs carry their own). |
| `client_secret` | no | App client secret (alternative to `private_key`; rejected by Okta for management scopes). |
| `scopes` | no | OAuth scope override; empty uses an IR-oriented default set. |

Provide `api_token` **or** the OAuth fields, not both.

## Actions

Every action that targets an entity requires an explicit selector (`user_id`, `group_id` + `user_id`, `factor_id`) — the extension refuses to run without one, preventing accidental org-wide containment.

`user_id` accepts either the Okta user id (e.g. `00u1a2b3c4...`) or the user's login (e.g. `alex@acme.com`).

### Pagination

Okta paginates with the HTTP `Link` header (`rel="next"`), not a body cursor. The `list_*` actions return `{data: [...], pagination: {next_link}}`; pass the opaque `next_link` back as the `next_link` parameter to fetch the next page (it is an absolute, self-contained URL — never hand-build it). `limit` is clamped to 200 for users/groups and 1000 for the System Log.

### Generic

#### `api_call`

Generic passthrough to the Okta management API.

| Field | Type | Notes |
| --- | --- | --- |
| `method` | enum | `GET` (default), `POST`, `PUT`, `DELETE`. |
| `path` | string | **Required.** Path relative to the org base (e.g. `api/v1/users/{id}/lifecycle/reactivate`) or a full `Link: rel="next"` URL. |
| `query` | object | Query-string parameters. |
| `headers` | object | Extra request headers. |
| `body` | object | JSON body for `POST`/`PUT`. |

### User investigation

| Action | Parameters | What it does |
| --- | --- | --- |
| `list_users` | `q`, `search`, `filter`, `sort_by`, `sort_order`, `limit`, `after`, `next_link`, `extra_query` | List/search users. Use `q` for a starts-with search on name/email, `search` for an expression (e.g. `status eq "ACTIVE"` or `profile.email eq "a@b.com"`), or `filter` for the older syntax. |
| `get_user` | `user_id` | Get one user. Read the returned `status` before a lifecycle action (see below). |
| `list_user_factors` | `user_id` + pagination | List a user's enrolled MFA factors (`id`, `factorType`, `status`) — spot attacker-registered MFA. |
| `list_user_groups` | `user_id` + pagination | List a user's group memberships (find a privileged group to strip, or confirm quarantine). |
| `list_user_grants` | `user_id` + pagination | List a user's OAuth consent grants (which apps/scopes they authorized). |
| `list_user_roles` | `user_id` + pagination | List a user's admin roles — is the account privileged (e.g. `SUPER_ADMIN`)? |
| `list_groups` | `q`, `search`, `filter`, `limit`, `after`, `next_link`, `extra_query` | List/search groups. Only `OKTA_GROUP`-type groups accept membership changes. |
| `list_system_log` | `since`, `until`, `filter`, `q`, `sort_order`, `limit`, `next_link`, `extra_query` | Query the System Log (`/api/v1/logs`). Scope with `since`/`until` (ISO-8601) and a SCIM `filter`, e.g. `eventType eq "user.session.start"`, `actor.id eq "00u..."`, or `target.id eq "00u..."`. |

Read the user's `status` first: `suspend` needs `ACTIVE`, `unsuspend` needs `SUSPENDED`, `activate` needs `STAGED`/`DEPROVISIONED`, `unlock` needs `LOCKED_OUT`. A `400` on a lifecycle action is a state mismatch; a `403` is a scope/role problem.

### User lifecycle

| Action | Parameters | What it does |
| --- | --- | --- |
| `suspend_user` | `user_id` | Block sign-in while preserving the account and assignments — the recommended **reversible** containment lever. Reverse with `unsuspend_user`. |
| `unsuspend_user` | `user_id` | Return a suspended user to `ACTIVE`. |
| `deactivate_user` | `user_id`, `send_email` (default `false`) | Deactivate/deprovision the user. **Destructive** — removes app access and is not a clean inverse of `activate`; prefer `suspend_user`. May complete asynchronously on large orgs. |
| `activate_user` | `user_id`, `send_email` (default `false`) | Activate a `STAGED`/`DEPROVISIONED` user. With `send_email=false` the response includes an activation URL/token. |
| `unlock_user` | `user_id` | Unlock a `LOCKED_OUT` user, returning them to `ACTIVE`. |

### Credentials

| Action | Parameters | What it does |
| --- | --- | --- |
| `expire_password` | `user_id`, `temp_password` (default `false`) | Force a password change at next sign-in (`PASSWORD_EXPIRED`). With `temp_password=true` the current password is invalidated immediately and a one-time temp password is returned. |
| `reset_password` | `user_id`, `send_email` (default `false`) | Trigger a reset (transitions the user to `RECOVERY`). With `send_email=false` the response carries a one-time `resetPasswordUrl` to deliver out-of-band. |
| `set_user_password` | `user_id`, `password` | Set a specific new password directly — lock an attacker out with a known-only-to-you value. Must satisfy the org password policy. |

### MFA

| Action | Parameters | What it does |
| --- | --- | --- |
| `reset_user_factors` | `user_id`, `remove_recovery_enrollment` (default `false`) | Unenroll **all** of a user's MFA factors, forcing re-enrollment — high-value against attacker-registered MFA. Does not change the password. |
| `reset_user_factor` | `user_id`, `factor_id`, `remove_recovery_enrollment` (default `false`) | Unenroll a **single** factor (from `list_user_factors`). Note: removing a push/signed-nonce factor also removes the user's related Okta Verify factors. |

### Sessions & OAuth tokens

| Action | Parameters | What it does |
| --- | --- | --- |
| `clear_user_sessions` | `user_id`, `oauth_tokens` (default `true`), `forget_devices` (default `false`) | Revoke all of a user's Okta sessions, forcing re-authentication. `oauth_tokens` defaults to **`true`** (the IR-safe choice) so refresh/access tokens are revoked too — Okta's own API default of `false` leaves them valid, the single most common containment mistake. `forget_devices` also clears remembered-device / factor-trust. |
| `revoke_user_grants` | `user_id` | Revoke **all** of a user's OAuth consent grants across all clients — cut off OAuth-based persistence. Inspect first with `list_user_grants`. |

> Clearing Okta sessions does **not** terminate sessions already established inside downstream apps (M365, Salesforce, …); those need the app's own session revocation.

### Group containment

| Action | Parameters | What it does |
| --- | --- | --- |
| `add_user_to_group` | `group_id`, `user_id` | Add a user to an `OKTA_GROUP` — e.g. drop a compromised user into a quarantine / high-friction sign-on-policy group. Find the group id with `list_groups`. |
| `remove_user_from_group` | `group_id`, `user_id` | Remove a user from a group — e.g. strip a compromised user out of a privileged/admin group. |

### Containment sequencing

For a confirmed account takeover, the effective combination is: `reset_user_factors` (drop attacker MFA) → `expire_password` with `temp_password=true` **or** `set_user_password` (invalidate the credential) → `clear_user_sessions` (kill live sessions **and** OAuth tokens) → `revoke_user_grants` (cut OAuth persistence). Revoke sessions/tokens **last** so the attacker's live session can't outlive the other steps. Use `suspend_user` as the single reversible lever to stop everything first.

## Detection & Response

Example response action that suspends the Okta user named in a detection:

```yaml
- action: extension request
extension action: suspend_user
extension name: ext-okta
extension request:
user_id: '{{ .event/user_id }}'
```

> **Wrap literal strings in `{{ "..." }}`.**
> Values under `extension request` are evaluated as templates. A bare string without `{{ }}` is interpreted as a [gjson](https://github.com/tidwall/gjson) path against the event and, if it doesn't resolve, the key is silently dropped from the payload.

`extension request` actions are fire-and-forget — the rule engine does not surface the response back into the rule's evaluation context. Workflows that chain (find the user, reset factors, clear sessions, revoke grants) belong in a [Playbook](../limacharlie/playbook.md) or an AI agent, which can hold state between calls.

## Notes

- **Two auth modes.** SSWS is the fastest to set up but is tied to an admin account's privilege and dies after 30 days of inactivity. The OAuth API Services app (private_key_jwt) is decoupled from any person, uses short-lived scoped tokens, and is the direction Okta officially steers integrations.
- **DPoP** (sender-constrained tokens) is forced on for new API Services apps; the extension mints a plain token first and, if Okta answers `invalid_dpop_proof`, transparently flips to DPoP for the token request and every API call.
- A single `401` on a call using an OAuth token is treated as a token-expiry race: the cached token is dropped and the request retried once with a fresh token. Rotating the secret in Secrets Manager recovers the same way — the next auth failure evicts the cached client and re-reads the secret.
- Token-endpoint `429`/`5xx` are backed off and retried; a data-endpoint `429` honors Okta's `Retry-After` for a bounded number of retries.
- Error messages are formatted `okta api <status> on <method> <path>: <errorCode>: <errorSummary>`, with query strings redacted.
- Unsubscribing from the extension preserves its saved configuration; re-subscribing restores it without reconfiguration.
Loading