feat(okta): add System Log, MFA, sessions, apps, roles, and group rules (#6741)

Expands the Okta block from 18 to 44 operations, covering the System Log, MFA
factors, sessions, applications, administrator roles, and group rules.

Adds shared helpers for the SSWS auth header, Okta error parsing, and the
Link-header `after` cursor, and routes every tool through them so there is one
auth and error path. All eight list operations now return `nextCursor` and
`hasMore`.

Makes the block's param transform authoritative over the serialized inputs: the
executor merges it on top of them, so a key the transform omits keeps the raw
subBlock string. Assigning `undefined` is what actually drops it, which is what
keeps a non-numeric `limit` from reaching Okta verbatim and stops a blank field
in a partial `update_user` from overwriting the stored value with an empty
string.
This commit is contained in:
Waleed
2026-08-15 16:47:02 -07:00
committed by GitHub
parent 4fc0fb4bad
commit d45dad7e8b
56 changed files with 5896 additions and 357 deletions
+762 -5
View File
@@ -1,6 +1,6 @@
---
title: Okta
description: Manage users and groups in Okta
description: Manage users, groups, apps, and MFA in Okta
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
@@ -32,7 +32,7 @@ If you encounter issues with the Okta integration, contact us at [help@sim.ai](m
## Usage Instructions
Integrate Okta identity management into your workflow. List, create, update, activate, suspend, and delete users. Reset passwords. Manage groups and group membership.
Integrate Okta identity management into your workflow. Manage users, groups, and group rules. Run service desk actions like resetting MFA factors and clearing sessions. Review and change application assignments and admin roles. Query the System Log to audit sign-ins and admin changes.
@@ -50,7 +50,8 @@ List all users in your Okta organization with optional search and filtering
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `search` | string | No | Okta search expression \(e.g., profile.firstName eq "John" or profile.email co "example.com"\) |
| `filter` | string | No | Okta filter expression \(e.g., status eq "ACTIVE"\) |
| `limit` | number | No | Maximum number of users to return \(default: 200, max: 200\) |
| `after` | string | No | Opaque pagination cursor returned as nextCursor by a previous call |
| `limit` | number | No | Maximum number of users to return per page \(default: 200\) |
#### Output
@@ -72,6 +73,8 @@ List all users in your Okta organization with optional search and filtering
| ↳ `activated` | string | Activation timestamp |
| ↳ `statusChanged` | string | Status change timestamp |
| `count` | number | Number of users returned |
| `nextCursor` | string | Cursor for the next page, or null on the last page |
| `hasMore` | boolean | Whether more users are available |
| `success` | boolean | Operation success status |
### Get User from Okta
@@ -320,7 +323,8 @@ List all groups in your Okta organization with optional search and filtering
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `search` | string | No | Okta search expression for groups \(e.g., profile.name sw "Engineering" or type eq "OKTA_GROUP"\) |
| `filter` | string | No | Okta filter expression \(e.g., type eq "OKTA_GROUP"\) |
| `limit` | number | No | Maximum number of groups to return \(default: 10000, max: 10000\) |
| `after` | string | No | Opaque pagination cursor returned as nextCursor by a previous call |
| `limit` | number | No | Maximum number of groups to return per page \(max: 10000\) |
#### Output
@@ -335,6 +339,8 @@ List all groups in your Okta organization with optional search and filtering
| ↳ `lastUpdated` | string | Last update timestamp |
| ↳ `lastMembershipUpdated` | string | Last membership change timestamp |
| `count` | number | Number of groups returned |
| `nextCursor` | string | Cursor for the next page, or null on the last page |
| `hasMore` | boolean | Whether more groups are available |
| `success` | boolean | Operation success status |
### Get Group from Okta
@@ -490,7 +496,8 @@ List all members of a specific group in your Okta organization
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `groupId` | string | Yes | Group ID to list members for |
| `limit` | number | No | Maximum number of members to return \(default: 1000, max: 1000\) |
| `after` | string | No | Opaque pagination cursor returned as nextCursor by a previous call |
| `limit` | number | No | Maximum number of members to return per page \(default: 1000, but Okta recommends 200\) |
#### Output
@@ -512,6 +519,756 @@ List all members of a specific group in your Okta organization
| ↳ `activated` | string | Activation timestamp |
| ↳ `statusChanged` | string | Status change timestamp |
| `count` | number | Number of members returned |
| `nextCursor` | string | Cursor for the next page, or null on the last page |
| `hasMore` | boolean | Whether more members are available |
| `success` | boolean | Operation success status |
### List Group Rules from Okta
List the group rules in your Okta organization. Each rule assigns users to groups automatically based on an expression over their profile, so this shows how group membership is being driven.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `search` | string | No | Keyword to search group rules for |
| `after` | string | No | Opaque pagination cursor returned as nextCursor by a previous call |
| `limit` | number | No | Maximum number of rules to return \(default: 50, max: 200\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `rules` | array | Array of group rules |
| ↳ `id` | string | Group rule ID |
| ↳ `name` | string | Group rule name |
| ↳ `type` | string | Rule type, always group_rule |
| ↳ `status` | string | Rule status \(ACTIVE, INACTIVE, INVALID\) |
| ↳ `created` | string | Creation timestamp |
| ↳ `lastUpdated` | string | Last update timestamp |
| ↳ `expression` | string | Okta expression that decides which users the rule matches |
| ↳ `expressionType` | string | Expression language, typically urn:okta:expression:1.0 |
| ↳ `assignUserToGroupIds` | array | Groups that matching users are assigned to |
| ↳ `excludedUserIds` | array | Users excluded from the rule |
| ↳ `excludedGroupIds` | array | Groups excluded from the rule |
| `count` | number | Number of rules returned |
| `nextCursor` | string | Cursor for the next page, or null on the last page |
| `hasMore` | boolean | Whether more rules are available |
| `success` | boolean | Operation success status |
### Get Group Rule from Okta
Retrieve a single Okta group rule by ID, including the expression that decides which users it matches and the groups those users are assigned to.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `groupRuleId` | string | Yes | Group rule ID to look up |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Group rule ID |
| `name` | string | Group rule name |
| `type` | string | Rule type, always group_rule |
| `status` | string | Rule status \(ACTIVE, INACTIVE, INVALID\) |
| `created` | string | Creation timestamp |
| `lastUpdated` | string | Last update timestamp |
| `expression` | string | Okta expression that decides which users the rule matches |
| `expressionType` | string | Expression language, typically urn:okta:expression:1.0 |
| `assignUserToGroupIds` | array | Groups that matching users are assigned to |
| `excludedUserIds` | array | Users excluded from the rule |
| `excludedGroupIds` | array | Groups excluded from the rule |
| `success` | boolean | Operation success status |
### Create Group Rule in Okta
Create a group rule that automatically assigns users matching an Okta expression to one or more groups. New rules are created INACTIVE, so run Activate Group Rule afterwards to start applying it.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `ruleName` | string | Yes | Name for the group rule \(maximum 50 characters\) |
| `expression` | string | Yes | Okta expression that must evaluate to a boolean \(e.g., user.department=="Engineering"\) |
| `assignUserToGroupIds` | string | Yes | Comma-separated group IDs that matching users are assigned to |
| `excludedUserIds` | string | No | Comma-separated user IDs to exclude from the rule |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Created group rule ID |
| `name` | string | Group rule name |
| `type` | string | Rule type, always group_rule |
| `status` | string | Rule status, which is INACTIVE for a newly created rule |
| `created` | string | Creation timestamp |
| `lastUpdated` | string | Last update timestamp |
| `expression` | string | Okta expression that decides which users the rule matches |
| `expressionType` | string | Expression language, typically urn:okta:expression:1.0 |
| `assignUserToGroupIds` | array | Groups that matching users are assigned to |
| `excludedUserIds` | array | Users excluded from the rule |
| `excludedGroupIds` | array | Groups excluded from the rule |
| `success` | boolean | Operation success status |
### Activate Group Rule in Okta
Activate a group rule so Okta starts applying it, assigning every matching user to the target groups.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `groupRuleId` | string | Yes | Group rule ID to activate |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `groupRuleId` | string | Activated group rule ID |
| `activated` | boolean | Whether the rule was activated |
| `success` | boolean | Operation success status |
### Deactivate Group Rule in Okta
Deactivate a group rule so Okta stops applying it. Existing memberships the rule created are left in place. A rule must be INACTIVE before it can be edited.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `groupRuleId` | string | Yes | Group rule ID to deactivate |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `groupRuleId` | string | Deactivated group rule ID |
| `deactivated` | boolean | Whether the rule was deactivated |
| `success` | boolean | Operation success status |
### Delete Group Rule in Okta
Permanently delete a group rule. Destructive and irreversible. Optionally also removes the users that this rule had assigned from those groups, which revokes any access those groups grant.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `groupRuleId` | string | Yes | Group rule ID to delete |
| `removeUsers` | boolean | No | Also remove the users this rule assigned from the groups it targeted \(default: false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `groupRuleId` | string | Deleted group rule ID |
| `deleted` | boolean | Whether the rule was deleted |
| `success` | boolean | Operation success status |
### List Factors from Okta
List the MFA factors a user has enrolled, with each factor type, provider, and enrollment status. Use this before resetting a factor to confirm which one to target.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `userId` | string | Yes | User ID or login to list enrolled factors for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `factors` | array | Array of enrolled MFA factors |
| ↳ `id` | string | Factor ID |
| ↳ `factorType` | string | Factor type \(sms, call, email, push, question, token:software:totp, webauthn, etc.\) |
| ↳ `provider` | string | Factor provider \(OKTA, GOOGLE, FIDO, DUO, RSA, SYMANTEC, YUBICO, CUSTOM\) |
| ↳ `vendorName` | string | Factor vendor name |
| ↳ `status` | string | Enrollment status \(ACTIVE, PENDING_ACTIVATION, NOT_SETUP, etc.\) |
| ↳ `created` | string | Enrollment timestamp |
| ↳ `lastUpdated` | string | Last update timestamp |
| ↳ `profile` | json | Factor-specific attributes, which vary by factor type \(phone number, email, question, credential ID\) |
| `count` | number | Number of enrolled factors |
| `success` | boolean | Operation success status |
### Get Factor from Okta
Retrieve a single enrolled MFA factor for a user, including its type, provider, enrollment status, and factor-specific profile.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `userId` | string | Yes | User ID or login the factor belongs to |
| `factorId` | string | Yes | Factor ID to look up |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Factor ID |
| `factorType` | string | Factor type |
| `provider` | string | Factor provider |
| `vendorName` | string | Factor vendor name |
| `status` | string | Enrollment status |
| `created` | string | Enrollment timestamp |
| `lastUpdated` | string | Last update timestamp |
| `profile` | json | Factor-specific attributes, which vary by factor type \(phone number, email, question, credential ID\) |
| `success` | boolean | Operation success status |
### Enroll Factor in Okta
Enroll an MFA factor for a user. The profile fields required depend on the factor type: a phone number for sms and call, an email address for email, and a question and answer for question. Factors that enroll from the user device, such as webauthn and push, need no profile fields.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `userId` | string | Yes | User ID or login to enroll the factor for |
| `factorType` | string | Yes | Factor type to enroll \(sms, call, email, question, push, token:software:totp, u2f, webauthn\) |
| `provider` | string | Yes | Factor provider \(OKTA, GOOGLE, FIDO, DUO, RSA, SYMANTEC, YUBICO, CUSTOM\). Each provider supports a subset of factor types |
| `phoneNumber` | string | No | Phone number in E.164 format. Required for the sms and call factor types |
| `factorEmail` | string | No | Email address to enroll. Required for the email factor type |
| `securityQuestion` | string | No | Security question key \(e.g., disliked_food\). Required for the question factor type |
| `securityAnswer` | string | No | Answer to the security question, minimum 4 characters. Required for the question factor type |
| `activate` | boolean | No | Activate the factor immediately as part of enrollment. Supported by the sms, call, email, and token:hotp factor types \(default: false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Enrolled factor ID |
| `factorType` | string | Factor type |
| `provider` | string | Factor provider |
| `vendorName` | string | Factor vendor name |
| `status` | string | Enrollment status, typically PENDING_ACTIVATION until the user activates it |
| `created` | string | Enrollment timestamp |
| `lastUpdated` | string | Last update timestamp |
| `profile` | json | Factor-specific attributes, which vary by factor type \(phone number, email, question, credential ID\) |
| `enrolled` | boolean | Whether the factor was enrolled |
| `success` | boolean | Operation success status |
### Reset Factor in Okta
Unenroll one specific MFA factor for a user so they can re-enroll it. Destructive and irreversible: the existing enrollment is removed. Unenrolling a push or signed_nonce factor also unenrolls the related Okta Verify factors. Factors cannot be unenrolled from a deactivated user.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `userId` | string | Yes | User ID or login the factor belongs to |
| `factorId` | string | Yes | Factor ID to unenroll |
| `removeRecoveryEnrollment` | boolean | No | Also remove the phone number as a recovery method, not only as a factor. Applies to sms and call factors only \(default: false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `userId` | string | User the factor belonged to |
| `factorId` | string | Unenrolled factor ID |
| `reset` | boolean | Whether the factor was unenrolled |
| `success` | boolean | Operation success status |
### Reset All Factors in Okta
Reset every MFA factor for a user, returning all enrollments to the unenrolled state. Destructive and irreversible: the user must re-enroll each factor before they can complete MFA again. The user status stays ACTIVE.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `userId` | string | Yes | User ID or login whose MFA factors will all be reset |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `userId` | string | User whose factors were reset |
| `reset` | boolean | Whether all factors were reset |
| `success` | boolean | Operation success status |
### Clear User Sessions in Okta
Revoke every active Okta session for a user, signing them out of all devices immediately. Destructive and irreversible: the user must sign in again. Optionally also revokes their OAuth and OpenID Connect tokens, and clears remembered factors.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `userId` | string | Yes | User ID or login whose sessions will be revoked |
| `oauthTokens` | boolean | No | Also revoke the user OpenID Connect and OAuth refresh and access tokens \(default: false\) |
| `forgetDevices` | boolean | No | Clear the user remembered factors for all devices \(default: true\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `userId` | string | User whose sessions were revoked |
| `cleared` | boolean | Whether the sessions were revoked |
| `success` | boolean | Operation success status |
### Get Session from Okta
Retrieve an Okta session by ID, including who it belongs to, when it expires, and which authentication methods were used to establish it.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `sessionId` | string | Yes | Session ID to look up |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Session ID |
| `login` | string | Login of the session user |
| `userId` | string | ID of the session user |
| `status` | string | Session status \(ACTIVE, MFA_ENROLL, MFA_REQUIRED\) |
| `createdAt` | string | Session creation timestamp |
| `expiresAt` | string | Session expiry timestamp |
| `lastPasswordVerification` | string | Timestamp of the last password verification |
| `lastFactorVerification` | string | Timestamp of the last factor verification |
| `amr` | array | Authentication methods used to establish the session |
| `idpId` | string | Identity provider ID |
| `idpType` | string | Identity provider type |
| `success` | boolean | Operation success status |
### Revoke Session in Okta
Revoke a single Okta session by ID, ending that sign-in immediately. Destructive and irreversible: the affected user must sign in again on that device.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `sessionId` | string | Yes | Session ID to revoke |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `sessionId` | string | Revoked session ID |
| `revoked` | boolean | Whether the session was revoked |
| `success` | boolean | Operation success status |
### List Applications from Okta
List the applications configured in your Okta organization, with optional name search, filtering, and cursor pagination.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `q` | string | No | Search for applications whose name or label starts with this value |
| `filter` | string | No | Okta filter expression \(e.g., status eq "ACTIVE"\) |
| `includeNonDeleted` | boolean | No | Also return inactive applications. Deleted applications stay excluded either way \(default: false\) |
| `after` | string | No | Opaque pagination cursor returned as nextCursor by a previous call |
| `limit` | number | No | Maximum number of applications to return \(max: 200\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `apps` | array | Array of Okta applications |
| ↳ `id` | string | Application ID |
| ↳ `name` | string | Application name \(the app template key\) |
| ↳ `label` | string | Application display label |
| ↳ `status` | string | Application status \(ACTIVE, INACTIVE, DELETED\) |
| ↳ `signOnMode` | string | Sign-on mode \(SAML_2_0, OPENID_CONNECT, BOOKMARK, etc.\) |
| ↳ `features` | array | Enabled provisioning features |
| ↳ `created` | string | Creation timestamp |
| ↳ `lastUpdated` | string | Last update timestamp |
| `count` | number | Number of applications returned |
| `nextCursor` | string | Cursor for the next page, or null on the last page |
| `hasMore` | boolean | Whether more applications are available |
| `success` | boolean | Operation success status |
### Get Application from Okta
Retrieve a single Okta application by ID, including its sign-on mode, status, enabled provisioning features, and configuration objects.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `appId` | string | Yes | Application ID to look up |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Application ID |
| `name` | string | Application name \(the app template key\) |
| `label` | string | Application display label |
| `status` | string | Application status \(ACTIVE, INACTIVE, DELETED\) |
| `signOnMode` | string | Sign-on mode |
| `features` | array | Enabled provisioning features |
| `created` | string | Creation timestamp |
| `lastUpdated` | string | Last update timestamp |
| `accessibility` | json | Access settings for the app |
| ↳ `errorRedirectUrl` | string | Custom error page URL |
| ↳ `loginRedirectUrl` | string | Custom login page URL |
| ↳ `selfService` | boolean | Whether users can self-assign the app |
| `visibility` | json | Visibility settings for the app |
| ↳ `appLinks` | json | Map of app link name to whether it appears on the End-User Dashboard |
| ↳ `autoLaunch` | boolean | Signs in to the app automatically when the user signs in to Okta |
| ↳ `autoSubmitToolbar` | boolean | Signs in automatically when the user lands on the sign-in page |
| ↳ `hide` | json | Which end-user apps hide this app |
| ↳ `iOS` | boolean | Hidden in Okta Mobile |
| ↳ `web` | boolean | Hidden on the Okta End-User Dashboard |
| `settings` | json | Application settings. Okta types these per app kind, so settings.app differs between a SAML, OIDC, bookmark, or SWA app |
| `profile` | json | Application profile attributes. Okta accepts any valid JSON schema here, so the shape is whatever the org configured |
| `success` | boolean | Operation success status |
### List Application Users from Okta
List the users assigned to an Okta application, including how each assignment was made and its provisioning sync state. Use this to audit who has access to an app.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `appId` | string | Yes | Application ID to list assigned users for |
| `q` | string | No | Search assigned users whose userName, firstName, lastName, or email starts with this value |
| `after` | string | No | Opaque pagination cursor returned as nextCursor by a previous call |
| `limit` | number | No | Maximum number of assigned users to return \(default: 50, max: 500\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `appUsers` | array | Array of application user assignments |
| ↳ `id` | string | Okta user ID |
| ↳ `externalId` | string | ID of the user in the downstream application |
| ↳ `created` | string | Assignment creation timestamp |
| ↳ `lastUpdated` | string | Last update timestamp |
| ↳ `scope` | string | How the assignment was made: USER \(direct\) or GROUP \(inherited\) |
| ↳ `status` | string | Assignment status |
| ↳ `statusChanged` | string | Status change timestamp |
| ↳ `passwordChanged` | string | App password change timestamp |
| ↳ `syncState` | string | Provisioning sync state |
| ↳ `lastSync` | string | Last provisioning sync |
| ↳ `userName` | string | Username the user signs in to the application with |
| ↳ `profile` | json | App-specific profile attributes, whose shape is set by the app schema |
| `count` | number | Number of assignments returned |
| `nextCursor` | string | Cursor for the next page, or null on the last page |
| `hasMore` | boolean | Whether more assignments are available |
| `success` | boolean | Operation success status |
### Assign User to Application in Okta
Assign a user to an Okta application, granting them access to it. Applications that require credentials also need the username the user signs in with.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `appId` | string | Yes | Application ID to assign the user to |
| `userId` | string | Yes | Okta user ID to assign |
| `scope` | string | No | Assignment scope: USER for a direct assignment, or GROUP |
| `appUserName` | string | No | Username the user signs in to the application with. Required by applications that store credentials |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Okta user ID that was assigned |
| `externalId` | string | ID of the user in the downstream application |
| `created` | string | Assignment creation timestamp |
| `lastUpdated` | string | Last update timestamp |
| `scope` | string | Assignment scope \(USER or GROUP\) |
| `status` | string | Assignment status |
| `statusChanged` | string | Status change timestamp |
| `passwordChanged` | string | App password change timestamp |
| `syncState` | string | Provisioning sync state |
| `lastSync` | string | Last provisioning sync |
| `userName` | string | Username the user signs in to the application with |
| `profile` | json | App-specific profile attributes, whose shape is set by the app schema |
| `assigned` | boolean | Whether the user was assigned |
| `success` | boolean | Operation success status |
### Remove User from Application in Okta
Unassign a user from an Okta application, revoking their access. Destructive and irreversible: the app profile for that user is permanently removed, and if provisioning is enabled the downstream account is deactivated.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `appId` | string | Yes | Application ID to remove the user from |
| `userId` | string | Yes | Okta user ID to unassign |
| `sendEmail` | boolean | No | Send a deactivation email to the administrator \(default: false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `appId` | string | Application ID |
| `userId` | string | User unassigned from the application |
| `removed` | boolean | Whether the user was unassigned |
| `success` | boolean | Operation success status |
### List Application Groups from Okta
List the groups assigned to an Okta application. Every member of an assigned group inherits access to the app, so this is the starting point for an app access review.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `appId` | string | Yes | Application ID to list assigned groups for |
| `q` | string | No | Search assigned groups whose name starts with this value |
| `after` | string | No | Opaque pagination cursor returned as nextCursor by a previous call |
| `limit` | number | No | Maximum number of assigned groups to return \(default: 20, range: 20 to 200\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `appGroups` | array | Array of application group assignments |
| ↳ `id` | string | Assigned group ID |
| ↳ `priority` | number | Assignment priority, which resolves conflicting profile mappings |
| ↳ `lastUpdated` | string | Last update timestamp |
| ↳ `profile` | json | App-specific profile attributes, whose shape is set by the app schema |
| `count` | number | Number of assignments returned |
| `nextCursor` | string | Cursor for the next page, or null on the last page |
| `hasMore` | boolean | Whether more assignments are available |
| `success` | boolean | Operation success status |
### Assign Group to Application in Okta
Assign a group to an Okta application so every member of the group inherits access to it.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `appId` | string | Yes | Application ID to assign the group to |
| `groupId` | string | Yes | Group ID to assign |
| `priority` | number | No | Assignment priority, which resolves conflicting profile mappings when a user belongs to several assigned groups |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Assigned group ID |
| `priority` | number | Assignment priority |
| `lastUpdated` | string | Last update timestamp |
| `profile` | json | App-specific profile attributes, whose shape is set by the app schema |
| `assigned` | boolean | Whether the group was assigned |
| `success` | boolean | Operation success status |
### Remove Group from Application in Okta
Unassign a group from an Okta application. Destructive: every member who had access only through this group loses access to the app.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `appId` | string | Yes | Application ID to remove the group from |
| `groupId` | string | Yes | Group ID to unassign |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `appId` | string | Application ID |
| `groupId` | string | Group unassigned from the application |
| `removed` | boolean | Whether the group was unassigned |
| `success` | boolean | Operation success status |
### List User Roles from Okta
List the administrator roles assigned to a user. Returns both standard roles and custom role bindings, so you can review who holds privileged access.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `userId` | string | Yes | User ID or login to list admin roles for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `roles` | array | Array of admin role assignments |
| ↳ `id` | string | Role assignment ID, which is the resource set binding ID for a custom role. Pass this to Remove User Role |
| ↳ `label` | string | Role label |
| ↳ `type` | string | Role type \(SUPER_ADMIN, ORG_ADMIN, APP_ADMIN, USER_ADMIN, HELP_DESK_ADMIN, READ_ONLY_ADMIN, CUSTOM, etc.\) |
| ↳ `status` | string | Role status \(ACTIVE, INACTIVE\) |
| ↳ `created` | string | Assignment timestamp |
| ↳ `lastUpdated` | string | Last update timestamp |
| ↳ `assignmentType` | string | How the role was assigned \(USER, GROUP, CLIENT\) |
| ↳ `role` | string | Custom role ID, present only on custom role assignments |
| ↳ `resourceSet` | string | Resource set ID, present only on custom role assignments |
| `count` | number | Number of role assignments returned |
| `success` | boolean | Operation success status |
### Assign User Role in Okta
Grant a user an administrator role. Use a standard role type such as USER_ADMIN or HELP_DESK_ADMIN, or CUSTOM together with a custom role ID and a resource set ID. This grants privileged access, so confirm the role is the least privilege that fits.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `userId` | string | Yes | User ID or login to assign the admin role to |
| `roleType` | string | Yes | Role type to assign: SUPER_ADMIN, ORG_ADMIN, APP_ADMIN, USER_ADMIN, HELP_DESK_ADMIN, READ_ONLY_ADMIN, API_ACCESS_MANAGEMENT_ADMIN, GROUP_MEMBERSHIP_ADMIN, REPORT_ADMIN, WORKFLOWS_ADMIN, ACCESS_CERTIFICATIONS_ADMIN, ACCESS_REQUESTS_ADMIN, or CUSTOM |
| `customRoleId` | string | No | Custom role ID. Required when the role type is CUSTOM |
| `resourceSetId` | string | No | Resource set ID the custom role applies to. Required when the role type is CUSTOM |
| `disableNotifications` | boolean | No | Grant the user third-party admin status, which suppresses Okta admin notifications \(default: false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Role assignment ID, which is what Remove User Role takes |
| `label` | string | Role label |
| `type` | string | Assigned role type |
| `status` | string | Role status |
| `created` | string | Assignment timestamp |
| `lastUpdated` | string | Last update timestamp |
| `assignmentType` | string | How the role was assigned \(USER, GROUP, CLIENT\) |
| `role` | string | Custom role ID, for custom roles |
| `resourceSet` | string | Resource set ID, for custom roles |
| `assigned` | boolean | Whether the role was assigned |
| `success` | boolean | Operation success status |
### Remove User Role in Okta
Revoke an administrator role from a user. Destructive: the user immediately loses the admin permissions that role granted. Takes the role assignment ID, not the role type, which List User Roles returns as the role id field.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `userId` | string | Yes | User ID or login to revoke the admin role from |
| `roleAssignmentId` | string | Yes | Role assignment ID to revoke, as returned by List User Roles. For a custom role this is the resource set binding ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `userId` | string | User the role was revoked from |
| `roleAssignmentId` | string | Revoked role assignment ID |
| `removed` | boolean | Whether the role was revoked |
| `success` | boolean | Operation success status |
### Get System Log Events from Okta
Query the Okta System Log for sign-ins, admin changes, and security events. Supports a time window, SCIM filter expressions, keyword search, and cursor pagination for audit and investigation workflows.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Okta API token for authentication |
| `domain` | string | Yes | Okta domain \(e.g., dev-123456.okta.com\) |
| `since` | string | No | Start of the query time window as an ISO 8601 timestamp \(default: 7 days before "until"\) |
| `until` | string | No | End of the query time window as an ISO 8601 timestamp \(default: now\) |
| `filter` | string | No | SCIM filter expression \(e.g., eventType eq "user.session.start" or outcome.result eq "FAILURE"\) |
| `q` | string | No | Keyword search across the event payload \(max 40 characters per keyword, max 10 keywords\) |
| `sortOrder` | string | No | Sort order: ASCENDING \(default\) or DESCENDING |
| `after` | string | No | Opaque pagination cursor returned as nextCursor by a previous call |
| `limit` | number | No | Maximum number of events to return \(default: 100, max: 1000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `events` | array | Array of System Log events |
| ↳ `uuid` | string | Unique event ID |
| ↳ `published` | string | Event timestamp |
| ↳ `eventType` | string | Event type \(e.g., user.session.start, user.account.update_password\) |
| ↳ `severity` | string | Event severity \(DEBUG, ERROR, INFO, WARN\) |
| ↳ `legacyEventType` | string | Legacy event type |
| ↳ `displayMessage` | string | Human-readable event description |
| ↳ `outcomeResult` | string | Event outcome \(SUCCESS, FAILURE, CHALLENGE, DENY, etc.\) |
| ↳ `outcomeReason` | string | Reason for the outcome |
| ↳ `actorId` | string | ID of the actor |
| ↳ `actorType` | string | Actor type \(User, Client, etc.\) |
| ↳ `actorAlternateId` | string | Actor alternate ID, usually the login |
| ↳ `actorDisplayName` | string | Actor display name |
| ↳ `clientIpAddress` | string | Client IP address |
| ↳ `clientDevice` | string | Client device category \(e.g., Computer\) |
| ↳ `clientZone` | string | Network zone |
| ↳ `clientBrowser` | string | Client browser |
| ↳ `clientOs` | string | Client operating system |
| ↳ `clientCity` | string | Client city |
| ↳ `clientState` | string | Client state or region |
| ↳ `clientCountry` | string | Client country |
| ↳ `authenticationProvider` | string | Authentication provider used |
| ↳ `credentialType` | string | Credential type used |
| ↳ `externalSessionId` | string | External session ID for correlating events |
| ↳ `securityAsOrg` | string | Autonomous system organization |
| ↳ `securityIsp` | string | Internet service provider |
| ↳ `securityIsProxy` | boolean | Whether the request came through a proxy |
| ↳ `transactionId` | string | Transaction ID |
| ↳ `transactionType` | string | Transaction type \(e.g., WEB, JOB\) |
| ↳ `targets` | array | Entities the event acted upon |
| ↳ `id` | string | Target ID |
| ↳ `type` | string | Target type |
| ↳ `alternateId` | string | Target alternate ID |
| ↳ `displayName` | string | Target display name |
| ↳ `debugData` | json | Extra context whose keys depend on the event type. Okta states these keys and values can change between releases, so treat them as a debugging aid rather than a contract |
| `count` | number | Number of events returned |
| `nextCursor` | string | Cursor for the next page, or null on the last page |
| `hasMore` | boolean | Whether more events are available |
| `success` | boolean | Operation success status |
+80
View File
@@ -0,0 +1,80 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { OktaBlock } from '@/blocks/blocks/okta'
/**
* The generic block handler runs `{ ...inputs, ...transformedParams }`, so the
* transform can only drop a value by assigning `undefined` to its key. Omitting
* the key leaves the raw subBlock string in place. These cases pin that
* behavior by asserting on the merge, not on the transform alone.
*/
function merge(inputs: Record<string, unknown>): Record<string, unknown> {
const transform = OktaBlock.tools.config?.params
if (!transform) throw new Error('Okta block has no params transform')
return { ...inputs, ...transform(inputs) }
}
const BASE = { operation: 'okta_list_users', apiKey: 'token', domain: 'dev-1.okta.com' }
describe('Okta block params transform', () => {
it('coerces a numeric limit', () => {
expect(merge({ ...BASE, limit: '25' }).limit).toBe(25)
})
it('drops a non-numeric limit rather than forwarding the raw entry', () => {
expect(merge({ ...BASE, limit: 'twenty' }).limit).toBeUndefined()
})
it('drops a non-numeric priority rather than forwarding the raw entry', () => {
const merged = merge({
...BASE,
operation: 'okta_assign_group_to_app',
priority: 'high',
})
expect(merged.priority).toBeUndefined()
})
it('drops a blank profile field so a partial update leaves Okta untouched', () => {
const merged = merge({
...BASE,
operation: 'okta_update_user',
userId: '00u1',
firstName: 'Ada',
lastName: '',
})
expect(merged.firstName).toBe('Ada')
expect(merged.lastName).toBeUndefined()
})
it('maps the group name and description onto the tool param names', () => {
const merged = merge({
...BASE,
operation: 'okta_create_group',
groupName: 'Engineering',
groupDescription: 'Eng team',
})
expect(merged.name).toBe('Engineering')
expect(merged.description).toBe('Eng team')
})
it('keeps a false toggle, which is a real choice rather than a blank field', () => {
const merged = merge({
...BASE,
operation: 'okta_activate_user',
userId: '00u1',
sendEmail: false,
})
expect(merged.sendEmail).toBe(false)
})
})
describe('Okta block outputs', () => {
it('declares only fields a tool emits at the top level', () => {
const declared = Object.keys(OktaBlock.outputs)
expect(declared).not.toContain('targets')
expect(declared).not.toContain('debugData')
expect(declared).toContain('events')
})
})
+786 -22
View File
@@ -1,16 +1,36 @@
import { OktaIcon } from '@/components/icons'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { IntegrationType } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { OktaResponse } from '@/tools/okta/types'
/**
* Coerces a numeric subBlock value, dropping anything that is not a real number.
*
* These fields are free-text inputs, so a stray non-numeric entry would otherwise
* become `NaN` and serialize to `null`, which Okta rejects with a validation
* error that points at the wrong thing. Dropping the field instead lets Okta
* apply its own default.
*/
function toFiniteNumber(value: unknown): number | undefined {
if (value === undefined || value === null || value === '') return undefined
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : undefined
}
/** Treats a blank subBlock value as absent. */
function blankToUndefined(value: unknown): unknown {
return value === null || value === '' ? undefined : value
}
export const OktaBlock: BlockConfig<OktaResponse> = {
type: 'okta',
name: 'Okta',
description: 'Manage users and groups in Okta',
description: 'Manage users, groups, apps, and MFA in Okta',
longDescription:
'Integrate Okta identity management into your workflow. List, create, update, activate, suspend, and delete users. Reset passwords. Manage groups and group membership.',
'Integrate Okta identity management into your workflow. Manage users, groups, and group rules. Run service desk actions like resetting MFA factors and clearing sessions. Review and change application assignments and admin roles. Query the System Log to audit sign-ins and admin changes.',
docsLink: 'https://docs.sim.ai/integrations/okta',
category: 'tools',
authMode: AuthMode.ApiKey,
integrationType: IntegrationType.Security,
bgColor: '#191919',
iconColor: '#007DC1',
@@ -66,6 +86,92 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
{ text: 'List the members of group', field: 'groupId', core: true },
{ text: ', up to', field: 'limit' },
],
okta_get_logs: [
'Read System Log events',
{ text: ', matching', field: 'q' },
{ text: ', filtered by', field: 'filter' },
{ text: ', since', field: 'since' },
{ text: ', until', field: 'until' },
],
okta_clear_user_sessions: [
{ text: 'Clear every session of user', field: 'userId', core: true },
],
okta_get_session: [{ text: 'Read session', field: 'sessionId', core: true }],
okta_revoke_session: [{ text: 'Revoke session', field: 'sessionId', core: true }],
okta_list_factors: [{ text: 'List the MFA factors of user', field: 'userId', core: true }],
okta_get_factor: [
{ text: 'Read factor', field: 'factorId', core: true },
{ text: 'of user', field: 'userId' },
],
okta_enroll_factor: [
{ text: 'Enroll factor', field: 'factorType', core: true },
{ text: 'for user', field: 'userId' },
],
okta_reset_factor: [
{ text: 'Reset factor', field: 'factorId', core: true },
{ text: 'for user', field: 'userId' },
],
okta_reset_all_factors: [
{ text: 'Reset every MFA factor of user', field: 'userId', core: true },
],
okta_list_apps: [
'List applications',
{ text: ', matching', field: 'q' },
{ text: ', filtered by', field: 'filter' },
{ text: ', up to', field: 'limit' },
],
okta_get_app: [{ text: 'Read application', field: 'appId', core: true }],
okta_list_app_users: [
{ text: 'List the users assigned to application', field: 'appId', core: true },
{ text: ', matching', field: 'q' },
],
okta_assign_user_to_app: [
{ text: 'Assign user', field: 'userId', core: true },
{ text: 'to application', field: 'appId' },
],
okta_remove_user_from_app: [
{ text: 'Remove user', field: 'userId', core: true },
{ text: 'from application', field: 'appId', core: true },
],
okta_list_app_groups: [
{ text: 'List the groups assigned to application', field: 'appId', core: true },
],
okta_assign_group_to_app: [
{ text: 'Assign group', field: 'groupId', core: true },
{ text: 'to application', field: 'appId' },
],
okta_remove_group_from_app: [
{ text: 'Remove group', field: 'groupId', core: true },
{ text: 'from application', field: 'appId' },
],
okta_list_user_roles: [
{ text: 'List the admin roles of user', field: 'userId', core: true },
],
okta_assign_user_role: [
{ text: 'Assign admin role', field: 'roleType', core: true },
{ text: 'to user', field: 'userId' },
],
okta_remove_user_role: [
{ text: 'Remove admin role assignment', field: 'roleAssignmentId', core: true },
{ text: 'from user', field: 'userId' },
],
okta_list_group_rules: [
'List group rules',
{ text: ', matching', field: 'search' },
{ text: ', up to', field: 'limit' },
],
okta_get_group_rule: [{ text: 'Read group rule', field: 'groupRuleId', core: true }],
okta_create_group_rule: [
{ text: 'Create group rule', field: 'ruleName', core: true },
{ text: ', matching users where', field: 'expression' },
],
okta_activate_group_rule: [
{ text: 'Activate group rule', field: 'groupRuleId', core: true },
],
okta_deactivate_group_rule: [
{ text: 'Deactivate group rule', field: 'groupRuleId', core: true },
],
okta_delete_group_rule: [{ text: 'Delete group rule', field: 'groupRuleId', core: true }],
},
},
},
@@ -94,6 +200,32 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
{ label: 'Add User to Group', id: 'okta_add_user_to_group' },
{ label: 'Remove User from Group', id: 'okta_remove_user_from_group' },
{ label: 'List Group Members', id: 'okta_list_group_members' },
{ label: 'List Group Rules', id: 'okta_list_group_rules' },
{ label: 'Get Group Rule', id: 'okta_get_group_rule' },
{ label: 'Create Group Rule', id: 'okta_create_group_rule' },
{ label: 'Activate Group Rule', id: 'okta_activate_group_rule' },
{ label: 'Deactivate Group Rule', id: 'okta_deactivate_group_rule' },
{ label: 'Delete Group Rule', id: 'okta_delete_group_rule' },
{ label: 'List Factors', id: 'okta_list_factors' },
{ label: 'Get Factor', id: 'okta_get_factor' },
{ label: 'Enroll Factor', id: 'okta_enroll_factor' },
{ label: 'Reset Factor', id: 'okta_reset_factor' },
{ label: 'Reset All Factors', id: 'okta_reset_all_factors' },
{ label: 'Clear User Sessions', id: 'okta_clear_user_sessions' },
{ label: 'Get Session', id: 'okta_get_session' },
{ label: 'Revoke Session', id: 'okta_revoke_session' },
{ label: 'List Applications', id: 'okta_list_apps' },
{ label: 'Get Application', id: 'okta_get_app' },
{ label: 'List Application Users', id: 'okta_list_app_users' },
{ label: 'Assign User to Application', id: 'okta_assign_user_to_app' },
{ label: 'Remove User from Application', id: 'okta_remove_user_from_app' },
{ label: 'List Application Groups', id: 'okta_list_app_groups' },
{ label: 'Assign Group to Application', id: 'okta_assign_group_to_app' },
{ label: 'Remove Group from Application', id: 'okta_remove_group_from_app' },
{ label: 'List User Roles', id: 'okta_list_user_roles' },
{ label: 'Assign User Role', id: 'okta_assign_user_role' },
{ label: 'Remove User Role', id: 'okta_remove_user_role' },
{ label: 'Get System Log Events', id: 'okta_get_logs' },
],
value: () => 'okta_list_users',
},
@@ -118,15 +250,43 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
title: 'Search',
type: 'short-input',
placeholder: 'profile.firstName eq "John"',
condition: { field: 'operation', value: ['okta_list_users', 'okta_list_groups'] },
condition: {
field: 'operation',
value: ['okta_list_users', 'okta_list_groups', 'okta_list_group_rules'],
},
wandConfig: {
enabled: true,
placeholder: 'Describe who or what to search for',
prompt:
'Generate an Okta search expression. The grammar is SCIM-style: a property, an operator (eq, sw, co, gt, ge, lt, le), and a quoted value, combined with and/or and parentheses. User properties are prefixed with profile (profile.firstName, profile.email, profile.department) plus the top-level id, status, created, activated, statusChanged and lastUpdated. Group properties are type plus profile.name and profile.description. Example: profile.department eq "Engineering" and status eq "ACTIVE". Return ONLY the expression - no explanations, no extra text.',
},
},
{
id: 'filter',
title: 'Filter',
type: 'short-input',
placeholder: 'status eq "ACTIVE"',
condition: { field: 'operation', value: ['okta_list_users', 'okta_list_groups'] },
condition: {
field: 'operation',
value: ['okta_list_users', 'okta_list_groups', 'okta_list_apps', 'okta_get_logs'],
},
mode: 'advanced',
wandConfig: {
enabled: true,
placeholder: 'Describe how to narrow the results',
prompt:
'Generate an Okta filter expression. The grammar is SCIM-style: a property, an operator (eq, and for lastUpdated also gt, ge, lt, le), and a quoted value, combined with and/or. Each listing supports a limited property set. Users: status, lastUpdated, id, profile.login, profile.email, profile.firstName, profile.lastName. Groups: id, type, lastUpdated, lastMembershipUpdated. Applications: id, status, name. System Log: any event property, such as eventType, outcome.result, actor.alternateId or client.ipAddress. Example: eventType eq "user.session.start" and outcome.result eq "FAILURE". Return ONLY the expression - no explanations, no extra text.',
},
},
{
id: 'q',
title: 'Query',
type: 'short-input',
placeholder: 'Keyword to search for',
condition: {
field: 'operation',
value: ['okta_get_logs', 'okta_list_apps', 'okta_list_app_users', 'okta_list_app_groups'],
},
},
// User ID (shared across user operations that need it)
{
@@ -147,6 +307,17 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
'okta_delete_user',
'okta_add_user_to_group',
'okta_remove_user_from_group',
'okta_clear_user_sessions',
'okta_list_factors',
'okta_get_factor',
'okta_enroll_factor',
'okta_reset_factor',
'okta_reset_all_factors',
'okta_assign_user_to_app',
'okta_remove_user_from_app',
'okta_list_user_roles',
'okta_assign_user_role',
'okta_remove_user_role',
],
},
required: {
@@ -162,6 +333,17 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
'okta_delete_user',
'okta_add_user_to_group',
'okta_remove_user_from_group',
'okta_clear_user_sessions',
'okta_list_factors',
'okta_get_factor',
'okta_enroll_factor',
'okta_reset_factor',
'okta_reset_all_factors',
'okta_assign_user_to_app',
'okta_remove_user_from_app',
'okta_list_user_roles',
'okta_assign_user_role',
'okta_remove_user_role',
],
},
},
@@ -180,6 +362,8 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
'okta_add_user_to_group',
'okta_remove_user_from_group',
'okta_list_group_members',
'okta_assign_group_to_app',
'okta_remove_group_from_app',
],
},
required: {
@@ -191,6 +375,8 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
'okta_add_user_to_group',
'okta_remove_user_from_group',
'okta_list_group_members',
'okta_assign_group_to_app',
'okta_remove_group_from_app',
],
},
},
@@ -264,7 +450,7 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
id: 'activate',
title: 'Activate Immediately',
type: 'switch',
condition: { field: 'operation', value: 'okta_create_user' },
condition: { field: 'operation', value: ['okta_create_user', 'okta_enroll_factor'] },
mode: 'advanced',
},
// Group name (for create/update group)
@@ -295,10 +481,360 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
'okta_deactivate_user',
'okta_reset_password',
'okta_delete_user',
'okta_remove_user_from_app',
],
},
mode: 'advanced',
},
// Group rule params
{
id: 'groupRuleId',
title: 'Group Rule ID',
type: 'short-input',
placeholder: 'Okta group rule ID',
condition: {
field: 'operation',
value: [
'okta_get_group_rule',
'okta_activate_group_rule',
'okta_deactivate_group_rule',
'okta_delete_group_rule',
],
},
required: {
field: 'operation',
value: [
'okta_get_group_rule',
'okta_activate_group_rule',
'okta_deactivate_group_rule',
'okta_delete_group_rule',
],
},
},
{
id: 'ruleName',
title: 'Rule Name',
type: 'short-input',
placeholder: 'Engineers',
condition: { field: 'operation', value: 'okta_create_group_rule' },
required: { field: 'operation', value: 'okta_create_group_rule' },
},
{
id: 'expression',
title: 'Expression',
type: 'short-input',
placeholder: 'user.department=="Engineering"',
condition: { field: 'operation', value: 'okta_create_group_rule' },
required: { field: 'operation', value: 'okta_create_group_rule' },
wandConfig: {
enabled: true,
placeholder: 'Describe which users the rule should match',
prompt:
'Generate an Okta Expression Language predicate that evaluates to a boolean over a user profile. Reference profile attributes as user.<attribute>, combine them with && and ||, and compare with == or !=. Example: user.department=="Engineering" && user.countryCode=="US". Return ONLY the expression - no explanations, no extra text.',
},
},
{
id: 'assignUserToGroupIds',
title: 'Assign to Group IDs',
type: 'short-input',
placeholder: 'Comma-separated group IDs',
condition: { field: 'operation', value: 'okta_create_group_rule' },
required: { field: 'operation', value: 'okta_create_group_rule' },
},
{
id: 'excludedUserIds',
title: 'Excluded User IDs',
type: 'short-input',
placeholder: 'Comma-separated user IDs to exclude',
condition: { field: 'operation', value: 'okta_create_group_rule' },
mode: 'advanced',
},
{
id: 'removeUsers',
title: 'Remove Assigned Users',
type: 'switch',
condition: { field: 'operation', value: 'okta_delete_group_rule' },
mode: 'advanced',
},
// Factor params
{
id: 'factorId',
title: 'Factor ID',
type: 'short-input',
placeholder: 'Okta factor ID',
condition: { field: 'operation', value: ['okta_get_factor', 'okta_reset_factor'] },
required: { field: 'operation', value: ['okta_get_factor', 'okta_reset_factor'] },
},
{
id: 'factorType',
title: 'Factor Type',
type: 'dropdown',
options: [
{ label: 'SMS', id: 'sms' },
{ label: 'Voice Call', id: 'call' },
{ label: 'Email', id: 'email' },
{ label: 'Security Question', id: 'question' },
{ label: 'Okta Verify Push', id: 'push' },
{ label: 'Software TOTP', id: 'token:software:totp' },
{ label: 'U2F', id: 'u2f' },
{ label: 'WebAuthn', id: 'webauthn' },
],
condition: { field: 'operation', value: 'okta_enroll_factor' },
required: { field: 'operation', value: 'okta_enroll_factor' },
},
{
id: 'provider',
title: 'Factor Provider',
type: 'dropdown',
options: [
{ label: 'Okta', id: 'OKTA' },
{ label: 'Google', id: 'GOOGLE' },
{ label: 'FIDO', id: 'FIDO' },
{ label: 'Duo', id: 'DUO' },
{ label: 'RSA', id: 'RSA' },
{ label: 'Symantec', id: 'SYMANTEC' },
{ label: 'Yubico', id: 'YUBICO' },
{ label: 'Custom', id: 'CUSTOM' },
],
condition: { field: 'operation', value: 'okta_enroll_factor' },
required: { field: 'operation', value: 'okta_enroll_factor' },
},
{
id: 'phoneNumber',
title: 'Phone Number',
type: 'short-input',
placeholder: '+14155550123 (required for SMS and voice call)',
condition: { field: 'operation', value: 'okta_enroll_factor' },
},
{
id: 'factorEmail',
title: 'Factor Email',
type: 'short-input',
placeholder: 'user@example.com (required for the email factor)',
condition: { field: 'operation', value: 'okta_enroll_factor' },
mode: 'advanced',
},
{
id: 'securityQuestion',
title: 'Security Question',
type: 'short-input',
placeholder: 'disliked_food (required for the question factor)',
condition: { field: 'operation', value: 'okta_enroll_factor' },
mode: 'advanced',
},
{
id: 'securityAnswer',
title: 'Security Answer',
type: 'short-input',
password: true,
placeholder: 'Answer, minimum 4 characters',
condition: { field: 'operation', value: 'okta_enroll_factor' },
mode: 'advanced',
},
{
id: 'removeRecoveryEnrollment',
title: 'Remove Recovery Enrollment',
type: 'switch',
condition: { field: 'operation', value: 'okta_reset_factor' },
mode: 'advanced',
},
// Session params
{
id: 'sessionId',
title: 'Session ID',
type: 'short-input',
placeholder: 'Okta session ID',
condition: { field: 'operation', value: ['okta_get_session', 'okta_revoke_session'] },
required: { field: 'operation', value: ['okta_get_session', 'okta_revoke_session'] },
},
{
id: 'oauthTokens',
title: 'Revoke OAuth Tokens',
type: 'switch',
condition: { field: 'operation', value: 'okta_clear_user_sessions' },
mode: 'advanced',
},
{
id: 'forgetDevices',
title: 'Forget Devices',
type: 'switch',
condition: { field: 'operation', value: 'okta_clear_user_sessions' },
mode: 'advanced',
},
// Application params
{
id: 'appId',
title: 'Application ID',
type: 'short-input',
placeholder: 'Okta application ID',
condition: {
field: 'operation',
value: [
'okta_get_app',
'okta_list_app_users',
'okta_assign_user_to_app',
'okta_remove_user_from_app',
'okta_list_app_groups',
'okta_assign_group_to_app',
'okta_remove_group_from_app',
],
},
required: {
field: 'operation',
value: [
'okta_get_app',
'okta_list_app_users',
'okta_assign_user_to_app',
'okta_remove_user_from_app',
'okta_list_app_groups',
'okta_assign_group_to_app',
'okta_remove_group_from_app',
],
},
},
{
id: 'appUserName',
title: 'Application Username',
type: 'short-input',
placeholder: 'Username the user signs in to the app with',
condition: { field: 'operation', value: 'okta_assign_user_to_app' },
},
{
id: 'scope',
title: 'Assignment Scope',
type: 'dropdown',
options: [
{ label: 'User', id: 'USER' },
{ label: 'Group', id: 'GROUP' },
],
condition: { field: 'operation', value: 'okta_assign_user_to_app' },
mode: 'advanced',
},
{
id: 'priority',
title: 'Priority',
type: 'short-input',
placeholder: 'Assignment priority',
condition: { field: 'operation', value: 'okta_assign_group_to_app' },
mode: 'advanced',
},
{
id: 'includeNonDeleted',
title: 'Include Non-Deleted',
type: 'switch',
condition: { field: 'operation', value: 'okta_list_apps' },
mode: 'advanced',
},
// Admin role params
{
id: 'roleType',
title: 'Role Type',
type: 'dropdown',
options: [
{ label: 'Super Admin', id: 'SUPER_ADMIN' },
{ label: 'Org Admin', id: 'ORG_ADMIN' },
{ label: 'App Admin', id: 'APP_ADMIN' },
{ label: 'User Admin', id: 'USER_ADMIN' },
{ label: 'Help Desk Admin', id: 'HELP_DESK_ADMIN' },
{ label: 'Read Only Admin', id: 'READ_ONLY_ADMIN' },
{ label: 'API Access Management Admin', id: 'API_ACCESS_MANAGEMENT_ADMIN' },
{ label: 'Group Membership Admin', id: 'GROUP_MEMBERSHIP_ADMIN' },
{ label: 'Report Admin', id: 'REPORT_ADMIN' },
{ label: 'Workflows Admin', id: 'WORKFLOWS_ADMIN' },
{ label: 'Access Certifications Admin', id: 'ACCESS_CERTIFICATIONS_ADMIN' },
{ label: 'Access Requests Admin', id: 'ACCESS_REQUESTS_ADMIN' },
{ label: 'Custom', id: 'CUSTOM' },
],
condition: { field: 'operation', value: 'okta_assign_user_role' },
required: { field: 'operation', value: 'okta_assign_user_role' },
},
{
id: 'customRoleId',
title: 'Custom Role ID',
type: 'short-input',
placeholder: 'ID of the custom role to grant',
condition: {
field: 'operation',
value: 'okta_assign_user_role',
and: { field: 'roleType', value: 'CUSTOM' },
},
required: {
field: 'operation',
value: 'okta_assign_user_role',
and: { field: 'roleType', value: 'CUSTOM' },
},
},
{
id: 'resourceSetId',
title: 'Resource Set ID',
type: 'short-input',
placeholder: 'Resource set the custom role applies to',
condition: {
field: 'operation',
value: 'okta_assign_user_role',
and: { field: 'roleType', value: 'CUSTOM' },
},
required: {
field: 'operation',
value: 'okta_assign_user_role',
and: { field: 'roleType', value: 'CUSTOM' },
},
},
{
id: 'disableNotifications',
title: 'Third-Party Admin',
type: 'switch',
condition: { field: 'operation', value: 'okta_assign_user_role' },
mode: 'advanced',
},
{
id: 'roleAssignmentId',
title: 'Role Assignment ID',
type: 'short-input',
placeholder: 'Role assignment ID from List User Roles',
condition: { field: 'operation', value: 'okta_remove_user_role' },
required: { field: 'operation', value: 'okta_remove_user_role' },
},
// System Log params
{
id: 'since',
title: 'Since',
type: 'short-input',
placeholder: '2026-08-01T00:00:00.000Z',
condition: { field: 'operation', value: 'okta_get_logs' },
wandConfig: {
enabled: true,
placeholder: 'Describe the start of the time window',
prompt:
'Generate an ISO 8601 UTC timestamp for the start of a System Log query window, for example 2026-08-01T00:00:00.000Z. Return ONLY the timestamp - no explanations, no extra text.',
generationType: 'timestamp',
},
},
{
id: 'until',
title: 'Until',
type: 'short-input',
placeholder: '2026-08-15T00:00:00.000Z',
condition: { field: 'operation', value: 'okta_get_logs' },
wandConfig: {
enabled: true,
placeholder: 'Describe the end of the time window',
prompt:
'Generate an ISO 8601 UTC timestamp for the end of a System Log query window, for example 2026-08-15T00:00:00.000Z. Return ONLY the timestamp - no explanations, no extra text.',
generationType: 'timestamp',
},
},
{
id: 'sortOrder',
title: 'Sort Order',
type: 'dropdown',
options: [
{ label: 'Ascending', id: 'ASCENDING' },
{ label: 'Descending', id: 'DESCENDING' },
],
condition: { field: 'operation', value: 'okta_get_logs' },
mode: 'advanced',
},
// Pagination
{
id: 'limit',
@@ -307,7 +843,36 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
placeholder: 'Max results to return',
condition: {
field: 'operation',
value: ['okta_list_users', 'okta_list_groups', 'okta_list_group_members'],
value: [
'okta_list_users',
'okta_list_groups',
'okta_list_group_members',
'okta_get_logs',
'okta_list_apps',
'okta_list_app_users',
'okta_list_app_groups',
'okta_list_group_rules',
],
},
mode: 'advanced',
},
{
id: 'after',
title: 'Cursor',
type: 'short-input',
placeholder: 'nextCursor from a previous run',
condition: {
field: 'operation',
value: [
'okta_list_users',
'okta_list_groups',
'okta_list_group_members',
'okta_get_logs',
'okta_list_apps',
'okta_list_app_users',
'okta_list_app_groups',
'okta_list_group_rules',
],
},
mode: 'advanced',
},
@@ -333,38 +898,69 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
'okta_add_user_to_group',
'okta_remove_user_from_group',
'okta_list_group_members',
'okta_list_group_rules',
'okta_get_group_rule',
'okta_create_group_rule',
'okta_activate_group_rule',
'okta_deactivate_group_rule',
'okta_delete_group_rule',
'okta_list_factors',
'okta_get_factor',
'okta_enroll_factor',
'okta_reset_factor',
'okta_reset_all_factors',
'okta_clear_user_sessions',
'okta_get_session',
'okta_revoke_session',
'okta_list_apps',
'okta_get_app',
'okta_list_app_users',
'okta_assign_user_to_app',
'okta_remove_user_from_app',
'okta_list_app_groups',
'okta_assign_group_to_app',
'okta_remove_group_from_app',
'okta_list_user_roles',
'okta_assign_user_role',
'okta_remove_user_role',
'okta_get_logs',
],
config: {
tool: (params) => params.operation as string,
/**
* Every param this block can send is assigned here, including the ones it
* decides to drop.
*
* The executor merges the result over the raw serialized inputs
* (`{ ...inputs, ...transformedParams }`), so a key this function *omits*
* keeps the raw subBlock string instead of being dropped. Assigning
* `undefined` is what actually removes it: otherwise a non-numeric `limit`
* would still reach Okta verbatim, and a blank field in a partial update
* (`update_user` is a POST merge) would overwrite the stored Okta value
* with an empty string rather than leaving it untouched.
*/
params: (params) => {
const result: Record<string, unknown> = {
apiKey: params.apiKey,
domain: params.domain,
limit: toFiniteNumber(params.limit),
priority: toFiniteNumber(params.priority),
// Group-specific UI fields carry the tool's generic param names.
name: blankToUndefined(params.groupName),
description: blankToUndefined(params.groupDescription),
}
if (params.limit) result.limit = Number(params.limit)
// Map group-specific UI fields to tool param names
if (params.groupName) result.name = params.groupName
if (params.groupDescription !== undefined) result.description = params.groupDescription
// Pass through all other params, skipping empty values. Blank fields in a
// partial update (e.g. update_user, a POST merge) must be omitted so they
// leave the existing Okta value unchanged rather than overwriting it with
// an empty string. This mirrors the agent tool-call path, which already
// filters empty params before execution.
const skipKeys = new Set([
const mappedKeys = new Set([
'operation',
'apiKey',
'domain',
'limit',
'priority',
'groupName',
'groupDescription',
])
for (const [key, value] of Object.entries(params)) {
if (!skipKeys.has(key) && value !== undefined && value !== null && value !== '') {
result[key] = value
}
if (!mappedKeys.has(key)) result[key] = blankToUndefined(value)
}
return result
@@ -393,6 +989,53 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
groupName: { type: 'string', description: 'Group name' },
groupDescription: { type: 'string', description: 'Group description' },
sendEmail: { type: 'boolean', description: 'Whether to send email notification' },
q: { type: 'string', description: 'Keyword search query' },
after: { type: 'string', description: 'Cursor for the next page of results' },
since: { type: 'string', description: 'Start of the System Log time window' },
until: { type: 'string', description: 'End of the System Log time window' },
sortOrder: { type: 'string', description: 'System Log sort order' },
sessionId: { type: 'string', description: 'Session ID' },
oauthTokens: { type: 'boolean', description: 'Also revoke OAuth and OIDC tokens' },
forgetDevices: { type: 'boolean', description: 'Clear remembered factors on all devices' },
factorId: { type: 'string', description: 'MFA factor ID' },
factorType: { type: 'string', description: 'MFA factor type to enroll' },
provider: { type: 'string', description: 'MFA factor provider' },
phoneNumber: { type: 'string', description: 'Phone number for the sms or call factor' },
factorEmail: { type: 'string', description: 'Email address for the email factor' },
securityQuestion: { type: 'string', description: 'Security question key' },
securityAnswer: { type: 'string', description: 'Security question answer' },
removeRecoveryEnrollment: {
type: 'boolean',
description: 'Also remove the phone number as a recovery method',
},
appId: { type: 'string', description: 'Application ID' },
appUserName: { type: 'string', description: 'Username the user signs in to the app with' },
scope: { type: 'string', description: 'Application assignment scope' },
priority: { type: 'number', description: 'Application group assignment priority' },
includeNonDeleted: {
type: 'boolean',
description: 'Include applications that are not deleted',
},
roleType: { type: 'string', description: 'Admin role type to assign' },
customRoleId: { type: 'string', description: 'Custom role ID' },
resourceSetId: { type: 'string', description: 'Resource set ID for a custom role' },
disableNotifications: { type: 'boolean', description: 'Grant third-party admin status' },
roleAssignmentId: { type: 'string', description: 'Role assignment ID to revoke' },
groupRuleId: { type: 'string', description: 'Group rule ID' },
ruleName: { type: 'string', description: 'Group rule name' },
expression: { type: 'string', description: 'Okta expression driving the group rule' },
assignUserToGroupIds: {
type: 'string',
description: 'Comma-separated group IDs the rule assigns users to',
},
excludedUserIds: {
type: 'string',
description: 'Comma-separated user IDs excluded from the rule',
},
removeUsers: {
type: 'boolean',
description: 'Remove users the rule assigned when deleting it',
},
},
outputs: {
@@ -420,6 +1063,24 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
name: { type: 'string', description: 'Group name' },
description: { type: 'string', description: 'Group description' },
type: { type: 'string', description: 'Group type' },
mobilePhone: { type: 'string', description: 'Mobile phone number' },
secondEmail: { type: 'string', description: 'Secondary email address' },
displayName: { type: 'string', description: 'Display name' },
title: { type: 'string', description: 'Job title' },
department: { type: 'string', description: 'Department' },
organization: { type: 'string', description: 'Organization' },
manager: { type: 'string', description: 'Manager name' },
managerId: { type: 'string', description: 'Manager ID' },
division: { type: 'string', description: 'Division' },
employeeNumber: { type: 'string', description: 'Employee number' },
userType: { type: 'string', description: 'User type' },
lastLogin: { type: 'string', description: 'Last sign-in timestamp' },
statusChanged: { type: 'string', description: 'Status change timestamp' },
passwordChanged: { type: 'string', description: 'Password change timestamp' },
lastMembershipUpdated: {
type: 'string',
description: 'Timestamp of the last group membership change',
},
count: { type: 'number', description: 'Number of results' },
added: { type: 'boolean', description: 'Whether user was added to group' },
removed: { type: 'boolean', description: 'Whether user was removed from group' },
@@ -436,6 +1097,91 @@ export const OktaBlock: BlockConfig<OktaResponse> = {
},
created: { type: 'string', description: 'Creation timestamp' },
lastUpdated: { type: 'string', description: 'Last update timestamp' },
events: {
type: 'json',
description:
'Array of System Log events (uuid, published, eventType, severity, displayMessage, outcomeResult, actor and client details, targets)',
},
factors: {
type: 'json',
description:
'Array of enrolled MFA factors (id, factorType, provider, vendorName, status, created, lastUpdated, profile)',
},
apps: {
type: 'json',
description:
'Array of applications (id, name, label, status, signOnMode, features, created, lastUpdated)',
},
appUsers: {
type: 'json',
description:
'Array of application user assignments (id, externalId, scope, status, syncState, userName, profile)',
},
appGroups: {
type: 'json',
description: 'Array of application group assignments (id, priority, lastUpdated, profile)',
},
roles: {
type: 'json',
description:
'Array of admin role assignments (id, label, type, status, assignmentType, role, resourceSet)',
},
rules: {
type: 'json',
description:
'Array of group rules (id, name, type, status, expression, assignUserToGroupIds, excludedUserIds)',
},
profile: { type: 'json', description: 'Factor, application, or app user profile attributes' },
settings: { type: 'json', description: 'Application settings' },
visibility: { type: 'json', description: 'Application visibility settings' },
accessibility: { type: 'json', description: 'Application accessibility settings' },
assignUserToGroupIds: { type: 'json', description: 'Groups a rule assigns matching users to' },
excludedUserIds: { type: 'json', description: 'Users excluded from a group rule' },
excludedGroupIds: { type: 'json', description: 'Groups excluded from a group rule' },
amr: { type: 'json', description: 'Authentication methods used to establish a session' },
features: { type: 'json', description: 'Provisioning features enabled on an application' },
label: { type: 'string', description: 'Application or role label' },
signOnMode: { type: 'string', description: 'Application sign-on mode' },
factorType: { type: 'string', description: 'MFA factor type' },
provider: { type: 'string', description: 'MFA factor provider' },
vendorName: { type: 'string', description: 'MFA factor vendor name' },
scope: { type: 'string', description: 'Application assignment scope' },
externalId: { type: 'string', description: 'ID of the user in the downstream application' },
syncState: { type: 'string', description: 'Application provisioning sync state' },
lastSync: { type: 'string', description: 'Last application provisioning sync' },
userName: { type: 'string', description: 'Username the user signs in to the application with' },
priority: { type: 'number', description: 'Application group assignment priority' },
assignmentType: { type: 'string', description: 'How an admin role was assigned' },
role: { type: 'string', description: 'Custom role ID' },
resourceSet: { type: 'string', description: 'Resource set ID for a custom role' },
expression: { type: 'string', description: 'Okta expression driving a group rule' },
expressionType: { type: 'string', description: 'Group rule expression language' },
userId: { type: 'string', description: 'User ID the operation acted on' },
groupId: { type: 'string', description: 'Group ID the operation acted on' },
appId: { type: 'string', description: 'Application ID the operation acted on' },
factorId: { type: 'string', description: 'Factor ID the operation acted on' },
sessionId: { type: 'string', description: 'Session ID the operation acted on' },
groupRuleId: { type: 'string', description: 'Group rule ID the operation acted on' },
roleAssignmentId: { type: 'string', description: 'Role assignment ID that was revoked' },
createdAt: { type: 'string', description: 'Session creation timestamp' },
expiresAt: { type: 'string', description: 'Session expiry timestamp' },
lastPasswordVerification: {
type: 'string',
description: 'Timestamp of the last password verification',
},
lastFactorVerification: {
type: 'string',
description: 'Timestamp of the last factor verification',
},
idpId: { type: 'string', description: 'Identity provider ID' },
idpType: { type: 'string', description: 'Identity provider type' },
nextCursor: { type: 'string', description: 'Cursor for the next page of results' },
hasMore: { type: 'boolean', description: 'Whether more results are available' },
assigned: { type: 'boolean', description: 'Whether the assignment was created' },
enrolled: { type: 'boolean', description: 'Whether the factor was enrolled' },
reset: { type: 'boolean', description: 'Whether the factors were reset' },
cleared: { type: 'boolean', description: 'Whether the sessions were cleared' },
revoked: { type: 'boolean', description: 'Whether the session was revoked' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
@@ -532,6 +1278,24 @@ export const OktaBlockMeta = {
content:
'# Audit Group Membership\n\nReview who belongs to Okta groups, focusing on privileged access.\n\n## Steps\n1. Run List Groups to enumerate the groups, or Get Group for a specific one.\n2. For each group of interest, run List Group Members.\n3. Highlight privileged or admin groups and call out any unexpected members.\n\n## Output\nA per-group roster with member counts, and a short list of access concerns to review.',
},
{
name: 'reset-user-mfa',
description: 'Reset a locked-out user MFA enrollment so they can enroll a factor again.',
content:
'# Reset User MFA\n\nClear a stuck multifactor enrollment, the most common Okta help desk request.\n\n## Steps\n1. Run List Factors for the user to see which factors are enrolled and their status.\n2. Reset the narrowest thing that fixes it: Reset Factor for one factor id, or Reset All Factors when every enrollment must go.\n3. Note that resetting push also unenrolls the related Okta Verify factors, and that factors cannot be reset on a deactivated user.\n4. Run Clear User Sessions if the user must be signed out of existing sessions before re-enrolling.\n\n## Output\nName the factors that were reset and state that the user must re-enroll before they can complete MFA again. Both resets are irreversible, so confirm the target user before running either.',
},
{
name: 'investigate-sign-in-failures',
description: 'Query the Okta System Log for failed sign-ins and suspicious authentication.',
content:
'# Investigate Sign-In Failures\n\nUse the System Log to explain why a user cannot sign in, or to review suspicious authentication.\n\n## Steps\n1. Run Get System Log Events with a Since and Until that bracket the incident.\n2. Filter to the events that matter, for example eventType eq "user.session.start" and outcome.result eq "FAILURE" for failed sign-ins.\n3. Narrow to one person or origin by adding actor.alternateId or client.ipAddress to the filter, or use the keyword query for a free-text sweep.\n4. Read outcomeReason, clientIpAddress, and the client geography on each event to separate a wrong password from an unexpected location.\n5. Page with the returned cursor while hasMore is true when the window is wide.\n\n## Output\nA short timeline of the matching events with actor, time, outcome, reason, and source IP, plus a plain statement of the likely cause.',
},
{
name: 'review-app-access',
description: 'Audit who can reach an Okta application through direct and group assignments.',
content:
'# Review Application Access\n\nEstablish who has access to an application and how they got it.\n\n## Steps\n1. Run List Applications to resolve the application id, or Get Application when the id is known.\n2. Run List Application Users and read the scope on each assignment: USER means a direct grant, GROUP means it was inherited.\n3. Run List Application Groups to see which groups confer access, since every member of those groups reaches the app.\n4. Expand any group of interest with List Group Members to get the real roster.\n5. Revoke with Remove User from Application for a direct grant, or Remove Group from Application to cut the whole group.\n\n## Output\nA roster split into direct and group-inherited access, naming the groups that grant it, and a list of assignments that look unjustified.',
},
{
name: 'reset-user-password',
description: 'Trigger an Okta password reset for a user who is locked out.',
+108 -4
View File
@@ -1,5 +1,5 @@
{
"updatedAt": "2026-08-14",
"updatedAt": "2026-08-15",
"integrations": [
{
"type": "onepassword",
@@ -13723,8 +13723,8 @@
"type": "okta",
"slug": "okta",
"name": "Okta",
"description": "Manage users and groups in Okta",
"longDescription": "Integrate Okta identity management into your workflow. List, create, update, activate, suspend, and delete users. Reset passwords. Manage groups and group membership.",
"description": "Manage users, groups, apps, and MFA in Okta",
"longDescription": "Integrate Okta identity management into your workflow. Manage users, groups, and group rules. Run service desk actions like resetting MFA factors and clearing sessions. Review and change application assignments and admin roles. Query the System Log to audit sign-ins and admin changes.",
"bgColor": "#191919",
"iconName": "OktaIcon",
"docsUrl": "https://docs.sim.ai/integrations/okta",
@@ -13800,9 +13800,113 @@
{
"name": "List Group Members",
"description": "List all members of a specific group in your Okta organization"
},
{
"name": "List Group Rules",
"description": "List the group rules in your Okta organization. Each rule assigns users to groups automatically based on an expression over their profile, so this shows how group membership is being driven."
},
{
"name": "Get Group Rule",
"description": "Retrieve a single Okta group rule by ID, including the expression that decides which users it matches and the groups those users are assigned to."
},
{
"name": "Create Group Rule",
"description": "Create a group rule that automatically assigns users matching an Okta expression to one or more groups. New rules are created INACTIVE, so run Activate Group Rule afterwards to start applying it."
},
{
"name": "Activate Group Rule",
"description": "Activate a group rule so Okta starts applying it, assigning every matching user to the target groups."
},
{
"name": "Deactivate Group Rule",
"description": "Deactivate a group rule so Okta stops applying it. Existing memberships the rule created are left in place. A rule must be INACTIVE before it can be edited."
},
{
"name": "Delete Group Rule",
"description": "Permanently delete a group rule. Destructive and irreversible. Optionally also removes the users that this rule had assigned from those groups, which revokes any access those groups grant."
},
{
"name": "List Factors",
"description": "List the MFA factors a user has enrolled, with each factor type, provider, and enrollment status. Use this before resetting a factor to confirm which one to target."
},
{
"name": "Get Factor",
"description": "Retrieve a single enrolled MFA factor for a user, including its type, provider, enrollment status, and factor-specific profile."
},
{
"name": "Enroll Factor",
"description": "Enroll an MFA factor for a user. The profile fields required depend on the factor type: a phone number for sms and call, an email address for email, and a question and answer for question. Factors that enroll from the user device, such as webauthn and push, need no profile fields."
},
{
"name": "Reset Factor",
"description": "Unenroll one specific MFA factor for a user so they can re-enroll it. Destructive and irreversible: the existing enrollment is removed. Unenrolling a push or signed_nonce factor also unenrolls the related Okta Verify factors. Factors cannot be unenrolled from a deactivated user."
},
{
"name": "Reset All Factors",
"description": "Reset every MFA factor for a user, returning all enrollments to the unenrolled state. Destructive and irreversible: the user must re-enroll each factor before they can complete MFA again. The user status stays ACTIVE."
},
{
"name": "Clear User Sessions",
"description": "Revoke every active Okta session for a user, signing them out of all devices immediately. Destructive and irreversible: the user must sign in again. Optionally also revokes their OAuth and OpenID Connect tokens, and clears remembered factors."
},
{
"name": "Get Session",
"description": "Retrieve an Okta session by ID, including who it belongs to, when it expires, and which authentication methods were used to establish it."
},
{
"name": "Revoke Session",
"description": "Revoke a single Okta session by ID, ending that sign-in immediately. Destructive and irreversible: the affected user must sign in again on that device."
},
{
"name": "List Applications",
"description": "List the applications configured in your Okta organization, with optional name search, filtering, and cursor pagination."
},
{
"name": "Get Application",
"description": "Retrieve a single Okta application by ID, including its sign-on mode, status, enabled provisioning features, and configuration objects."
},
{
"name": "List Application Users",
"description": "List the users assigned to an Okta application, including how each assignment was made and its provisioning sync state. Use this to audit who has access to an app."
},
{
"name": "Assign User to Application",
"description": "Assign a user to an Okta application, granting them access to it. Applications that require credentials also need the username the user signs in with."
},
{
"name": "Remove User from Application",
"description": "Unassign a user from an Okta application, revoking their access. Destructive and irreversible: the app profile for that user is permanently removed, and if provisioning is enabled the downstream account is deactivated."
},
{
"name": "List Application Groups",
"description": "List the groups assigned to an Okta application. Every member of an assigned group inherits access to the app, so this is the starting point for an app access review."
},
{
"name": "Assign Group to Application",
"description": "Assign a group to an Okta application so every member of the group inherits access to it."
},
{
"name": "Remove Group from Application",
"description": "Unassign a group from an Okta application. Destructive: every member who had access only through this group loses access to the app."
},
{
"name": "List User Roles",
"description": "List the administrator roles assigned to a user. Returns both standard roles and custom role bindings, so you can review who holds privileged access."
},
{
"name": "Assign User Role",
"description": "Grant a user an administrator role. Use a standard role type such as USER_ADMIN or HELP_DESK_ADMIN, or CUSTOM together with a custom role ID and a resource set ID. This grants privileged access, so confirm the role is the least privilege that fits."
},
{
"name": "Remove User Role",
"description": "Revoke an administrator role from a user. Destructive: the user immediately loses the admin permissions that role granted. Takes the role assignment ID, not the role type, which List User Roles returns as the role id field."
},
{
"name": "Get System Log Events",
"description": "Query the Okta System Log for sign-ins, admin changes, and security events. Supports a time window, SCIM filter expressions, keyword search, and cursor pagination for audit and investigation workflows."
}
],
"operationCount": 18,
"operationCount": 44,
"triggers": [],
"triggerCount": 0,
"authType": "api-key",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,69 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaActivateGroupRuleParams, OktaActivateGroupRuleResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaActivateGroupRule')
export const oktaActivateGroupRuleTool: ToolConfig<
OktaActivateGroupRuleParams,
OktaActivateGroupRuleResponse
> = {
id: 'okta_activate_group_rule',
name: 'Activate Group Rule in Okta',
description:
'Activate a group rule so Okta starts applying it, assigning every matching user to the target groups.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
groupRuleId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Group rule ID to activate',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/groups/rules/${encodeURIComponent(params.groupRuleId.trim())}/lifecycle/activate`
},
method: 'POST',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to activate group rule in Okta')
}
return {
success: true,
output: {
groupRuleId: params?.groupRuleId ?? '',
activated: true,
success: true,
},
}
},
outputs: {
groupRuleId: { type: 'string', description: 'Activated group rule ID' },
activated: { type: 'boolean', description: 'Whether the rule was activated' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+4 -18
View File
@@ -1,10 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaActivateUserParams,
OktaActivateUserResponse,
OktaApiError,
} from '@/tools/okta/types'
import type { OktaActivateUserParams, OktaActivateUserResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaActivateUser')
@@ -50,23 +47,12 @@ export const oktaActivateUserTool: ToolConfig<OktaActivateUserParams, OktaActiva
return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/lifecycle/activate?sendEmail=${sendEmail}`
},
method: 'POST',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// empty response body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to activate user in Okta')
await throwOktaError(response, logger, 'Failed to activate user in Okta')
}
let activationUrl: string | null = null
+4 -18
View File
@@ -1,10 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaAddUserToGroupParams,
OktaAddUserToGroupResponse,
OktaApiError,
} from '@/tools/okta/types'
import type { OktaAddUserToGroupParams, OktaAddUserToGroupResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaAddUserToGroup')
@@ -51,23 +48,12 @@ export const oktaAddUserToGroupTool: ToolConfig<
return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId.trim())}/users/${encodeURIComponent(params.userId.trim())}`
},
method: 'PUT',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// empty response body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to add user to group in Okta')
await throwOktaError(response, logger, 'Failed to add user to group in Okta')
}
return {
@@ -0,0 +1,99 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaAppGroupAssignment,
OktaAssignGroupToAppParams,
OktaAssignGroupToAppResponse,
} from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaAssignGroupToApp')
export const oktaAssignGroupToAppTool: ToolConfig<
OktaAssignGroupToAppParams,
OktaAssignGroupToAppResponse
> = {
id: 'okta_assign_group_to_app',
name: 'Assign Group to Application in Okta',
description:
'Assign a group to an Okta application so every member of the group inherits access to it.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
appId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Application ID to assign the group to',
},
groupId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Group ID to assign',
},
priority: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'Assignment priority, which resolves conflicting profile mappings when a user belongs to several assigned groups',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/apps/${encodeURIComponent(params.appId.trim())}/groups/${encodeURIComponent(params.groupId.trim())}`
},
method: 'PUT',
headers: (params) => oktaHeaders(params.apiKey),
body: (params) => (params.priority === undefined ? {} : { priority: params.priority }),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to assign group to application in Okta')
}
const assignment: OktaAppGroupAssignment = await response.json()
return {
success: true,
output: {
id: assignment.id,
priority: assignment.priority ?? null,
lastUpdated: assignment.lastUpdated,
profile: assignment.profile ?? null,
assigned: true,
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Assigned group ID' },
priority: { type: 'number', description: 'Assignment priority', optional: true },
lastUpdated: { type: 'string', description: 'Last update timestamp' },
profile: {
type: 'json',
description: 'App-specific profile attributes, whose shape is set by the app schema',
optional: true,
},
assigned: { type: 'boolean', description: 'Whether the group was assigned' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+143
View File
@@ -0,0 +1,143 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaAssignUserRoleParams,
OktaAssignUserRoleResponse,
OktaRoleAssignment,
} from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaAssignUserRole')
export const oktaAssignUserRoleTool: ToolConfig<
OktaAssignUserRoleParams,
OktaAssignUserRoleResponse
> = {
id: 'okta_assign_user_role',
name: 'Assign User Role in Okta',
description:
'Grant a user an administrator role. Use a standard role type such as USER_ADMIN or HELP_DESK_ADMIN, or CUSTOM together with a custom role ID and a resource set ID. This grants privileged access, so confirm the role is the least privilege that fits.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID or login to assign the admin role to',
},
roleType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Role type to assign: SUPER_ADMIN, ORG_ADMIN, APP_ADMIN, USER_ADMIN, HELP_DESK_ADMIN, READ_ONLY_ADMIN, API_ACCESS_MANAGEMENT_ADMIN, GROUP_MEMBERSHIP_ADMIN, REPORT_ADMIN, WORKFLOWS_ADMIN, ACCESS_CERTIFICATIONS_ADMIN, ACCESS_REQUESTS_ADMIN, or CUSTOM',
},
customRoleId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom role ID. Required when the role type is CUSTOM',
},
resourceSetId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Resource set ID the custom role applies to. Required when the role type is CUSTOM',
},
disableNotifications: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description:
'Grant the user third-party admin status, which suppresses Okta admin notifications (default: false)',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
const base = `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/roles`
return params.disableNotifications === undefined
? base
: `${base}?disableNotifications=${params.disableNotifications}`
},
method: 'POST',
headers: (params) => oktaHeaders(params.apiKey),
body: (params) => {
if (params.roleType === 'CUSTOM') {
return {
type: 'CUSTOM',
role: params.customRoleId,
'resource-set': params.resourceSetId,
}
}
return { type: params.roleType }
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to assign user role in Okta')
}
const role: OktaRoleAssignment = await response.json()
return {
success: true,
output: {
id: role.id ?? null,
label: role.label ?? null,
type: role.type,
status: role.status ?? null,
created: role.created ?? null,
lastUpdated: role.lastUpdated ?? null,
assignmentType: role.assignmentType ?? null,
role: role.role ?? null,
resourceSet: role['resource-set'] ?? null,
assigned: true,
success: true,
},
}
},
outputs: {
id: {
type: 'string',
description: 'Role assignment ID, which is what Remove User Role takes',
optional: true,
},
label: { type: 'string', description: 'Role label', optional: true },
type: { type: 'string', description: 'Assigned role type' },
status: { type: 'string', description: 'Role status', optional: true },
created: { type: 'string', description: 'Assignment timestamp', optional: true },
lastUpdated: { type: 'string', description: 'Last update timestamp', optional: true },
assignmentType: {
type: 'string',
description: 'How the role was assigned (USER, GROUP, CLIENT)',
optional: true,
},
role: { type: 'string', description: 'Custom role ID, for custom roles', optional: true },
resourceSet: {
type: 'string',
description: 'Resource set ID, for custom roles',
optional: true,
},
assigned: { type: 'boolean', description: 'Whether the role was assigned' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+138
View File
@@ -0,0 +1,138 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaAppUser,
OktaAssignUserToAppParams,
OktaAssignUserToAppResponse,
} from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaAssignUserToApp')
export const oktaAssignUserToAppTool: ToolConfig<
OktaAssignUserToAppParams,
OktaAssignUserToAppResponse
> = {
id: 'okta_assign_user_to_app',
name: 'Assign User to Application in Okta',
description:
'Assign a user to an Okta application, granting them access to it. Applications that require credentials also need the username the user signs in with.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
appId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Application ID to assign the user to',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Okta user ID to assign',
},
scope: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Assignment scope: USER for a direct assignment, or GROUP',
},
appUserName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Username the user signs in to the application with. Required by applications that store credentials',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/apps/${encodeURIComponent(params.appId.trim())}/users`
},
method: 'POST',
headers: (params) => oktaHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = { id: params.userId }
if (params.scope) body.scope = params.scope
if (params.appUserName) body.credentials = { userName: params.appUserName }
return body
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to assign user to application in Okta')
}
const appUser: OktaAppUser = await response.json()
return {
success: true,
output: {
id: appUser.id,
externalId: appUser.externalId ?? null,
created: appUser.created,
lastUpdated: appUser.lastUpdated,
scope: appUser.scope,
status: appUser.status,
statusChanged: appUser.statusChanged ?? null,
passwordChanged: appUser.passwordChanged ?? null,
syncState: appUser.syncState ?? null,
lastSync: appUser.lastSync ?? null,
userName: appUser.credentials?.userName ?? null,
profile: appUser.profile ?? null,
assigned: true,
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Okta user ID that was assigned' },
externalId: {
type: 'string',
description: 'ID of the user in the downstream application',
optional: true,
},
created: { type: 'string', description: 'Assignment creation timestamp' },
lastUpdated: { type: 'string', description: 'Last update timestamp' },
scope: { type: 'string', description: 'Assignment scope (USER or GROUP)' },
status: { type: 'string', description: 'Assignment status' },
statusChanged: { type: 'string', description: 'Status change timestamp', optional: true },
passwordChanged: {
type: 'string',
description: 'App password change timestamp',
optional: true,
},
syncState: { type: 'string', description: 'Provisioning sync state', optional: true },
lastSync: { type: 'string', description: 'Last provisioning sync', optional: true },
userName: {
type: 'string',
description: 'Username the user signs in to the application with',
optional: true,
},
profile: {
type: 'json',
description: 'App-specific profile attributes, whose shape is set by the app schema',
optional: true,
},
assigned: { type: 'boolean', description: 'Whether the user was assigned' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
@@ -0,0 +1,93 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaClearUserSessionsParams, OktaClearUserSessionsResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaClearUserSessions')
export const oktaClearUserSessionsTool: ToolConfig<
OktaClearUserSessionsParams,
OktaClearUserSessionsResponse
> = {
id: 'okta_clear_user_sessions',
name: 'Clear User Sessions in Okta',
description:
'Revoke every active Okta session for a user, signing them out of all devices immediately. Destructive and irreversible: the user must sign in again. Optionally also revokes their OAuth and OpenID Connect tokens, and clears remembered factors.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID or login whose sessions will be revoked',
},
oauthTokens: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description:
'Also revoke the user OpenID Connect and OAuth refresh and access tokens (default: false)',
},
forgetDevices: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Clear the user remembered factors for all devices (default: true)',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
const queryParams = new URLSearchParams()
if (params.oauthTokens !== undefined) {
queryParams.append('oauthTokens', String(params.oauthTokens))
}
if (params.forgetDevices !== undefined) {
queryParams.append('forgetDevices', String(params.forgetDevices))
}
const queryString = queryParams.toString()
const base = `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/sessions`
return queryString ? `${base}?${queryString}` : base
},
method: 'DELETE',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to clear user sessions in Okta')
}
return {
success: true,
output: {
userId: params?.userId ?? '',
cleared: true,
success: true,
},
}
},
outputs: {
userId: { type: 'string', description: 'User whose sessions were revoked' },
cleared: { type: 'boolean', description: 'Whether the sessions were revoked' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+4 -19
View File
@@ -1,11 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaCreateGroupParams,
OktaCreateGroupResponse,
OktaGroup,
} from '@/tools/okta/types'
import type { OktaCreateGroupParams, OktaCreateGroupResponse, OktaGroup } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaCreateGroup')
@@ -49,11 +45,7 @@ export const oktaCreateGroupTool: ToolConfig<OktaCreateGroupParams, OktaCreateGr
return `https://${domain}/api/v1/groups`
},
method: 'POST',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
body: (params) => {
const profile: Record<string, string> = { name: params.name }
if (params.description) profile.description = params.description
@@ -63,14 +55,7 @@ export const oktaCreateGroupTool: ToolConfig<OktaCreateGroupParams, OktaCreateGr
transformResponse: async (response: Response) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// non-JSON error body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to create group in Okta')
await throwOktaError(response, logger, 'Failed to create group in Okta')
}
const group: OktaGroup = await response.json()
+158
View File
@@ -0,0 +1,158 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaCreateGroupRuleParams,
OktaCreateGroupRuleResponse,
OktaGroupRule,
} from '@/tools/okta/types'
import { mapOktaGroupRule, oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaCreateGroupRule')
/**
* Splits a comma or newline separated ID list into trimmed, non-empty entries.
*/
function parseIdList(value: string | undefined): string[] {
if (!value) return []
return value
.split(/[\n,]/)
.map((id) => id.trim())
.filter((id) => id.length > 0)
}
export const oktaCreateGroupRuleTool: ToolConfig<
OktaCreateGroupRuleParams,
OktaCreateGroupRuleResponse
> = {
id: 'okta_create_group_rule',
name: 'Create Group Rule in Okta',
description:
'Create a group rule that automatically assigns users matching an Okta expression to one or more groups. New rules are created INACTIVE, so run Activate Group Rule afterwards to start applying it.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
ruleName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name for the group rule (maximum 50 characters)',
},
expression: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Okta expression that must evaluate to a boolean (e.g., user.department=="Engineering")',
},
assignUserToGroupIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated group IDs that matching users are assigned to',
},
excludedUserIds: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated user IDs to exclude from the rule',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/groups/rules`
},
method: 'POST',
headers: (params) => oktaHeaders(params.apiKey),
body: (params) => {
const conditions: Record<string, unknown> = {
expression: {
type: 'urn:okta:expression:1.0',
value: params.expression,
},
}
const excludedUserIds = parseIdList(params.excludedUserIds)
if (excludedUserIds.length > 0) {
conditions.people = { users: { exclude: excludedUserIds } }
}
return {
type: 'group_rule',
name: params.ruleName,
conditions,
actions: {
assignUserToGroups: { groupIds: parseIdList(params.assignUserToGroupIds) },
},
}
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to create group rule in Okta')
}
const rule: OktaGroupRule = await response.json()
return {
success: true,
output: {
...mapOktaGroupRule(rule),
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Created group rule ID' },
name: { type: 'string', description: 'Group rule name' },
type: { type: 'string', description: 'Rule type, always group_rule' },
status: {
type: 'string',
description: 'Rule status, which is INACTIVE for a newly created rule',
},
created: { type: 'string', description: 'Creation timestamp', optional: true },
lastUpdated: { type: 'string', description: 'Last update timestamp', optional: true },
expression: {
type: 'string',
description: 'Okta expression that decides which users the rule matches',
optional: true,
},
expressionType: {
type: 'string',
description: 'Expression language, typically urn:okta:expression:1.0',
optional: true,
},
assignUserToGroupIds: {
type: 'array',
description: 'Groups that matching users are assigned to',
items: { type: 'string', description: 'Group ID' },
},
excludedUserIds: {
type: 'array',
description: 'Users excluded from the rule',
items: { type: 'string', description: 'User ID' },
},
excludedGroupIds: {
type: 'array',
description: 'Groups excluded from the rule',
items: { type: 'string', description: 'Group ID' },
},
success: { type: 'boolean', description: 'Operation success status' },
},
}
+4 -19
View File
@@ -1,11 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaCreateUserParams,
OktaCreateUserResponse,
OktaUser,
} from '@/tools/okta/types'
import type { OktaCreateUserParams, OktaCreateUserResponse, OktaUser } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaCreateUser')
@@ -92,11 +88,7 @@ export const oktaCreateUserTool: ToolConfig<OktaCreateUserParams, OktaCreateUser
return `https://${domain}/api/v1/users?activate=${activate}`
},
method: 'POST',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
body: (params) => {
const profile: Record<string, string> = {
firstName: params.firstName,
@@ -123,14 +115,7 @@ export const oktaCreateUserTool: ToolConfig<OktaCreateUserParams, OktaCreateUser
transformResponse: async (response: Response) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// non-JSON error body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to create user in Okta')
await throwOktaError(response, logger, 'Failed to create user in Okta')
}
const user: OktaUser = await response.json()
@@ -0,0 +1,72 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaDeactivateGroupRuleParams,
OktaDeactivateGroupRuleResponse,
} from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaDeactivateGroupRule')
export const oktaDeactivateGroupRuleTool: ToolConfig<
OktaDeactivateGroupRuleParams,
OktaDeactivateGroupRuleResponse
> = {
id: 'okta_deactivate_group_rule',
name: 'Deactivate Group Rule in Okta',
description:
'Deactivate a group rule so Okta stops applying it. Existing memberships the rule created are left in place. A rule must be INACTIVE before it can be edited.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
groupRuleId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Group rule ID to deactivate',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/groups/rules/${encodeURIComponent(params.groupRuleId.trim())}/lifecycle/deactivate`
},
method: 'POST',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to deactivate group rule in Okta')
}
return {
success: true,
output: {
groupRuleId: params?.groupRuleId ?? '',
deactivated: true,
success: true,
},
}
},
outputs: {
groupRuleId: { type: 'string', description: 'Deactivated group rule ID' },
deactivated: { type: 'boolean', description: 'Whether the rule was deactivated' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+4 -18
View File
@@ -1,10 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaDeactivateUserParams,
OktaDeactivateUserResponse,
} from '@/tools/okta/types'
import type { OktaDeactivateUserParams, OktaDeactivateUserResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaDeactivateUser')
@@ -53,23 +50,12 @@ export const oktaDeactivateUserTool: ToolConfig<
return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/lifecycle/deactivate?sendEmail=${sendEmail}`
},
method: 'POST',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// empty response body on some error codes
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to deactivate user in Okta')
await throwOktaError(response, logger, 'Failed to deactivate user in Okta')
}
return {
+4 -18
View File
@@ -1,10 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaDeleteGroupParams,
OktaDeleteGroupResponse,
} from '@/tools/okta/types'
import type { OktaDeleteGroupParams, OktaDeleteGroupResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaDeleteGroup')
@@ -43,23 +40,12 @@ export const oktaDeleteGroupTool: ToolConfig<OktaDeleteGroupParams, OktaDeleteGr
return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId.trim())}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// empty response body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to delete group from Okta')
await throwOktaError(response, logger, 'Failed to delete group from Okta')
}
return {
+77
View File
@@ -0,0 +1,77 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaDeleteGroupRuleParams, OktaDeleteGroupRuleResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaDeleteGroupRule')
export const oktaDeleteGroupRuleTool: ToolConfig<
OktaDeleteGroupRuleParams,
OktaDeleteGroupRuleResponse
> = {
id: 'okta_delete_group_rule',
name: 'Delete Group Rule in Okta',
description:
'Permanently delete a group rule. Destructive and irreversible. Optionally also removes the users that this rule had assigned from those groups, which revokes any access those groups grant.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
groupRuleId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Group rule ID to delete',
},
removeUsers: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description:
'Also remove the users this rule assigned from the groups it targeted (default: false)',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
const base = `https://${domain}/api/v1/groups/rules/${encodeURIComponent(params.groupRuleId.trim())}`
return params.removeUsers === undefined ? base : `${base}?removeUsers=${params.removeUsers}`
},
method: 'DELETE',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to delete group rule in Okta')
}
return {
success: true,
output: {
groupRuleId: params?.groupRuleId ?? '',
deleted: true,
success: true,
},
}
},
outputs: {
groupRuleId: { type: 'string', description: 'Deleted group rule ID' },
deleted: { type: 'boolean', description: 'Whether the rule was deleted' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+4 -14
View File
@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaApiError, OktaDeleteUserParams, OktaDeleteUserResponse } from '@/tools/okta/types'
import type { OktaDeleteUserParams, OktaDeleteUserResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaDeleteUser')
@@ -46,23 +47,12 @@ export const oktaDeleteUserTool: ToolConfig<OktaDeleteUserParams, OktaDeleteUser
return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}?sendEmail=${sendEmail}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// empty response body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to delete user from Okta')
await throwOktaError(response, logger, 'Failed to delete user from Okta')
}
return {
+159
View File
@@ -0,0 +1,159 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaEnrollFactorParams,
OktaEnrollFactorResponse,
OktaFactor,
} from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaEnrollFactor')
export const oktaEnrollFactorTool: ToolConfig<OktaEnrollFactorParams, OktaEnrollFactorResponse> = {
id: 'okta_enroll_factor',
name: 'Enroll Factor in Okta',
description:
'Enroll an MFA factor for a user. The profile fields required depend on the factor type: a phone number for sms and call, an email address for email, and a question and answer for question. Factors that enroll from the user device, such as webauthn and push, need no profile fields.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID or login to enroll the factor for',
},
factorType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Factor type to enroll (sms, call, email, question, push, token:software:totp, u2f, webauthn)',
},
provider: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Factor provider (OKTA, GOOGLE, FIDO, DUO, RSA, SYMANTEC, YUBICO, CUSTOM). Each provider supports a subset of factor types',
},
phoneNumber: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Phone number in E.164 format. Required for the sms and call factor types',
},
factorEmail: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Email address to enroll. Required for the email factor type',
},
securityQuestion: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Security question key (e.g., disliked_food). Required for the question factor type',
},
securityAnswer: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Answer to the security question, minimum 4 characters. Required for the question factor type',
},
activate: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description:
'Activate the factor immediately as part of enrollment. Supported by the sms, call, email, and token:hotp factor types (default: false)',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
const base = `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/factors`
return params.activate === undefined ? base : `${base}?activate=${params.activate}`
},
method: 'POST',
headers: (params) => oktaHeaders(params.apiKey),
body: (params) => {
const profile: Record<string, string> = {}
if (params.phoneNumber) profile.phoneNumber = params.phoneNumber
if (params.factorEmail) profile.email = params.factorEmail
if (params.securityQuestion) profile.question = params.securityQuestion
if (params.securityAnswer) profile.answer = params.securityAnswer
const body: Record<string, unknown> = {
factorType: params.factorType,
provider: params.provider,
}
if (Object.keys(profile).length > 0) body.profile = profile
return body
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to enroll factor in Okta')
}
const factor: OktaFactor = await response.json()
return {
success: true,
output: {
id: factor.id,
factorType: factor.factorType,
provider: factor.provider ?? null,
vendorName: factor.vendorName ?? null,
status: factor.status ?? null,
created: factor.created ?? null,
lastUpdated: factor.lastUpdated ?? null,
profile: factor.profile ?? null,
enrolled: true,
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Enrolled factor ID' },
factorType: { type: 'string', description: 'Factor type' },
provider: { type: 'string', description: 'Factor provider', optional: true },
vendorName: { type: 'string', description: 'Factor vendor name', optional: true },
status: {
type: 'string',
description: 'Enrollment status, typically PENDING_ACTIVATION until the user activates it',
optional: true,
},
created: { type: 'string', description: 'Enrollment timestamp', optional: true },
lastUpdated: { type: 'string', description: 'Last update timestamp', optional: true },
profile: {
type: 'json',
description:
'Factor-specific attributes, which vary by factor type (phone number, email, question, credential ID)',
optional: true,
},
enrolled: { type: 'boolean', description: 'Whether the factor was enrolled' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+161
View File
@@ -0,0 +1,161 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaApplication, OktaGetAppParams, OktaGetAppResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaGetApp')
export const oktaGetAppTool: ToolConfig<OktaGetAppParams, OktaGetAppResponse> = {
id: 'okta_get_app',
name: 'Get Application from Okta',
description:
'Retrieve a single Okta application by ID, including its sign-on mode, status, enabled provisioning features, and configuration objects.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
appId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Application ID to look up',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/apps/${encodeURIComponent(params.appId.trim())}`
},
method: 'GET',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to get application from Okta')
}
const app: OktaApplication = await response.json()
return {
success: true,
output: {
id: app.id,
name: app.name,
label: app.label,
status: app.status,
signOnMode: app.signOnMode,
features: app.features ?? [],
created: app.created,
lastUpdated: app.lastUpdated,
accessibility: app.accessibility ?? null,
visibility: app.visibility ?? null,
settings: app.settings ?? null,
profile: app.profile ?? null,
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Application ID' },
name: { type: 'string', description: 'Application name (the app template key)' },
label: { type: 'string', description: 'Application display label' },
status: { type: 'string', description: 'Application status (ACTIVE, INACTIVE, DELETED)' },
signOnMode: { type: 'string', description: 'Sign-on mode' },
features: {
type: 'array',
description: 'Enabled provisioning features',
items: { type: 'string', description: 'Feature name' },
},
created: { type: 'string', description: 'Creation timestamp' },
lastUpdated: { type: 'string', description: 'Last update timestamp' },
accessibility: {
type: 'json',
description: 'Access settings for the app',
optional: true,
properties: {
errorRedirectUrl: {
type: 'string',
description: 'Custom error page URL',
optional: true,
},
loginRedirectUrl: {
type: 'string',
description: 'Custom login page URL',
optional: true,
},
selfService: {
type: 'boolean',
description: 'Whether users can self-assign the app',
optional: true,
},
},
},
visibility: {
type: 'json',
description: 'Visibility settings for the app',
optional: true,
properties: {
appLinks: {
type: 'json',
description: 'Map of app link name to whether it appears on the End-User Dashboard',
optional: true,
},
autoLaunch: {
type: 'boolean',
description: 'Signs in to the app automatically when the user signs in to Okta',
optional: true,
},
autoSubmitToolbar: {
type: 'boolean',
description: 'Signs in automatically when the user lands on the sign-in page',
optional: true,
},
hide: {
type: 'json',
description: 'Which end-user apps hide this app',
optional: true,
properties: {
iOS: {
type: 'boolean',
description: 'Hidden in Okta Mobile',
optional: true,
},
web: {
type: 'boolean',
description: 'Hidden on the Okta End-User Dashboard',
optional: true,
},
},
},
},
},
settings: {
type: 'json',
description:
'Application settings. Okta types these per app kind, so settings.app differs between a SAML, OIDC, bookmark, or SWA app',
optional: true,
},
profile: {
type: 'json',
description:
'Application profile attributes. Okta accepts any valid JSON schema here, so the shape is whatever the org configured',
optional: true,
},
success: { type: 'boolean', description: 'Operation success status' },
},
}
+91
View File
@@ -0,0 +1,91 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaFactor, OktaGetFactorParams, OktaGetFactorResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaGetFactor')
export const oktaGetFactorTool: ToolConfig<OktaGetFactorParams, OktaGetFactorResponse> = {
id: 'okta_get_factor',
name: 'Get Factor from Okta',
description:
'Retrieve a single enrolled MFA factor for a user, including its type, provider, enrollment status, and factor-specific profile.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID or login the factor belongs to',
},
factorId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Factor ID to look up',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/factors/${encodeURIComponent(params.factorId.trim())}`
},
method: 'GET',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to get factor from Okta')
}
const factor: OktaFactor = await response.json()
return {
success: true,
output: {
id: factor.id,
factorType: factor.factorType,
provider: factor.provider ?? null,
vendorName: factor.vendorName ?? null,
status: factor.status ?? null,
created: factor.created ?? null,
lastUpdated: factor.lastUpdated ?? null,
profile: factor.profile ?? null,
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Factor ID' },
factorType: { type: 'string', description: 'Factor type' },
provider: { type: 'string', description: 'Factor provider', optional: true },
vendorName: { type: 'string', description: 'Factor vendor name', optional: true },
status: { type: 'string', description: 'Enrollment status', optional: true },
created: { type: 'string', description: 'Enrollment timestamp', optional: true },
lastUpdated: { type: 'string', description: 'Last update timestamp', optional: true },
profile: {
type: 'json',
description:
'Factor-specific attributes, which vary by factor type (phone number, email, question, credential ID)',
optional: true,
},
success: { type: 'boolean', description: 'Operation success status' },
},
}
+4 -19
View File
@@ -1,11 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaGetGroupParams,
OktaGetGroupResponse,
OktaGroup,
} from '@/tools/okta/types'
import type { OktaGetGroupParams, OktaGetGroupResponse, OktaGroup } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaGetGroup')
@@ -43,23 +39,12 @@ export const oktaGetGroupTool: ToolConfig<OktaGetGroupParams, OktaGetGroupRespon
return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId.trim())}`
},
method: 'GET',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// non-JSON error body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to get group from Okta')
await throwOktaError(response, logger, 'Failed to get group from Okta')
}
const group: OktaGroup = await response.json()
+100
View File
@@ -0,0 +1,100 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaGetGroupRuleParams,
OktaGetGroupRuleResponse,
OktaGroupRule,
} from '@/tools/okta/types'
import { mapOktaGroupRule, oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaGetGroupRule')
export const oktaGetGroupRuleTool: ToolConfig<OktaGetGroupRuleParams, OktaGetGroupRuleResponse> = {
id: 'okta_get_group_rule',
name: 'Get Group Rule from Okta',
description:
'Retrieve a single Okta group rule by ID, including the expression that decides which users it matches and the groups those users are assigned to.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
groupRuleId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Group rule ID to look up',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/groups/rules/${encodeURIComponent(params.groupRuleId.trim())}`
},
method: 'GET',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to get group rule from Okta')
}
const rule: OktaGroupRule = await response.json()
return {
success: true,
output: {
...mapOktaGroupRule(rule),
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Group rule ID' },
name: { type: 'string', description: 'Group rule name' },
type: { type: 'string', description: 'Rule type, always group_rule' },
status: { type: 'string', description: 'Rule status (ACTIVE, INACTIVE, INVALID)' },
created: { type: 'string', description: 'Creation timestamp', optional: true },
lastUpdated: { type: 'string', description: 'Last update timestamp', optional: true },
expression: {
type: 'string',
description: 'Okta expression that decides which users the rule matches',
optional: true,
},
expressionType: {
type: 'string',
description: 'Expression language, typically urn:okta:expression:1.0',
optional: true,
},
assignUserToGroupIds: {
type: 'array',
description: 'Groups that matching users are assigned to',
items: { type: 'string', description: 'Group ID' },
},
excludedUserIds: {
type: 'array',
description: 'Users excluded from the rule',
items: { type: 'string', description: 'User ID' },
},
excludedGroupIds: {
type: 'array',
description: 'Groups excluded from the rule',
items: { type: 'string', description: 'Group ID' },
},
success: { type: 'boolean', description: 'Operation success status' },
},
}
+265
View File
@@ -0,0 +1,265 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaGetLogsParams, OktaGetLogsResponse, OktaLogEvent } from '@/tools/okta/types'
import { oktaHeaders, parseOktaPagination, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaGetLogs')
export const oktaGetLogsTool: ToolConfig<OktaGetLogsParams, OktaGetLogsResponse> = {
id: 'okta_get_logs',
name: 'Get System Log Events from Okta',
description:
'Query the Okta System Log for sign-ins, admin changes, and security events. Supports a time window, SCIM filter expressions, keyword search, and cursor pagination for audit and investigation workflows.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
since: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Start of the query time window as an ISO 8601 timestamp (default: 7 days before "until")',
},
until: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'End of the query time window as an ISO 8601 timestamp (default: now)',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'SCIM filter expression (e.g., eventType eq "user.session.start" or outcome.result eq "FAILURE")',
},
q: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Keyword search across the event payload (max 40 characters per keyword, max 10 keywords)',
},
sortOrder: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort order: ASCENDING (default) or DESCENDING',
},
after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor returned as nextCursor by a previous call',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of events to return (default: 100, max: 1000)',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
const queryParams = new URLSearchParams()
if (params.since) queryParams.append('since', params.since)
if (params.until) queryParams.append('until', params.until)
if (params.filter) queryParams.append('filter', params.filter)
if (params.q) queryParams.append('q', params.q)
if (params.sortOrder) queryParams.append('sortOrder', params.sortOrder)
if (params.after) queryParams.append('after', params.after)
if (params.limit) queryParams.append('limit', params.limit.toString())
const queryString = queryParams.toString()
return queryString
? `https://${domain}/api/v1/logs?${queryString}`
: `https://${domain}/api/v1/logs`
},
method: 'GET',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to get System Log events from Okta')
}
const { nextCursor, hasMore } = parseOktaPagination(response)
const data: OktaLogEvent[] = await response.json()
const events = data.map((event) => ({
uuid: event.uuid,
published: event.published,
eventType: event.eventType,
severity: event.severity,
legacyEventType: event.legacyEventType ?? null,
displayMessage: event.displayMessage ?? null,
outcomeResult: event.outcome?.result ?? null,
outcomeReason: event.outcome?.reason ?? null,
actorId: event.actor?.id ?? null,
actorType: event.actor?.type ?? null,
actorAlternateId: event.actor?.alternateId ?? null,
actorDisplayName: event.actor?.displayName ?? null,
clientIpAddress: event.client?.ipAddress ?? null,
clientDevice: event.client?.device ?? null,
clientZone: event.client?.zone ?? null,
clientBrowser: event.client?.userAgent?.browser ?? null,
clientOs: event.client?.userAgent?.os ?? null,
clientCity: event.client?.geographicalContext?.city ?? null,
clientState: event.client?.geographicalContext?.state ?? null,
clientCountry: event.client?.geographicalContext?.country ?? null,
authenticationProvider: event.authenticationContext?.authenticationProvider ?? null,
credentialType: event.authenticationContext?.credentialType ?? null,
externalSessionId: event.authenticationContext?.externalSessionId ?? null,
securityAsOrg: event.securityContext?.asOrg ?? null,
securityIsp: event.securityContext?.isp ?? null,
securityIsProxy: event.securityContext?.isProxy ?? null,
transactionId: event.transaction?.id ?? null,
transactionType: event.transaction?.type ?? null,
targets: (event.target ?? []).map((target) => ({
id: target.id ?? null,
type: target.type ?? null,
alternateId: target.alternateId ?? null,
displayName: target.displayName ?? null,
})),
debugData: event.debugContext?.debugData ?? null,
}))
return {
success: true,
output: {
events,
count: events.length,
nextCursor,
hasMore,
success: true,
},
}
},
outputs: {
events: {
type: 'array',
description: 'Array of System Log events',
items: {
type: 'object',
properties: {
uuid: { type: 'string', description: 'Unique event ID' },
published: { type: 'string', description: 'Event timestamp' },
eventType: {
type: 'string',
description: 'Event type (e.g., user.session.start, user.account.update_password)',
},
severity: { type: 'string', description: 'Event severity (DEBUG, ERROR, INFO, WARN)' },
legacyEventType: { type: 'string', description: 'Legacy event type', optional: true },
displayMessage: {
type: 'string',
description: 'Human-readable event description',
optional: true,
},
outcomeResult: {
type: 'string',
description: 'Event outcome (SUCCESS, FAILURE, CHALLENGE, DENY, etc.)',
optional: true,
},
outcomeReason: { type: 'string', description: 'Reason for the outcome', optional: true },
actorId: { type: 'string', description: 'ID of the actor', optional: true },
actorType: {
type: 'string',
description: 'Actor type (User, Client, etc.)',
optional: true,
},
actorAlternateId: {
type: 'string',
description: 'Actor alternate ID, usually the login',
optional: true,
},
actorDisplayName: { type: 'string', description: 'Actor display name', optional: true },
clientIpAddress: { type: 'string', description: 'Client IP address', optional: true },
clientDevice: {
type: 'string',
description: 'Client device category (e.g., Computer)',
optional: true,
},
clientZone: { type: 'string', description: 'Network zone', optional: true },
clientBrowser: { type: 'string', description: 'Client browser', optional: true },
clientOs: { type: 'string', description: 'Client operating system', optional: true },
clientCity: { type: 'string', description: 'Client city', optional: true },
clientState: { type: 'string', description: 'Client state or region', optional: true },
clientCountry: { type: 'string', description: 'Client country', optional: true },
authenticationProvider: {
type: 'string',
description: 'Authentication provider used',
optional: true,
},
credentialType: { type: 'string', description: 'Credential type used', optional: true },
externalSessionId: {
type: 'string',
description: 'External session ID for correlating events',
optional: true,
},
securityAsOrg: {
type: 'string',
description: 'Autonomous system organization',
optional: true,
},
securityIsp: { type: 'string', description: 'Internet service provider', optional: true },
securityIsProxy: {
type: 'boolean',
description: 'Whether the request came through a proxy',
optional: true,
},
transactionId: { type: 'string', description: 'Transaction ID', optional: true },
transactionType: {
type: 'string',
description: 'Transaction type (e.g., WEB, JOB)',
optional: true,
},
targets: {
type: 'array',
description: 'Entities the event acted upon',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Target ID', optional: true },
type: { type: 'string', description: 'Target type', optional: true },
alternateId: { type: 'string', description: 'Target alternate ID', optional: true },
displayName: { type: 'string', description: 'Target display name', optional: true },
},
},
},
debugData: {
type: 'json',
description:
'Extra context whose keys depend on the event type. Okta states these keys and values can change between releases, so treat them as a debugging aid rather than a contract',
optional: true,
},
},
},
},
count: { type: 'number', description: 'Number of events returned' },
nextCursor: {
type: 'string',
description: 'Cursor for the next page, or null on the last page',
optional: true,
},
hasMore: { type: 'boolean', description: 'Whether more events are available' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+102
View File
@@ -0,0 +1,102 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaGetSessionParams, OktaGetSessionResponse, OktaSession } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaGetSession')
export const oktaGetSessionTool: ToolConfig<OktaGetSessionParams, OktaGetSessionResponse> = {
id: 'okta_get_session',
name: 'Get Session from Okta',
description:
'Retrieve an Okta session by ID, including who it belongs to, when it expires, and which authentication methods were used to establish it.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
sessionId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Session ID to look up',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/sessions/${encodeURIComponent(params.sessionId.trim())}`
},
method: 'GET',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to get session from Okta')
}
const session: OktaSession = await response.json()
return {
success: true,
output: {
id: session.id,
login: session.login ?? null,
userId: session.userId ?? null,
status: session.status ?? null,
createdAt: session.createdAt ?? null,
expiresAt: session.expiresAt ?? null,
lastPasswordVerification: session.lastPasswordVerification ?? null,
lastFactorVerification: session.lastFactorVerification ?? null,
amr: session.amr ?? [],
idpId: session.idp?.id ?? null,
idpType: session.idp?.type ?? null,
success: true,
},
}
},
outputs: {
id: { type: 'string', description: 'Session ID' },
login: { type: 'string', description: 'Login of the session user', optional: true },
userId: { type: 'string', description: 'ID of the session user', optional: true },
status: {
type: 'string',
description: 'Session status (ACTIVE, MFA_ENROLL, MFA_REQUIRED)',
optional: true,
},
createdAt: { type: 'string', description: 'Session creation timestamp', optional: true },
expiresAt: { type: 'string', description: 'Session expiry timestamp', optional: true },
lastPasswordVerification: {
type: 'string',
description: 'Timestamp of the last password verification',
optional: true,
},
lastFactorVerification: {
type: 'string',
description: 'Timestamp of the last factor verification',
optional: true,
},
amr: {
type: 'array',
description: 'Authentication methods used to establish the session',
items: { type: 'string', description: 'Authentication method reference' },
},
idpId: { type: 'string', description: 'Identity provider ID', optional: true },
idpType: { type: 'string', description: 'Identity provider type', optional: true },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+4 -19
View File
@@ -1,11 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaGetUserParams,
OktaGetUserResponse,
OktaUser,
} from '@/tools/okta/types'
import type { OktaGetUserParams, OktaGetUserResponse, OktaUser } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaGetUser')
@@ -43,23 +39,12 @@ export const oktaGetUserTool: ToolConfig<OktaGetUserParams, OktaGetUserResponse>
return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}`
},
method: 'GET',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// non-JSON error body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to get user from Okta')
await throwOktaError(response, logger, 'Failed to get user from Okta')
}
const user: OktaUser = await response.json()
+26
View File
@@ -1,17 +1,43 @@
export { oktaActivateGroupRuleTool } from './activate_group_rule'
export { oktaActivateUserTool } from './activate_user'
export { oktaAddUserToGroupTool } from './add_user_to_group'
export { oktaAssignGroupToAppTool } from './assign_group_to_app'
export { oktaAssignUserRoleTool } from './assign_user_role'
export { oktaAssignUserToAppTool } from './assign_user_to_app'
export { oktaClearUserSessionsTool } from './clear_user_sessions'
export { oktaCreateGroupTool } from './create_group'
export { oktaCreateGroupRuleTool } from './create_group_rule'
export { oktaCreateUserTool } from './create_user'
export { oktaDeactivateGroupRuleTool } from './deactivate_group_rule'
export { oktaDeactivateUserTool } from './deactivate_user'
export { oktaDeleteGroupTool } from './delete_group'
export { oktaDeleteGroupRuleTool } from './delete_group_rule'
export { oktaDeleteUserTool } from './delete_user'
export { oktaEnrollFactorTool } from './enroll_factor'
export { oktaGetAppTool } from './get_app'
export { oktaGetFactorTool } from './get_factor'
export { oktaGetGroupTool } from './get_group'
export { oktaGetGroupRuleTool } from './get_group_rule'
export { oktaGetLogsTool } from './get_logs'
export { oktaGetSessionTool } from './get_session'
export { oktaGetUserTool } from './get_user'
export { oktaListAppGroupsTool } from './list_app_groups'
export { oktaListAppUsersTool } from './list_app_users'
export { oktaListAppsTool } from './list_apps'
export { oktaListFactorsTool } from './list_factors'
export { oktaListGroupMembersTool } from './list_group_members'
export { oktaListGroupRulesTool } from './list_group_rules'
export { oktaListGroupsTool } from './list_groups'
export { oktaListUserRolesTool } from './list_user_roles'
export { oktaListUsersTool } from './list_users'
export { oktaRemoveGroupFromAppTool } from './remove_group_from_app'
export { oktaRemoveUserFromAppTool } from './remove_user_from_app'
export { oktaRemoveUserFromGroupTool } from './remove_user_from_group'
export { oktaRemoveUserRoleTool } from './remove_user_role'
export { oktaResetAllFactorsTool } from './reset_all_factors'
export { oktaResetFactorTool } from './reset_factor'
export { oktaResetPasswordTool } from './reset_password'
export { oktaRevokeSessionTool } from './revoke_session'
export { oktaSuspendUserTool } from './suspend_user'
export * from './types'
export { oktaUnsuspendUserTool } from './unsuspend_user'
+135
View File
@@ -0,0 +1,135 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaAppGroupAssignment,
OktaListAppGroupsParams,
OktaListAppGroupsResponse,
} from '@/tools/okta/types'
import { oktaHeaders, parseOktaPagination, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaListAppGroups')
export const oktaListAppGroupsTool: ToolConfig<OktaListAppGroupsParams, OktaListAppGroupsResponse> =
{
id: 'okta_list_app_groups',
name: 'List Application Groups from Okta',
description:
'List the groups assigned to an Okta application. Every member of an assigned group inherits access to the app, so this is the starting point for an app access review.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
appId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Application ID to list assigned groups for',
},
q: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search assigned groups whose name starts with this value',
},
after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor returned as nextCursor by a previous call',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of assigned groups to return (default: 20, range: 20 to 200)',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
const queryParams = new URLSearchParams()
if (params.q) queryParams.append('q', params.q)
if (params.after) queryParams.append('after', params.after)
if (params.limit) queryParams.append('limit', params.limit.toString())
const queryString = queryParams.toString()
const base = `https://${domain}/api/v1/apps/${encodeURIComponent(params.appId.trim())}/groups`
return queryString ? `${base}?${queryString}` : base
},
method: 'GET',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to list application groups from Okta')
}
const { nextCursor, hasMore } = parseOktaPagination(response)
const data: OktaAppGroupAssignment[] = await response.json()
const appGroups = data.map((assignment) => ({
id: assignment.id,
priority: assignment.priority ?? null,
lastUpdated: assignment.lastUpdated,
profile: assignment.profile ?? null,
}))
return {
success: true,
output: {
appGroups,
count: appGroups.length,
nextCursor,
hasMore,
success: true,
},
}
},
outputs: {
appGroups: {
type: 'array',
description: 'Array of application group assignments',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Assigned group ID' },
priority: {
type: 'number',
description: 'Assignment priority, which resolves conflicting profile mappings',
optional: true,
},
lastUpdated: { type: 'string', description: 'Last update timestamp' },
profile: {
type: 'json',
description: 'App-specific profile attributes, whose shape is set by the app schema',
optional: true,
},
},
},
},
count: { type: 'number', description: 'Number of assignments returned' },
nextCursor: {
type: 'string',
description: 'Cursor for the next page, or null on the last page',
optional: true,
},
hasMore: { type: 'boolean', description: 'Whether more assignments are available' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+166
View File
@@ -0,0 +1,166 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaAppUser,
OktaListAppUsersParams,
OktaListAppUsersResponse,
} from '@/tools/okta/types'
import { oktaHeaders, parseOktaPagination, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaListAppUsers')
export const oktaListAppUsersTool: ToolConfig<OktaListAppUsersParams, OktaListAppUsersResponse> = {
id: 'okta_list_app_users',
name: 'List Application Users from Okta',
description:
'List the users assigned to an Okta application, including how each assignment was made and its provisioning sync state. Use this to audit who has access to an app.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
appId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Application ID to list assigned users for',
},
q: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Search assigned users whose userName, firstName, lastName, or email starts with this value',
},
after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor returned as nextCursor by a previous call',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of assigned users to return (default: 50, max: 500)',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
const queryParams = new URLSearchParams()
if (params.q) queryParams.append('q', params.q)
if (params.after) queryParams.append('after', params.after)
if (params.limit) queryParams.append('limit', params.limit.toString())
const queryString = queryParams.toString()
const base = `https://${domain}/api/v1/apps/${encodeURIComponent(params.appId.trim())}/users`
return queryString ? `${base}?${queryString}` : base
},
method: 'GET',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to list application users from Okta')
}
const { nextCursor, hasMore } = parseOktaPagination(response)
const data: OktaAppUser[] = await response.json()
const appUsers = data.map((appUser) => ({
id: appUser.id,
externalId: appUser.externalId ?? null,
created: appUser.created,
lastUpdated: appUser.lastUpdated,
scope: appUser.scope,
status: appUser.status,
statusChanged: appUser.statusChanged ?? null,
passwordChanged: appUser.passwordChanged ?? null,
syncState: appUser.syncState ?? null,
lastSync: appUser.lastSync ?? null,
userName: appUser.credentials?.userName ?? null,
profile: appUser.profile ?? null,
}))
return {
success: true,
output: {
appUsers,
count: appUsers.length,
nextCursor,
hasMore,
success: true,
},
}
},
outputs: {
appUsers: {
type: 'array',
description: 'Array of application user assignments',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Okta user ID' },
externalId: {
type: 'string',
description: 'ID of the user in the downstream application',
optional: true,
},
created: { type: 'string', description: 'Assignment creation timestamp' },
lastUpdated: { type: 'string', description: 'Last update timestamp' },
scope: {
type: 'string',
description: 'How the assignment was made: USER (direct) or GROUP (inherited)',
},
status: { type: 'string', description: 'Assignment status' },
statusChanged: {
type: 'string',
description: 'Status change timestamp',
optional: true,
},
passwordChanged: {
type: 'string',
description: 'App password change timestamp',
optional: true,
},
syncState: { type: 'string', description: 'Provisioning sync state', optional: true },
lastSync: { type: 'string', description: 'Last provisioning sync', optional: true },
userName: {
type: 'string',
description: 'Username the user signs in to the application with',
optional: true,
},
profile: {
type: 'json',
description: 'App-specific profile attributes, whose shape is set by the app schema',
optional: true,
},
},
},
},
count: { type: 'number', description: 'Number of assignments returned' },
nextCursor: {
type: 'string',
description: 'Cursor for the next page, or null on the last page',
optional: true,
},
hasMore: { type: 'boolean', description: 'Whether more assignments are available' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+149
View File
@@ -0,0 +1,149 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaApplication, OktaListAppsParams, OktaListAppsResponse } from '@/tools/okta/types'
import { oktaHeaders, parseOktaPagination, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaListApps')
export const oktaListAppsTool: ToolConfig<OktaListAppsParams, OktaListAppsResponse> = {
id: 'okta_list_apps',
name: 'List Applications from Okta',
description:
'List the applications configured in your Okta organization, with optional name search, filtering, and cursor pagination.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
q: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search for applications whose name or label starts with this value',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Okta filter expression (e.g., status eq "ACTIVE")',
},
includeNonDeleted: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description:
'Also return inactive applications. Deleted applications stay excluded either way (default: false)',
},
after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor returned as nextCursor by a previous call',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of applications to return (max: 200)',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
const queryParams = new URLSearchParams()
if (params.q) queryParams.append('q', params.q)
if (params.filter) queryParams.append('filter', params.filter)
if (params.includeNonDeleted !== undefined) {
queryParams.append('includeNonDeleted', String(params.includeNonDeleted))
}
if (params.after) queryParams.append('after', params.after)
if (params.limit) queryParams.append('limit', params.limit.toString())
const queryString = queryParams.toString()
return queryString
? `https://${domain}/api/v1/apps?${queryString}`
: `https://${domain}/api/v1/apps`
},
method: 'GET',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to list applications from Okta')
}
const { nextCursor, hasMore } = parseOktaPagination(response)
const data: OktaApplication[] = await response.json()
const apps = data.map((app) => ({
id: app.id,
name: app.name,
label: app.label,
status: app.status,
signOnMode: app.signOnMode,
features: app.features ?? [],
created: app.created,
lastUpdated: app.lastUpdated,
}))
return {
success: true,
output: {
apps,
count: apps.length,
nextCursor,
hasMore,
success: true,
},
}
},
outputs: {
apps: {
type: 'array',
description: 'Array of Okta applications',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Application ID' },
name: { type: 'string', description: 'Application name (the app template key)' },
label: { type: 'string', description: 'Application display label' },
status: { type: 'string', description: 'Application status (ACTIVE, INACTIVE, DELETED)' },
signOnMode: {
type: 'string',
description: 'Sign-on mode (SAML_2_0, OPENID_CONNECT, BOOKMARK, etc.)',
},
features: {
type: 'array',
description: 'Enabled provisioning features',
items: { type: 'string', description: 'Feature name' },
},
created: { type: 'string', description: 'Creation timestamp' },
lastUpdated: { type: 'string', description: 'Last update timestamp' },
},
},
},
count: { type: 'number', description: 'Number of applications returned' },
nextCursor: {
type: 'string',
description: 'Cursor for the next page, or null on the last page',
optional: true,
},
hasMore: { type: 'boolean', description: 'Whether more applications are available' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+112
View File
@@ -0,0 +1,112 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaFactor, OktaListFactorsParams, OktaListFactorsResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaListFactors')
export const oktaListFactorsTool: ToolConfig<OktaListFactorsParams, OktaListFactorsResponse> = {
id: 'okta_list_factors',
name: 'List Factors from Okta',
description:
'List the MFA factors a user has enrolled, with each factor type, provider, and enrollment status. Use this before resetting a factor to confirm which one to target.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID or login to list enrolled factors for',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/factors`
},
method: 'GET',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to list factors from Okta')
}
const data: OktaFactor[] = await response.json()
const factors = data.map((factor) => ({
id: factor.id,
factorType: factor.factorType,
provider: factor.provider ?? null,
vendorName: factor.vendorName ?? null,
status: factor.status ?? null,
created: factor.created ?? null,
lastUpdated: factor.lastUpdated ?? null,
profile: factor.profile ?? null,
}))
return {
success: true,
output: {
factors,
count: factors.length,
success: true,
},
}
},
outputs: {
factors: {
type: 'array',
description: 'Array of enrolled MFA factors',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Factor ID' },
factorType: {
type: 'string',
description:
'Factor type (sms, call, email, push, question, token:software:totp, webauthn, etc.)',
},
provider: {
type: 'string',
description: 'Factor provider (OKTA, GOOGLE, FIDO, DUO, RSA, SYMANTEC, YUBICO, CUSTOM)',
optional: true,
},
vendorName: { type: 'string', description: 'Factor vendor name', optional: true },
status: {
type: 'string',
description: 'Enrollment status (ACTIVE, PENDING_ACTIVATION, NOT_SETUP, etc.)',
optional: true,
},
created: { type: 'string', description: 'Enrollment timestamp', optional: true },
lastUpdated: { type: 'string', description: 'Last update timestamp', optional: true },
profile: {
type: 'json',
description:
'Factor-specific attributes, which vary by factor type (phone number, email, question, credential ID)',
optional: true,
},
},
},
},
count: { type: 'number', description: 'Number of enrolled factors' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+21 -15
View File
@@ -1,11 +1,11 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaListGroupMembersParams,
OktaListGroupMembersResponse,
OktaUser,
} from '@/tools/okta/types'
import { oktaHeaders, parseOktaPagination, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaListGroupMembers')
@@ -38,11 +38,18 @@ export const oktaListGroupMembersTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Group ID to list members for',
},
after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor returned as nextCursor by a previous call',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of members to return (default: 1000, max: 1000)',
description:
'Maximum number of members to return per page (default: 1000, but Okta recommends 200)',
},
},
@@ -51,6 +58,7 @@ export const oktaListGroupMembersTool: ToolConfig<
const domain = validateOktaDomain(params.domain)
const queryParams = new URLSearchParams()
if (params.after) queryParams.append('after', params.after)
if (params.limit) queryParams.append('limit', params.limit.toString())
const queryString = queryParams.toString()
@@ -58,25 +66,15 @@ export const oktaListGroupMembersTool: ToolConfig<
return queryString ? `${base}?${queryString}` : base
},
method: 'GET',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// non-JSON error body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to list group members from Okta')
await throwOktaError(response, logger, 'Failed to list group members from Okta')
}
const { nextCursor, hasMore } = parseOktaPagination(response)
const data: OktaUser[] = await response.json()
const members = data.map((user) => ({
@@ -101,6 +99,8 @@ export const oktaListGroupMembersTool: ToolConfig<
output: {
members,
count: members.length,
nextCursor,
hasMore,
success: true,
},
}
@@ -135,6 +135,12 @@ export const oktaListGroupMembersTool: ToolConfig<
},
},
count: { type: 'number', description: 'Number of members returned' },
nextCursor: {
type: 'string',
description: 'Cursor for the next page, or null on the last page',
optional: true,
},
hasMore: { type: 'boolean', description: 'Whether more members are available' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+150
View File
@@ -0,0 +1,150 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaGroupRule,
OktaListGroupRulesParams,
OktaListGroupRulesResponse,
} from '@/tools/okta/types'
import {
mapOktaGroupRule,
oktaHeaders,
parseOktaPagination,
throwOktaError,
} from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaListGroupRules')
export const oktaListGroupRulesTool: ToolConfig<
OktaListGroupRulesParams,
OktaListGroupRulesResponse
> = {
id: 'okta_list_group_rules',
name: 'List Group Rules from Okta',
description:
'List the group rules in your Okta organization. Each rule assigns users to groups automatically based on an expression over their profile, so this shows how group membership is being driven.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
search: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Keyword to search group rules for',
},
after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor returned as nextCursor by a previous call',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of rules to return (default: 50, max: 200)',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
const queryParams = new URLSearchParams()
if (params.search) queryParams.append('search', params.search)
if (params.after) queryParams.append('after', params.after)
if (params.limit) queryParams.append('limit', params.limit.toString())
const queryString = queryParams.toString()
return queryString
? `https://${domain}/api/v1/groups/rules?${queryString}`
: `https://${domain}/api/v1/groups/rules`
},
method: 'GET',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to list group rules from Okta')
}
const { nextCursor, hasMore } = parseOktaPagination(response)
const data: OktaGroupRule[] = await response.json()
const rules = data.map(mapOktaGroupRule)
return {
success: true,
output: {
rules,
count: rules.length,
nextCursor,
hasMore,
success: true,
},
}
},
outputs: {
rules: {
type: 'array',
description: 'Array of group rules',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Group rule ID' },
name: { type: 'string', description: 'Group rule name' },
type: { type: 'string', description: 'Rule type, always group_rule' },
status: { type: 'string', description: 'Rule status (ACTIVE, INACTIVE, INVALID)' },
created: { type: 'string', description: 'Creation timestamp', optional: true },
lastUpdated: { type: 'string', description: 'Last update timestamp', optional: true },
expression: {
type: 'string',
description: 'Okta expression that decides which users the rule matches',
optional: true,
},
expressionType: {
type: 'string',
description: 'Expression language, typically urn:okta:expression:1.0',
optional: true,
},
assignUserToGroupIds: {
type: 'array',
description: 'Groups that matching users are assigned to',
items: { type: 'string', description: 'Group ID' },
},
excludedUserIds: {
type: 'array',
description: 'Users excluded from the rule',
items: { type: 'string', description: 'User ID' },
},
excludedGroupIds: {
type: 'array',
description: 'Groups excluded from the rule',
items: { type: 'string', description: 'Group ID' },
},
},
},
},
count: { type: 'number', description: 'Number of rules returned' },
nextCursor: {
type: 'string',
description: 'Cursor for the next page, or null on the last page',
optional: true,
},
hasMore: { type: 'boolean', description: 'Whether more rules are available' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+21 -20
View File
@@ -1,11 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaGroup,
OktaListGroupsParams,
OktaListGroupsResponse,
} from '@/tools/okta/types'
import type { OktaGroup, OktaListGroupsParams, OktaListGroupsResponse } from '@/tools/okta/types'
import { oktaHeaders, parseOktaPagination, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaListGroups')
@@ -42,11 +38,17 @@ export const oktaListGroupsTool: ToolConfig<OktaListGroupsParams, OktaListGroups
visibility: 'user-or-llm',
description: 'Okta filter expression (e.g., type eq "OKTA_GROUP")',
},
after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor returned as nextCursor by a previous call',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of groups to return (default: 10000, max: 10000)',
description: 'Maximum number of groups to return per page (max: 10000)',
},
},
@@ -57,6 +59,7 @@ export const oktaListGroupsTool: ToolConfig<OktaListGroupsParams, OktaListGroups
if (params.search) queryParams.append('search', params.search)
if (params.filter) queryParams.append('filter', params.filter)
if (params.after) queryParams.append('after', params.after)
if (params.limit) queryParams.append('limit', params.limit.toString())
const queryString = queryParams.toString()
@@ -65,25 +68,15 @@ export const oktaListGroupsTool: ToolConfig<OktaListGroupsParams, OktaListGroups
: `https://${domain}/api/v1/groups`
},
method: 'GET',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// non-JSON error body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to list groups from Okta')
await throwOktaError(response, logger, 'Failed to list groups from Okta')
}
const { nextCursor, hasMore } = parseOktaPagination(response)
const data: OktaGroup[] = await response.json()
const groups = data.map((group) => ({
@@ -101,6 +94,8 @@ export const oktaListGroupsTool: ToolConfig<OktaListGroupsParams, OktaListGroups
output: {
groups,
count: groups.length,
nextCursor,
hasMore,
success: true,
},
}
@@ -128,6 +123,12 @@ export const oktaListGroupsTool: ToolConfig<OktaListGroupsParams, OktaListGroups
},
},
count: { type: 'number', description: 'Number of groups returned' },
nextCursor: {
type: 'string',
description: 'Cursor for the next page, or null on the last page',
optional: true,
},
hasMore: { type: 'boolean', description: 'Whether more groups are available' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+127
View File
@@ -0,0 +1,127 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaListUserRolesParams,
OktaListUserRolesResponse,
OktaRoleAssignment,
} from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaListUserRoles')
export const oktaListUserRolesTool: ToolConfig<OktaListUserRolesParams, OktaListUserRolesResponse> =
{
id: 'okta_list_user_roles',
name: 'List User Roles from Okta',
description:
'List the administrator roles assigned to a user. Returns both standard roles and custom role bindings, so you can review who holds privileged access.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID or login to list admin roles for',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/roles`
},
method: 'GET',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to list user roles from Okta')
}
const data: OktaRoleAssignment[] = await response.json()
const roles = data.map((role) => ({
id: role.id ?? null,
label: role.label ?? null,
type: role.type,
status: role.status ?? null,
created: role.created ?? null,
lastUpdated: role.lastUpdated ?? null,
assignmentType: role.assignmentType ?? null,
role: role.role ?? null,
resourceSet: role['resource-set'] ?? null,
}))
return {
success: true,
output: {
roles,
count: roles.length,
success: true,
},
}
},
outputs: {
roles: {
type: 'array',
description: 'Array of admin role assignments',
items: {
type: 'object',
properties: {
id: {
type: 'string',
description:
'Role assignment ID, which is the resource set binding ID for a custom role. Pass this to Remove User Role',
optional: true,
},
label: { type: 'string', description: 'Role label', optional: true },
type: {
type: 'string',
description:
'Role type (SUPER_ADMIN, ORG_ADMIN, APP_ADMIN, USER_ADMIN, HELP_DESK_ADMIN, READ_ONLY_ADMIN, CUSTOM, etc.)',
},
status: {
type: 'string',
description: 'Role status (ACTIVE, INACTIVE)',
optional: true,
},
created: { type: 'string', description: 'Assignment timestamp', optional: true },
lastUpdated: { type: 'string', description: 'Last update timestamp', optional: true },
assignmentType: {
type: 'string',
description: 'How the role was assigned (USER, GROUP, CLIENT)',
optional: true,
},
role: {
type: 'string',
description: 'Custom role ID, present only on custom role assignments',
optional: true,
},
resourceSet: {
type: 'string',
description: 'Resource set ID, present only on custom role assignments',
optional: true,
},
},
},
},
count: { type: 'number', description: 'Number of role assignments returned' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+21 -20
View File
@@ -1,11 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaListUsersParams,
OktaListUsersResponse,
OktaUser,
} from '@/tools/okta/types'
import type { OktaListUsersParams, OktaListUsersResponse, OktaUser } from '@/tools/okta/types'
import { oktaHeaders, parseOktaPagination, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaListUsers')
@@ -42,11 +38,17 @@ export const oktaListUsersTool: ToolConfig<OktaListUsersParams, OktaListUsersRes
visibility: 'user-or-llm',
description: 'Okta filter expression (e.g., status eq "ACTIVE")',
},
after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor returned as nextCursor by a previous call',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of users to return (default: 200, max: 200)',
description: 'Maximum number of users to return per page (default: 200)',
},
},
@@ -57,6 +59,7 @@ export const oktaListUsersTool: ToolConfig<OktaListUsersParams, OktaListUsersRes
if (params.search) queryParams.append('search', params.search)
if (params.filter) queryParams.append('filter', params.filter)
if (params.after) queryParams.append('after', params.after)
if (params.limit) queryParams.append('limit', params.limit.toString())
const queryString = queryParams.toString()
@@ -65,25 +68,15 @@ export const oktaListUsersTool: ToolConfig<OktaListUsersParams, OktaListUsersRes
: `https://${domain}/api/v1/users`
},
method: 'GET',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// non-JSON error body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to list users from Okta')
await throwOktaError(response, logger, 'Failed to list users from Okta')
}
const { nextCursor, hasMore } = parseOktaPagination(response)
const data: OktaUser[] = await response.json()
const users = data.map((user) => ({
@@ -108,6 +101,8 @@ export const oktaListUsersTool: ToolConfig<OktaListUsersParams, OktaListUsersRes
output: {
users,
count: users.length,
nextCursor,
hasMore,
success: true,
},
}
@@ -141,6 +136,12 @@ export const oktaListUsersTool: ToolConfig<OktaListUsersParams, OktaListUsersRes
},
},
count: { type: 'number', description: 'Number of users returned' },
nextCursor: {
type: 'string',
description: 'Cursor for the next page, or null on the last page',
optional: true,
},
hasMore: { type: 'boolean', description: 'Whether more users are available' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
@@ -0,0 +1,80 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaRemoveGroupFromAppParams,
OktaRemoveGroupFromAppResponse,
} from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaRemoveGroupFromApp')
export const oktaRemoveGroupFromAppTool: ToolConfig<
OktaRemoveGroupFromAppParams,
OktaRemoveGroupFromAppResponse
> = {
id: 'okta_remove_group_from_app',
name: 'Remove Group from Application in Okta',
description:
'Unassign a group from an Okta application. Destructive: every member who had access only through this group loses access to the app.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
appId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Application ID to remove the group from',
},
groupId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Group ID to unassign',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/apps/${encodeURIComponent(params.appId.trim())}/groups/${encodeURIComponent(params.groupId.trim())}`
},
method: 'DELETE',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to remove group from application in Okta')
}
return {
success: true,
output: {
appId: params?.appId ?? '',
groupId: params?.groupId ?? '',
removed: true,
success: true,
},
}
},
outputs: {
appId: { type: 'string', description: 'Application ID' },
groupId: { type: 'string', description: 'Group unassigned from the application' },
removed: { type: 'boolean', description: 'Whether the group was unassigned' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
@@ -0,0 +1,84 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaRemoveUserFromAppParams, OktaRemoveUserFromAppResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaRemoveUserFromApp')
export const oktaRemoveUserFromAppTool: ToolConfig<
OktaRemoveUserFromAppParams,
OktaRemoveUserFromAppResponse
> = {
id: 'okta_remove_user_from_app',
name: 'Remove User from Application in Okta',
description:
'Unassign a user from an Okta application, revoking their access. Destructive and irreversible: the app profile for that user is permanently removed, and if provisioning is enabled the downstream account is deactivated.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
appId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Application ID to remove the user from',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Okta user ID to unassign',
},
sendEmail: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Send a deactivation email to the administrator (default: false)',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
const base = `https://${domain}/api/v1/apps/${encodeURIComponent(params.appId.trim())}/users/${encodeURIComponent(params.userId.trim())}`
return params.sendEmail === undefined ? base : `${base}?sendEmail=${params.sendEmail}`
},
method: 'DELETE',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to remove user from application in Okta')
}
return {
success: true,
output: {
appId: params?.appId ?? '',
userId: params?.userId ?? '',
removed: true,
success: true,
},
}
},
outputs: {
appId: { type: 'string', description: 'Application ID' },
userId: { type: 'string', description: 'User unassigned from the application' },
removed: { type: 'boolean', description: 'Whether the user was unassigned' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+3 -14
View File
@@ -1,10 +1,10 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaRemoveUserFromGroupParams,
OktaRemoveUserFromGroupResponse,
} from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaRemoveUserFromGroup')
@@ -51,23 +51,12 @@ export const oktaRemoveUserFromGroupTool: ToolConfig<
return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId.trim())}/users/${encodeURIComponent(params.userId.trim())}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// empty response body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to remove user from group in Okta')
await throwOktaError(response, logger, 'Failed to remove user from group in Okta')
}
return {
+78
View File
@@ -0,0 +1,78 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaRemoveUserRoleParams, OktaRemoveUserRoleResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaRemoveUserRole')
export const oktaRemoveUserRoleTool: ToolConfig<
OktaRemoveUserRoleParams,
OktaRemoveUserRoleResponse
> = {
id: 'okta_remove_user_role',
name: 'Remove User Role in Okta',
description:
'Revoke an administrator role from a user. Destructive: the user immediately loses the admin permissions that role granted. Takes the role assignment ID, not the role type, which List User Roles returns as the role id field.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID or login to revoke the admin role from',
},
roleAssignmentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Role assignment ID to revoke, as returned by List User Roles. For a custom role this is the resource set binding ID',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/roles/${encodeURIComponent(params.roleAssignmentId.trim())}`
},
method: 'DELETE',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to remove user role in Okta')
}
return {
success: true,
output: {
userId: params?.userId ?? '',
roleAssignmentId: params?.roleAssignmentId ?? '',
removed: true,
success: true,
},
}
},
outputs: {
userId: { type: 'string', description: 'User the role was revoked from' },
roleAssignmentId: { type: 'string', description: 'Revoked role assignment ID' },
removed: { type: 'boolean', description: 'Whether the role was revoked' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+69
View File
@@ -0,0 +1,69 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaResetAllFactorsParams, OktaResetAllFactorsResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaResetAllFactors')
export const oktaResetAllFactorsTool: ToolConfig<
OktaResetAllFactorsParams,
OktaResetAllFactorsResponse
> = {
id: 'okta_reset_all_factors',
name: 'Reset All Factors in Okta',
description:
'Reset every MFA factor for a user, returning all enrollments to the unenrolled state. Destructive and irreversible: the user must re-enroll each factor before they can complete MFA again. The user status stays ACTIVE.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID or login whose MFA factors will all be reset',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/lifecycle/reset_factors`
},
method: 'POST',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to reset all factors in Okta')
}
return {
success: true,
output: {
userId: params?.userId ?? '',
reset: true,
success: true,
},
}
},
outputs: {
userId: { type: 'string', description: 'User whose factors were reset' },
reset: { type: 'boolean', description: 'Whether all factors were reset' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+84
View File
@@ -0,0 +1,84 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaResetFactorParams, OktaResetFactorResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaResetFactor')
export const oktaResetFactorTool: ToolConfig<OktaResetFactorParams, OktaResetFactorResponse> = {
id: 'okta_reset_factor',
name: 'Reset Factor in Okta',
description:
'Unenroll one specific MFA factor for a user so they can re-enroll it. Destructive and irreversible: the existing enrollment is removed. Unenrolling a push or signed_nonce factor also unenrolls the related Okta Verify factors. Factors cannot be unenrolled from a deactivated user.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID or login the factor belongs to',
},
factorId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Factor ID to unenroll',
},
removeRecoveryEnrollment: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description:
'Also remove the phone number as a recovery method, not only as a factor. Applies to sms and call factors only (default: false)',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
const base = `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/factors/${encodeURIComponent(params.factorId.trim())}`
return params.removeRecoveryEnrollment === undefined
? base
: `${base}?removeRecoveryEnrollment=${params.removeRecoveryEnrollment}`
},
method: 'DELETE',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to reset factor in Okta')
}
return {
success: true,
output: {
userId: params?.userId ?? '',
factorId: params?.factorId ?? '',
reset: true,
success: true,
},
}
},
outputs: {
userId: { type: 'string', description: 'User the factor belonged to' },
factorId: { type: 'string', description: 'Unenrolled factor ID' },
reset: { type: 'boolean', description: 'Whether the factor was unenrolled' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+4 -18
View File
@@ -1,10 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaResetPasswordParams,
OktaResetPasswordResponse,
} from '@/tools/okta/types'
import type { OktaResetPasswordParams, OktaResetPasswordResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaResetPassword')
@@ -51,23 +48,12 @@ export const oktaResetPasswordTool: ToolConfig<OktaResetPasswordParams, OktaRese
return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/lifecycle/reset_password?sendEmail=${sendEmail}`
},
method: 'POST',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// empty response body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to reset password in Okta')
await throwOktaError(response, logger, 'Failed to reset password in Okta')
}
let resetPasswordUrl: string | null = null
+67
View File
@@ -0,0 +1,67 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type { OktaRevokeSessionParams, OktaRevokeSessionResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaRevokeSession')
export const oktaRevokeSessionTool: ToolConfig<OktaRevokeSessionParams, OktaRevokeSessionResponse> =
{
id: 'okta_revoke_session',
name: 'Revoke Session in Okta',
description:
'Revoke a single Okta session by ID, ending that sign-in immediately. Destructive and irreversible: the affected user must sign in again on that device.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta API token for authentication',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Okta domain (e.g., dev-123456.okta.com)',
},
sessionId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Session ID to revoke',
},
},
request: {
url: (params) => {
const domain = validateOktaDomain(params.domain)
return `https://${domain}/api/v1/sessions/${encodeURIComponent(params.sessionId.trim())}`
},
method: 'DELETE',
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
await throwOktaError(response, logger, 'Failed to revoke session in Okta')
}
return {
success: true,
output: {
sessionId: params?.sessionId ?? '',
revoked: true,
success: true,
},
}
},
outputs: {
sessionId: { type: 'string', description: 'Revoked session ID' },
revoked: { type: 'boolean', description: 'Whether the session was revoked' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
+4 -18
View File
@@ -1,10 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaSuspendUserParams,
OktaSuspendUserResponse,
} from '@/tools/okta/types'
import type { OktaSuspendUserParams, OktaSuspendUserResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaSuspendUser')
@@ -43,23 +40,12 @@ export const oktaSuspendUserTool: ToolConfig<OktaSuspendUserParams, OktaSuspendU
return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/lifecycle/suspend`
},
method: 'POST',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// empty response body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to suspend user in Okta')
await throwOktaError(response, logger, 'Failed to suspend user in Okta')
}
return {
+738
View File
@@ -113,6 +113,7 @@ interface OktaGroupOutput {
export interface OktaListUsersParams extends OktaBaseParams {
search?: string
filter?: string
after?: string
limit?: number
}
@@ -120,6 +121,8 @@ export interface OktaListUsersResponse extends ToolResponse {
output: {
users: OktaUserOutput[]
count: number
nextCursor: string | null
hasMore: boolean
success: boolean
}
}
@@ -229,6 +232,7 @@ export interface OktaDeactivateUserResponse extends ToolResponse {
export interface OktaListGroupsParams extends OktaBaseParams {
search?: string
filter?: string
after?: string
limit?: number
}
@@ -236,6 +240,8 @@ export interface OktaListGroupsResponse extends ToolResponse {
output: {
groups: OktaGroupOutput[]
count: number
nextCursor: string | null
hasMore: boolean
success: boolean
}
}
@@ -291,6 +297,7 @@ export interface OktaRemoveUserFromGroupResponse extends ToolResponse {
// List Group Members
export interface OktaListGroupMembersParams extends OktaBaseParams {
groupId: string
after?: string
limit?: number
}
@@ -298,6 +305,8 @@ export interface OktaListGroupMembersResponse extends ToolResponse {
output: {
members: OktaUserOutput[]
count: number
nextCursor: string | null
hasMore: boolean
success: boolean
}
}
@@ -424,6 +433,709 @@ export interface OktaDeleteGroupResponse extends ToolResponse {
}
}
/**
* Okta Application object from the API.
*
* `accessibility`, `visibility`, `settings`, and `profile` are polymorphic per
* `signOnMode`, so they stay untyped maps rather than a guessed fixed shape.
*/
export interface OktaApplication {
id: string
name: string
label: string
status: string
signOnMode: string
features?: string[] | null
created: string
lastUpdated: string
accessibility?: Record<string, unknown> | null
visibility?: Record<string, unknown> | null
settings?: Record<string, unknown> | null
profile?: Record<string, unknown> | null
}
/**
* Transformed application summary output
*/
interface OktaApplicationOutput {
id: string
name: string
label: string
status: string
signOnMode: string
features: string[]
created: string
lastUpdated: string
}
/**
* Okta Application User (assignment) object from the API
*/
export interface OktaAppUser {
id: string
externalId?: string | null
created: string
lastUpdated: string
scope: string
status: string
statusChanged?: string | null
passwordChanged?: string | null
syncState?: string | null
lastSync?: string | null
credentials?: { userName?: string | null } | null
profile?: Record<string, unknown> | null
}
/**
* Transformed application user output
*/
interface OktaAppUserOutput {
id: string
externalId: string | null
created: string
lastUpdated: string
scope: string
status: string
statusChanged: string | null
passwordChanged: string | null
syncState: string | null
lastSync: string | null
userName: string | null
profile: Record<string, unknown> | null
}
/**
* Okta Application Group Assignment object from the API
*/
export interface OktaAppGroupAssignment {
id: string
priority?: number | null
lastUpdated: string
profile?: Record<string, unknown> | null
}
/**
* Transformed application group assignment output
*/
interface OktaAppGroupOutput {
id: string
priority: number | null
lastUpdated: string
profile: Record<string, unknown> | null
}
/**
* Okta admin role assignment object.
*
* Standard and custom assignments form a union discriminated on `type`. A custom
* assignment is a resource-set binding, so its `id` is the binding id and it
* carries `role` plus the hyphenated `resource-set` rather than a label alone.
*/
export interface OktaRoleAssignment {
id?: string | null
label?: string | null
type: string
status?: string | null
created?: string | null
lastUpdated?: string | null
assignmentType?: string | null
role?: string | null
'resource-set'?: string | null
}
/**
* Transformed admin role assignment output
*/
interface OktaRoleAssignmentOutput {
id: string | null
label: string | null
type: string
status: string | null
created: string | null
lastUpdated: string | null
assignmentType: string | null
role: string | null
resourceSet: string | null
}
/**
* Okta Group Rule object from the API
*/
export interface OktaGroupRule {
id: string
name: string
type: string
status: string
created?: string | null
lastUpdated?: string | null
conditions?: {
expression?: { type?: string | null; value?: string | null } | null
people?: {
users?: { exclude?: string[] | null } | null
groups?: { exclude?: string[] | null } | null
} | null
} | null
actions?: { assignUserToGroups?: { groupIds?: string[] | null } | null } | null
}
/**
* Transformed group rule output
*/
interface OktaGroupRuleOutput {
id: string
name: string
type: string
status: string
created: string | null
lastUpdated: string | null
expression: string | null
expressionType: string | null
assignUserToGroupIds: string[]
excludedUserIds: string[]
excludedGroupIds: string[]
}
/**
* Okta System Log event object from the API
*/
export interface OktaLogEvent {
uuid: string
published: string
eventType: string
severity: string
legacyEventType?: string | null
displayMessage?: string | null
actor?: {
id?: string | null
type?: string | null
alternateId?: string | null
displayName?: string | null
} | null
client?: {
id?: string | null
ipAddress?: string | null
device?: string | null
zone?: string | null
userAgent?: { browser?: string | null; os?: string | null; rawUserAgent?: string | null } | null
geographicalContext?: {
city?: string | null
state?: string | null
country?: string | null
} | null
} | null
authenticationContext?: {
externalSessionId?: string | null
authenticationProvider?: string | null
credentialProvider?: string | null
credentialType?: string | null
interface?: string | null
} | null
securityContext?: {
asOrg?: string | null
isp?: string | null
domain?: string | null
isProxy?: boolean | null
} | null
target?:
| {
id?: string | null
type?: string | null
alternateId?: string | null
displayName?: string | null
}[]
| null
transaction?: { id?: string | null; type?: string | null } | null
debugContext?: { debugData?: Record<string, unknown> | null } | null
outcome?: { result?: string | null; reason?: string | null } | null
}
/**
* Transformed System Log event output
*/
interface OktaLogEventOutput {
uuid: string
published: string
eventType: string
severity: string
legacyEventType: string | null
displayMessage: string | null
outcomeResult: string | null
outcomeReason: string | null
actorId: string | null
actorType: string | null
actorAlternateId: string | null
actorDisplayName: string | null
clientIpAddress: string | null
clientDevice: string | null
clientZone: string | null
clientBrowser: string | null
clientOs: string | null
clientCity: string | null
clientState: string | null
clientCountry: string | null
authenticationProvider: string | null
credentialType: string | null
externalSessionId: string | null
securityAsOrg: string | null
securityIsp: string | null
securityIsProxy: boolean | null
transactionId: string | null
transactionType: string | null
targets: {
id: string | null
type: string | null
alternateId: string | null
displayName: string | null
}[]
debugData: Record<string, unknown> | null
}
/**
* Okta Session object from the API
*/
export interface OktaSession {
id: string
login?: string | null
userId?: string | null
status?: string | null
createdAt?: string | null
expiresAt?: string | null
lastPasswordVerification?: string | null
lastFactorVerification?: string | null
amr?: string[] | null
idp?: { id?: string | null; type?: string | null } | null
}
/**
* Okta UserFactor object from the API.
*
* `profile` is a discriminated union keyed on `factorType` (phone number for
* `sms`/`call`, email for `email`, question/answer for `question`, credential id
* for the token families), so it stays an untyped map rather than one guessed
* shape. `_embedded` is free-form in the schema and is not surfaced.
*/
export interface OktaFactor {
id: string
factorType: string
provider?: string | null
vendorName?: string | null
status?: string | null
created?: string | null
lastUpdated?: string | null
profile?: Record<string, unknown> | null
}
/**
* Transformed factor output
*/
interface OktaFactorOutput {
id: string
factorType: string
provider: string | null
vendorName: string | null
status: string | null
created: string | null
lastUpdated: string | null
profile: Record<string, unknown> | null
}
// List Factors
export interface OktaListFactorsParams extends OktaBaseParams {
userId: string
}
export interface OktaListFactorsResponse extends ToolResponse {
output: {
factors: OktaFactorOutput[]
count: number
success: boolean
}
}
// Get Factor
export interface OktaGetFactorParams extends OktaBaseParams {
userId: string
factorId: string
}
export interface OktaGetFactorResponse extends ToolResponse {
output: OktaFactorOutput & { success: boolean }
}
// Reset Factor
export interface OktaResetFactorParams extends OktaBaseParams {
userId: string
factorId: string
removeRecoveryEnrollment?: boolean
}
export interface OktaResetFactorResponse extends ToolResponse {
output: {
userId: string
factorId: string
reset: boolean
success: boolean
}
}
// Reset All Factors
export interface OktaResetAllFactorsParams extends OktaBaseParams {
userId: string
}
export interface OktaResetAllFactorsResponse extends ToolResponse {
output: {
userId: string
reset: boolean
success: boolean
}
}
// Enroll Factor
export interface OktaEnrollFactorParams extends OktaBaseParams {
userId: string
factorType: string
provider: string
phoneNumber?: string
factorEmail?: string
securityQuestion?: string
securityAnswer?: string
activate?: boolean
}
export interface OktaEnrollFactorResponse extends ToolResponse {
output: OktaFactorOutput & { enrolled: boolean; success: boolean }
}
// Get Logs
export interface OktaGetLogsParams extends OktaBaseParams {
since?: string
until?: string
filter?: string
q?: string
sortOrder?: string
after?: string
limit?: number
}
export interface OktaGetLogsResponse extends ToolResponse {
output: {
events: OktaLogEventOutput[]
count: number
nextCursor: string | null
hasMore: boolean
success: boolean
}
}
// Clear User Sessions
export interface OktaClearUserSessionsParams extends OktaBaseParams {
userId: string
oauthTokens?: boolean
forgetDevices?: boolean
}
export interface OktaClearUserSessionsResponse extends ToolResponse {
output: {
userId: string
cleared: boolean
success: boolean
}
}
// Get Session
export interface OktaGetSessionParams extends OktaBaseParams {
sessionId: string
}
export interface OktaGetSessionResponse extends ToolResponse {
output: {
id: string
login: string | null
userId: string | null
status: string | null
createdAt: string | null
expiresAt: string | null
lastPasswordVerification: string | null
lastFactorVerification: string | null
amr: string[]
idpId: string | null
idpType: string | null
success: boolean
}
}
// Revoke Session
export interface OktaRevokeSessionParams extends OktaBaseParams {
sessionId: string
}
export interface OktaRevokeSessionResponse extends ToolResponse {
output: {
sessionId: string
revoked: boolean
success: boolean
}
}
// List Applications
export interface OktaListAppsParams extends OktaBaseParams {
q?: string
filter?: string
includeNonDeleted?: boolean
after?: string
limit?: number
}
export interface OktaListAppsResponse extends ToolResponse {
output: {
apps: OktaApplicationOutput[]
count: number
nextCursor: string | null
hasMore: boolean
success: boolean
}
}
// Get Application
export interface OktaGetAppParams extends OktaBaseParams {
appId: string
}
export interface OktaGetAppResponse extends ToolResponse {
output: {
id: string
name: string
label: string
status: string
signOnMode: string
features: string[]
created: string
lastUpdated: string
accessibility: Record<string, unknown> | null
visibility: Record<string, unknown> | null
settings: Record<string, unknown> | null
profile: Record<string, unknown> | null
success: boolean
}
}
// List Application Users
export interface OktaListAppUsersParams extends OktaBaseParams {
appId: string
q?: string
after?: string
limit?: number
}
export interface OktaListAppUsersResponse extends ToolResponse {
output: {
appUsers: OktaAppUserOutput[]
count: number
nextCursor: string | null
hasMore: boolean
success: boolean
}
}
// Assign User to Application
export interface OktaAssignUserToAppParams extends OktaBaseParams {
appId: string
userId: string
scope?: string
appUserName?: string
}
export interface OktaAssignUserToAppResponse extends ToolResponse {
output: OktaAppUserOutput & { assigned: boolean; success: boolean }
}
// Remove User from Application
export interface OktaRemoveUserFromAppParams extends OktaBaseParams {
appId: string
userId: string
sendEmail?: boolean
}
export interface OktaRemoveUserFromAppResponse extends ToolResponse {
output: {
appId: string
userId: string
removed: boolean
success: boolean
}
}
// List Application Groups
export interface OktaListAppGroupsParams extends OktaBaseParams {
appId: string
q?: string
after?: string
limit?: number
}
export interface OktaListAppGroupsResponse extends ToolResponse {
output: {
appGroups: OktaAppGroupOutput[]
count: number
nextCursor: string | null
hasMore: boolean
success: boolean
}
}
// Assign Group to Application
export interface OktaAssignGroupToAppParams extends OktaBaseParams {
appId: string
groupId: string
priority?: number
}
export interface OktaAssignGroupToAppResponse extends ToolResponse {
output: {
id: string
priority: number | null
lastUpdated: string
profile: Record<string, unknown> | null
assigned: boolean
success: boolean
}
}
// Remove Group from Application
export interface OktaRemoveGroupFromAppParams extends OktaBaseParams {
appId: string
groupId: string
}
export interface OktaRemoveGroupFromAppResponse extends ToolResponse {
output: {
appId: string
groupId: string
removed: boolean
success: boolean
}
}
// List User Roles
export interface OktaListUserRolesParams extends OktaBaseParams {
userId: string
}
export interface OktaListUserRolesResponse extends ToolResponse {
output: {
roles: OktaRoleAssignmentOutput[]
count: number
success: boolean
}
}
// Assign User Role
export interface OktaAssignUserRoleParams extends OktaBaseParams {
userId: string
roleType: string
customRoleId?: string
resourceSetId?: string
disableNotifications?: boolean
}
export interface OktaAssignUserRoleResponse extends ToolResponse {
output: OktaRoleAssignmentOutput & { assigned: boolean; success: boolean }
}
// Remove User Role
export interface OktaRemoveUserRoleParams extends OktaBaseParams {
userId: string
roleAssignmentId: string
}
export interface OktaRemoveUserRoleResponse extends ToolResponse {
output: {
userId: string
roleAssignmentId: string
removed: boolean
success: boolean
}
}
// List Group Rules
export interface OktaListGroupRulesParams extends OktaBaseParams {
search?: string
after?: string
limit?: number
}
export interface OktaListGroupRulesResponse extends ToolResponse {
output: {
rules: OktaGroupRuleOutput[]
count: number
nextCursor: string | null
hasMore: boolean
success: boolean
}
}
// Get Group Rule
export interface OktaGetGroupRuleParams extends OktaBaseParams {
groupRuleId: string
}
export interface OktaGetGroupRuleResponse extends ToolResponse {
output: OktaGroupRuleOutput & { success: boolean }
}
// Create Group Rule
export interface OktaCreateGroupRuleParams extends OktaBaseParams {
ruleName: string
expression: string
assignUserToGroupIds: string
excludedUserIds?: string
}
export interface OktaCreateGroupRuleResponse extends ToolResponse {
output: OktaGroupRuleOutput & { success: boolean }
}
// Activate Group Rule
export interface OktaActivateGroupRuleParams extends OktaBaseParams {
groupRuleId: string
}
export interface OktaActivateGroupRuleResponse extends ToolResponse {
output: {
groupRuleId: string
activated: boolean
success: boolean
}
}
// Deactivate Group Rule
export interface OktaDeactivateGroupRuleParams extends OktaBaseParams {
groupRuleId: string
}
export interface OktaDeactivateGroupRuleResponse extends ToolResponse {
output: {
groupRuleId: string
deactivated: boolean
success: boolean
}
}
// Delete Group Rule
export interface OktaDeleteGroupRuleParams extends OktaBaseParams {
groupRuleId: string
removeUsers?: boolean
}
export interface OktaDeleteGroupRuleResponse extends ToolResponse {
output: {
groupRuleId: string
deleted: boolean
success: boolean
}
}
// Generic response type for the block
export type OktaResponse =
| OktaListUsersResponse
@@ -444,3 +1156,29 @@ export type OktaResponse =
| OktaAddUserToGroupResponse
| OktaRemoveUserFromGroupResponse
| OktaListGroupMembersResponse
| OktaGetLogsResponse
| OktaClearUserSessionsResponse
| OktaGetSessionResponse
| OktaRevokeSessionResponse
| OktaListFactorsResponse
| OktaGetFactorResponse
| OktaResetFactorResponse
| OktaResetAllFactorsResponse
| OktaEnrollFactorResponse
| OktaListAppsResponse
| OktaGetAppResponse
| OktaListAppUsersResponse
| OktaAssignUserToAppResponse
| OktaRemoveUserFromAppResponse
| OktaListAppGroupsResponse
| OktaAssignGroupToAppResponse
| OktaRemoveGroupFromAppResponse
| OktaListUserRolesResponse
| OktaAssignUserRoleResponse
| OktaRemoveUserRoleResponse
| OktaListGroupRulesResponse
| OktaGetGroupRuleResponse
| OktaCreateGroupRuleResponse
| OktaActivateGroupRuleResponse
| OktaDeactivateGroupRuleResponse
| OktaDeleteGroupRuleResponse
+4 -18
View File
@@ -1,10 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaUnsuspendUserParams,
OktaUnsuspendUserResponse,
} from '@/tools/okta/types'
import type { OktaUnsuspendUserParams, OktaUnsuspendUserResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaUnsuspendUser')
@@ -44,23 +41,12 @@ export const oktaUnsuspendUserTool: ToolConfig<OktaUnsuspendUserParams, OktaUnsu
return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/lifecycle/unsuspend`
},
method: 'POST',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// empty response body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to unsuspend user in Okta')
await throwOktaError(response, logger, 'Failed to unsuspend user in Okta')
}
return {
+4 -19
View File
@@ -1,11 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaGroup,
OktaUpdateGroupParams,
OktaUpdateGroupResponse,
} from '@/tools/okta/types'
import type { OktaGroup, OktaUpdateGroupParams, OktaUpdateGroupResponse } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaUpdateGroup')
@@ -56,11 +52,7 @@ export const oktaUpdateGroupTool: ToolConfig<OktaUpdateGroupParams, OktaUpdateGr
return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId.trim())}`
},
method: 'PUT',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
body: (params) => ({
profile: {
name: params.name,
@@ -71,14 +63,7 @@ export const oktaUpdateGroupTool: ToolConfig<OktaUpdateGroupParams, OktaUpdateGr
transformResponse: async (response: Response) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// non-JSON error body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to update group in Okta')
await throwOktaError(response, logger, 'Failed to update group in Okta')
}
const group: OktaGroup = await response.json()
+4 -19
View File
@@ -1,11 +1,7 @@
import { createLogger } from '@sim/logger'
import { validateOktaDomain } from '@/lib/core/security/input-validation'
import type {
OktaApiError,
OktaUpdateUserParams,
OktaUpdateUserResponse,
OktaUser,
} from '@/tools/okta/types'
import type { OktaUpdateUserParams, OktaUpdateUserResponse, OktaUser } from '@/tools/okta/types'
import { oktaHeaders, throwOktaError } from '@/tools/okta/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OktaUpdateUser')
@@ -85,11 +81,7 @@ export const oktaUpdateUserTool: ToolConfig<OktaUpdateUserParams, OktaUpdateUser
return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}`
},
method: 'POST',
headers: (params) => ({
Authorization: `SSWS ${params.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}),
headers: (params) => oktaHeaders(params.apiKey),
body: (params) => {
const profile: Record<string, string> = {}
@@ -107,14 +99,7 @@ export const oktaUpdateUserTool: ToolConfig<OktaUpdateUserParams, OktaUpdateUser
transformResponse: async (response: Response) => {
if (!response.ok) {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// non-JSON error body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || 'Failed to update user in Okta')
await throwOktaError(response, logger, 'Failed to update user in Okta')
}
const user: OktaUser = await response.json()
+101
View File
@@ -0,0 +1,101 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { mapOktaGroupRule, oktaHeaders, parseOktaPagination } from '@/tools/okta/utils'
/** Obvious non-secret so credential scanners do not flag these fixtures. */
const PLACEHOLDER_TOKEN = 'not-a-real-api-token'
const BASE = 'https://example.okta.com/api/v1/users'
function responseWithLink(link?: string): Response {
return new Response('[]', {
status: 200,
headers: link ? { Link: link } : {},
})
}
describe('oktaHeaders', () => {
it('authenticates with the SSWS scheme rather than Bearer', () => {
expect(oktaHeaders(PLACEHOLDER_TOKEN).Authorization).toBe(`SSWS ${PLACEHOLDER_TOKEN}`)
})
})
describe('parseOktaPagination', () => {
it('extracts the after cursor from a rel="next" link', () => {
const link = `<${BASE}?after=cursor123&limit=200>; rel="next"`
expect(parseOktaPagination(responseWithLink(link))).toEqual({
nextCursor: 'cursor123',
hasMore: true,
})
})
it('reports the last page when only a self link is present', () => {
const link = `<${BASE}?limit=200>; rel="self"`
expect(parseOktaPagination(responseWithLink(link))).toEqual({
nextCursor: null,
hasMore: false,
})
})
it('picks the next link when self is advertised alongside it', () => {
const link = `<${BASE}?limit=200>; rel="self", <${BASE}?after=page2>; rel="next"`
expect(parseOktaPagination(responseWithLink(link))).toEqual({
nextCursor: 'page2',
hasMore: true,
})
})
it('reports the last page when no Link header is sent at all', () => {
expect(parseOktaPagination(responseWithLink())).toEqual({ nextCursor: null, hasMore: false })
})
it('reports more results without a cursor when the next link is malformed', () => {
expect(parseOktaPagination(responseWithLink('<not a url>; rel="next"'))).toEqual({
nextCursor: null,
hasMore: true,
})
})
})
describe('mapOktaGroupRule', () => {
it('lifts the expression, target groups, and exclusions to the top level', () => {
const mapped = mapOktaGroupRule({
id: '0pr1',
name: 'Engineers',
type: 'group_rule',
status: 'ACTIVE',
created: '2026-01-01T00:00:00.000Z',
lastUpdated: '2026-01-02T00:00:00.000Z',
conditions: {
expression: { value: 'user.department == "Eng"', type: 'urn:okta:expression:1.0' },
people: { users: { exclude: ['00u1'] }, groups: { exclude: ['00g1'] } },
},
actions: { assignUserToGroups: { groupIds: ['00g2'] } },
})
expect(mapped).toMatchObject({
id: '0pr1',
expression: 'user.department == "Eng"',
expressionType: 'urn:okta:expression:1.0',
assignUserToGroupIds: ['00g2'],
excludedUserIds: ['00u1'],
excludedGroupIds: ['00g1'],
})
})
it('defaults absent nested conditions instead of throwing', () => {
expect(
mapOktaGroupRule({ id: '0pr2', name: 'Bare', type: 'group_rule', status: 'INACTIVE' })
).toMatchObject({
created: null,
lastUpdated: null,
expression: null,
expressionType: null,
assignUserToGroupIds: [],
excludedUserIds: [],
excludedGroupIds: [],
})
})
})
+90
View File
@@ -0,0 +1,90 @@
import type { Logger } from '@sim/logger'
import type { OktaApiError, OktaGroupRule } from '@/tools/okta/types'
/**
* Standard headers for every Okta Management API request.
*
* Okta authenticates API tokens with the `SSWS` scheme rather than `Bearer`.
*/
export function oktaHeaders(apiKey: string): Record<string, string> {
return {
Authorization: `SSWS ${apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}
}
/**
* Reads an Okta error body and throws a `ToolResponse`-friendly `Error`.
*
* Okta returns `errorSummary` on failure, but lifecycle endpoints answer with an
* empty body, so the JSON parse is best-effort and falls back to the caller's
* message.
*/
export async function throwOktaError(
response: Response,
logger: Logger,
fallbackMessage: string
): Promise<never> {
let error: OktaApiError = {}
try {
error = await response.json()
} catch {
// Lifecycle endpoints answer with an empty or non-JSON body
}
logger.error('Okta API request failed', { data: error, status: response.status })
throw new Error(error.errorSummary || fallbackMessage)
}
const NEXT_LINK_PATTERN = /<([^>]+)>\s*;\s*rel="next"/i
/**
* Extracts Okta's cursor pagination state from the `Link` response header.
*
* Okta paginates with an opaque `after` cursor advertised in a `Link` header
* entry marked `rel="next"` never in the response body, which is a bare JSON
* array. The absence of that entry is what marks the final page.
*/
export function parseOktaPagination(response: Response): {
nextCursor: string | null
hasMore: boolean
} {
const linkHeader = response.headers.get('Link')
if (!linkHeader) return { nextCursor: null, hasMore: false }
const nextMatch = linkHeader.match(NEXT_LINK_PATTERN)
if (!nextMatch) return { nextCursor: null, hasMore: false }
let nextCursor: string | null = null
try {
nextCursor = new URL(nextMatch[1]).searchParams.get('after')
} catch {
// Malformed next link — report more results without a usable cursor
}
return { nextCursor, hasMore: true }
}
/**
* Flattens a group rule into the shape the list, get, and create tools all emit.
*
* The API nests the driving expression and the target groups several levels
* deep, which is awkward to reference from a workflow, so the fields callers act
* on are lifted to the top level. Okta documents `exclude` lists only there is
* no `include` counterpart on either people condition.
*/
export function mapOktaGroupRule(rule: OktaGroupRule) {
return {
id: rule.id,
name: rule.name,
type: rule.type,
status: rule.status,
created: rule.created ?? null,
lastUpdated: rule.lastUpdated ?? null,
expression: rule.conditions?.expression?.value ?? null,
expressionType: rule.conditions?.expression?.type ?? null,
assignUserToGroupIds: rule.actions?.assignUserToGroups?.groupIds ?? [],
excludedUserIds: rule.conditions?.people?.users?.exclude ?? [],
excludedGroupIds: rule.conditions?.people?.groups?.exclude ?? [],
}
}
+52
View File
@@ -2868,20 +2868,46 @@ import {
obsidianSearchTool,
} from '@/tools/obsidian'
import {
oktaActivateGroupRuleTool,
oktaActivateUserTool,
oktaAddUserToGroupTool,
oktaAssignGroupToAppTool,
oktaAssignUserRoleTool,
oktaAssignUserToAppTool,
oktaClearUserSessionsTool,
oktaCreateGroupRuleTool,
oktaCreateGroupTool,
oktaCreateUserTool,
oktaDeactivateGroupRuleTool,
oktaDeactivateUserTool,
oktaDeleteGroupRuleTool,
oktaDeleteGroupTool,
oktaDeleteUserTool,
oktaEnrollFactorTool,
oktaGetAppTool,
oktaGetFactorTool,
oktaGetGroupRuleTool,
oktaGetGroupTool,
oktaGetLogsTool,
oktaGetSessionTool,
oktaGetUserTool,
oktaListAppGroupsTool,
oktaListAppsTool,
oktaListAppUsersTool,
oktaListFactorsTool,
oktaListGroupMembersTool,
oktaListGroupRulesTool,
oktaListGroupsTool,
oktaListUserRolesTool,
oktaListUsersTool,
oktaRemoveGroupFromAppTool,
oktaRemoveUserFromAppTool,
oktaRemoveUserFromGroupTool,
oktaRemoveUserRoleTool,
oktaResetAllFactorsTool,
oktaResetFactorTool,
oktaResetPasswordTool,
oktaRevokeSessionTool,
oktaSuspendUserTool,
oktaUnsuspendUserTool,
oktaUpdateGroupTool,
@@ -6371,6 +6397,32 @@ export const tools: Record<string, ToolConfig> = {
okta_add_user_to_group: oktaAddUserToGroupTool,
okta_remove_user_from_group: oktaRemoveUserFromGroupTool,
okta_list_group_members: oktaListGroupMembersTool,
okta_get_logs: oktaGetLogsTool,
okta_clear_user_sessions: oktaClearUserSessionsTool,
okta_get_session: oktaGetSessionTool,
okta_revoke_session: oktaRevokeSessionTool,
okta_list_factors: oktaListFactorsTool,
okta_get_factor: oktaGetFactorTool,
okta_enroll_factor: oktaEnrollFactorTool,
okta_reset_factor: oktaResetFactorTool,
okta_reset_all_factors: oktaResetAllFactorsTool,
okta_list_apps: oktaListAppsTool,
okta_get_app: oktaGetAppTool,
okta_list_app_users: oktaListAppUsersTool,
okta_assign_user_to_app: oktaAssignUserToAppTool,
okta_remove_user_from_app: oktaRemoveUserFromAppTool,
okta_list_app_groups: oktaListAppGroupsTool,
okta_assign_group_to_app: oktaAssignGroupToAppTool,
okta_remove_group_from_app: oktaRemoveGroupFromAppTool,
okta_list_user_roles: oktaListUserRolesTool,
okta_assign_user_role: oktaAssignUserRoleTool,
okta_remove_user_role: oktaRemoveUserRoleTool,
okta_list_group_rules: oktaListGroupRulesTool,
okta_get_group_rule: oktaGetGroupRuleTool,
okta_create_group_rule: oktaCreateGroupRuleTool,
okta_activate_group_rule: oktaActivateGroupRuleTool,
okta_deactivate_group_rule: oktaDeactivateGroupRuleTool,
okta_delete_group_rule: oktaDeleteGroupRuleTool,
onepassword_list_vaults: onepasswordListVaultsTool,
onepassword_get_vault: onepasswordGetVaultTool,
onepassword_list_items: onepasswordListItemsTool,