> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deploybaseline.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Okta

> OAuth 2.0 and OIDC, authorization servers, users, groups, apps.

Okta identity provider emulation with OAuth 2.0 / OIDC, user management, groups, apps, and authorization servers. Supports both default org server and custom authorization server paths.

## At a glance

| | |
| - | - |
| Package | `@deploybaseline/okta` |
| Port | `4006` in the default set, `4000` when started alone |
| Config key | `okta` |
| HTTP operations | 86, browsable at `http://localhost:3999/docs/okta` while it runs ([API reference](/api-reference.md)) |

```bash CLI theme={null}
npx @deploybaseline/cli start --service okta
```

```ts Programmatic theme={null}
import { createEmulator } from "@deploybaseline/cli";

const okta = await createEmulator({ service: "okta" });
// okta.url is http://localhost:4000
```

```json baseline.config.json theme={null}
{
  "okta": {
    "users": [
      {
        "login": "testuser@okta.local",
        "email": "testuser@okta.local",
        "first_name": "Test",
        "last_name": "User"
      }
    ],
    "groups": [
      {
        "name": "Everyone",
        "description": "All users",
        "type": "BUILT_IN",
        "okta_id": "00g_everyone"
      }
    ],
    "authorization_servers": [
      {
        "id": "default",
        "name": "default",
        "audiences": ["api://default"]
      }
    ],
    "oauth_clients": [
      {
        "client_id": "okta-test-client",
        "client_secret": "okta-test-secret",
        "name": "Sample OIDC Client",
        "redirect_uris": ["http://localhost:3000/callback"],
        "auth_server_id": "default"
      }
    ]
  }
}
```

## Start

```bash theme={null}
npx @deploybaseline/cli start --service okta
```

Okta listens on port `4006` in the default set, or on `http://localhost:4000` when started alone. Without a seed config there is one user (`testuser@okta.local`), the built-in `Everyone` group, the `default` authorization server, a confidential OAuth client (`okta-test-client`, secret `okta-test-secret`, redirect URI `http://localhost:3000/callback`), a public PKCE client (`okta-test-app`), and one OIDC app (Sample OIDC App).

The `@okta/okta-sdk-nodejs` client works unchanged with `orgUrl` set to the emulator:

```ts theme={null}
import okta from "@okta/okta-sdk-nodejs";

const client = new okta.Client({ orgUrl: process.env.OKTA_EMULATOR_URL, token: "any-token" });
```

## Auth

Every `/api/v1/*` route needs `Authorization: SSWS <token>` (or `Bearer`); a missing or unknown token is `401 E0000011` ("Invalid token provided"). From the CLI any non-empty token works.

OAuth for Okta: the Management API and `/oauth2/v1/clients` also take `Authorization: Bearer <access token>` from the org authorization server (`/oauth2/v1/token`) when the token has the scope for the request: `okta.<resource>.read` or `okta.<resource>.manage` for `GET`, `okta.<resource>.manage` otherwise, for `users`, `groups`, `apps`, `authorizationServers`, and `clients`, and `okta.clients.register` for registering a client. A missing scope is `403 E0000006` with `WWW-Authenticate: Bearer error="insufficient_scope"`. Tokens from custom authorization servers are for your own APIs and get `401 E0000011`.

## Management API conventions

Lists use Okta's cursor pagination: `limit` and `after`. Each response carries a `Link` header with `rel="self"` and, while more remain, `rel="next"`, whose URL holds the cursor; the SDK's collections follow it. Default and maximum page sizes follow Okta (users 200, groups 10000, group members 1000, apps 20 of up to 200, app users 50 of up to 500, authorization servers 200).

`search` and `filter` take Okta's expression language: `eq`, `ne`, `gt`, `ge`, `lt`, `le`, `sw`, `co`, `ew`, and `pr`, joined with `and`, `or`, `not`, and parentheses, with quoted values (`profile.email eq "a@example.com" and status eq "ACTIVE"`). String comparisons ignore case. A malformed expression or an attribute Okta does not allow there is `400 E0000031`. `q` matches the start of a name.

Errors use Okta's shape (`errorCode`, `errorSummary`, `errorLink`, `errorId`, `errorCauses`). A missing resource is `404 E0000007` with Okta's summary (`Not found: Resource not found: <id> (User)`), and a validation failure is `400 E0000001` (`Api validation failed: login`) with the field in `errorCauses`.

## OAuth and OIDC

Default org server and custom authorization server paths (`/oauth2/:authServerId/...`):

* `GET /.well-known/openid-configuration` — Get OIDC discovery document: default server
* `GET /oauth2/:authServerId/.well-known/openid-configuration` — Get server discovery document: per authorization server
* `GET /oauth2/v1/keys` — Get JSON Web Key Set (JWKS)
* `GET /oauth2/v1/authorize` — Authorize
* `POST /oauth2/v1/token` — Exchange token
* `GET /oauth2/v1/userinfo` — Get user info
* `POST /oauth2/v1/revoke` — Revoke token
* `POST /oauth2/v1/introspect` — Introspect token
* `GET /oauth2/v1/logout` — End session

`response_mode` may be `query` (default), `fragment` (the `code` and `state` come back after `#`), or `form_post`. A `refresh_token` is issued only when the granted scope includes `offline_access` and the client has the `refresh_token` grant type, as on Okta. Each refresh rotates it. A `scope` parameter may narrow the new access token; asking for a scope outside the original grant is `400 invalid_scope`, and the rotated refresh token keeps the original grant. A client without the `refresh_token` grant gets `unauthorized_client`.

### Signing key

ID tokens are signed with a key stored in emulator state (`kid`: `baseline-okta-1`), so they stay valid across snapshot restores and persisted restarts. Set `okta.signing_key.private_jwk` to an RSA private JWK to pin the key. See [OIDC signing keys](/configuration.md#oidc-signing-keys).

## Client registration

Okta's Client Application API, the `registration_endpoint` every discovery document lists (`/oauth2/v1/clients`, at the org level for every server). It needs a management token (`Authorization: SSWS ...`) or an org server access token with `okta.clients.register` (to register) or `okta.clients.manage` (see [Auth](#auth)).

* `POST /oauth2/v1/clients` — Register client: RFC 7591. `client_name` (required), `redirect_uris`, `application_type` (`web`, `native`, `browser`, `service`), `grant_types` (`authorization_code`, `refresh_token`, `client_credentials`), `response_types` (`code`), `token_endpoint_auth_method` (`client_secret_basic`, `client_secret_post`, `none`). Returns `201` with `client_id` and, for confidential clients, `client_secret` (`client_secret_expires_at: 0`)
* `GET /oauth2/v1/clients` — List clients: `q` matches the start of the name, `limit`
* `GET /oauth2/v1/clients/:clientId` — Get client (without the secret)
* `PUT /oauth2/v1/clients/:clientId` — Replace client metadata
* `DELETE /oauth2/v1/clients/:clientId` — Delete client
* `POST /oauth2/v1/clients/:clientId/lifecycle/newSecret` — Rotate client secret

Defaults follow Okta: `web` clients use `client_secret_basic` and `authorization_code`, `browser` and `native` clients are public (`none`), and `service` clients use `client_credentials`. Bad metadata is `400 invalid_client_metadata`; grant types the emulator does not run (`implicit`, `password`, device code) are refused the same way. A registered client can sign in through any authorization server.

## Users

* `GET /api/v1/users` — List users: `search` takes any attribute (`id`, `status`, `created`, `activated`, `statusChanged`, `lastLogin`, `lastUpdated`, `passwordChanged`, `type.id`, `profile.*`) with `sortBy` and `sortOrder`; `filter` takes `id`, `status`, `lastUpdated`, `profile.login`, `profile.email`, `profile.firstName`, and `profile.lastName`; `q` matches the start of the first name, last name, or email. Without `search` or `filter`, deprovisioned users are left out, as on Okta
* `POST /api/v1/users` — Create user: `profile.login`, `profile.email`, `profile.firstName`, and `profile.lastName` are required; login and email must look like email addresses and logins are unique (emails need not be). Other profile attributes (`mobilePhone`, custom attributes) are kept. Optional `credentials.password.value` and `groupIds`. `?activate=false` creates a `STAGED` user, `?nextLogin=changePassword` a `PASSWORD_EXPIRED` one. New users join the built-in Everyone group
* `GET /api/v1/users/me` — Get current user: the token's user
* `GET /api/v1/users/:userId` — Get user: by ID, login, or email
* `POST /api/v1/users/:userId` — Update user: partial update, only the profile attributes sent change, and `null` clears one. `credentials.password` sets a password
* `PUT /api/v1/users/:userId` — Replace user: profile attributes left out are cleared, and the required ones must be sent
* `DELETE /api/v1/users/:userId` — Delete user: the first call deactivates, the second deletes
* `GET /api/v1/users/:userId/groups` — List user groups
* `POST /api/v1/users/:userId/lifecycle/activate` — Activate user: from `STAGED`, `PROVISIONED`, or `DEPROVISIONED` to `ACTIVE`; an active user is `403 E0000016`. Answers `{}`, or `{ activationUrl, activationToken }` with `sendEmail=false` (the URL is not served)
* `POST /api/v1/users/:userId/lifecycle/reactivate` — Reactivate user: only for `PROVISIONED` users; answers like activate
* `POST /api/v1/users/:userId/lifecycle/deactivate` — Deactivate user: to `DEPROVISIONED`, removing the user's app assignments
* `POST /api/v1/users/:userId/lifecycle/suspend` — Suspend user: `ACTIVE` to `SUSPENDED` (`400 E0000001` otherwise)
* `POST /api/v1/users/:userId/lifecycle/unsuspend` — Unsuspend user: `SUSPENDED` to `ACTIVE` (`400 E0000001` otherwise)
* `POST /api/v1/users/:userId/lifecycle/unlock` — Unlock user: `LOCKED_OUT` to `ACTIVE` (`403 E0000032` otherwise)
* `POST /api/v1/users/:userId/lifecycle/expire_password` — Expire password: to `PASSWORD_EXPIRED`; returns the user
* `POST /api/v1/users/:userId/credentials/change_password` — Change password: `oldPassword` and `newPassword`; a wrong old password is `403 E0000014`

Other status changes Okta forbids are `403 E0000038`. Only `ACTIVE` and `PASSWORD_EXPIRED` users appear on the sign-in page, and a suspended or deactivated user's codes and refresh tokens stop working.

## Groups

* `GET /api/v1/groups` — List groups: `q`, `search` on `id`, `type`, `created`, `lastUpdated`, `lastMembershipUpdated`, `profile.name`, `profile.description`; `filter` on `id`, `type`, `lastUpdated`, `lastMembershipUpdated`
* `POST /api/v1/groups` — Create group (`profile.name` required and unique)
* `GET /api/v1/groups/:groupId` — Get group
* `PUT /api/v1/groups/:groupId` — Replace group profile
* `DELETE /api/v1/groups/:groupId` — Delete group: with its memberships and app assignments
* `GET /api/v1/groups/:groupId/users` — List group members
* `PUT /api/v1/groups/:groupId/users/:userId` — Add group member (updates `lastMembershipUpdated`)
* `DELETE /api/v1/groups/:groupId/users/:userId` — Remove group member
* `GET /api/v1/groups/:groupId/apps` — List group apps: apps assigned to the group

## Apps

* `GET /api/v1/apps` — List apps: `q` on name or label; `filter` on `status`, `name`, `user.id`, `group.id`
* `POST /api/v1/apps` — Create app (`label` and `signOnMode` required; `?activate=false` creates it `INACTIVE`)
* `GET /api/v1/apps/:appId` — Get app
* `PUT /api/v1/apps/:appId` — Update app
* `DELETE /api/v1/apps/:appId` — Delete app: deletes an `INACTIVE` app
* `POST /api/v1/apps/:appId/lifecycle/activate` — Activate app (answers `{}`)
* `POST /api/v1/apps/:appId/lifecycle/deactivate` — Deactivate app (answers `{}`)
* `GET /api/v1/apps/:appId/users` — List app users: direct (`scope: "USER"`) and through groups (`scope: "GROUP"`)
* `POST /api/v1/apps/:appId/users` — Assign user to app (`id`, optional app `profile`)
* `GET /api/v1/apps/:appId/users/:userId` — Get app user
* `POST /api/v1/apps/:appId/users/:userId` — Update app user profile
* `DELETE /api/v1/apps/:appId/users/:userId` — Unassign user from app
* `GET /api/v1/apps/:appId/groups` — List app group assignments: by `priority`
* `PUT /api/v1/apps/:appId/groups/:groupId` — Assign group to app (`priority`, `profile`)
* `GET /api/v1/apps/:appId/groups/:groupId` — Get app group assignment
* `DELETE /api/v1/apps/:appId/groups/:groupId` — Remove app group assignment

An `OPENID_CONNECT` app is an OAuth client, as on Okta. `settings.oauthClient` (`redirect_uris`, `grant_types`, `response_types`, `application_type`) is required and validated like client registration, and `credentials.oauthClient` may set `token_endpoint_auth_method` and a custom `client_id`. The response's `credentials.oauthClient` carries the `client_id` (the app ID unless one was given) and `client_secret`. The client signs in through any authorization server while the app is active; deactivating the app makes it `invalid_client`, and deleting the app removes the client. A client registered at `/oauth2/v1/clients` is listed as an app too, and deleting either removes both.

## Authorization servers

* `GET /api/v1/authorizationServers` — List authorization servers (`q`, `limit`, `after`)
* `POST /api/v1/authorizationServers` — Create authorization server (`name`, optional `id` and `audiences`; `201`)
* `GET /api/v1/authorizationServers/:authServerId` — Get authorization server
* `PUT /api/v1/authorizationServers/:authServerId` — Update authorization server
* `DELETE /api/v1/authorizationServers/:authServerId` — Delete authorization server
* `POST /api/v1/authorizationServers/:authServerId/lifecycle/activate` — Activate authorization server (`204`)
* `POST /api/v1/authorizationServers/:authServerId/lifecycle/deactivate` — Deactivate authorization server (`204`)

## Access policies

Each custom authorization server has access policies that decide which requests get tokens and how long the tokens last (`OAUTH_AUTHORIZATION_POLICY` policies with `RESOURCE_ACCESS` rules, as in the `@okta/okta-sdk-nodejs` `authorizationServerApi`):

* `GET|POST /api/v1/authorizationServers/:authServerId/policies`, `GET|PUT|DELETE .../policies/:policyId`, `POST .../policies/:policyId/lifecycle/activate|deactivate`
* `GET|POST .../policies/:policyId/rules`, `GET|PUT|DELETE .../rules/:ruleId`, `POST .../rules/:ruleId/lifecycle/activate|deactivate`

A policy names its clients (`conditions.clients.include`, client IDs or `ALL_CLIENTS`). A rule matches grant types (`conditions.grantTypes.include`), people (`conditions.people.users.include` user IDs, `conditions.people.groups.include` group IDs or `EVERYONE`), and scopes (`conditions.scopes.include`, or `*`), and sets `actions.token.accessTokenLifetimeMinutes` (5 to 1440, default 60), `refreshTokenLifetimeMinutes` (0 is unlimited), and `refreshTokenWindowMinutes` (default 10080, seven days). Priorities stay 1 to n; creating or moving one to a taken priority shifts the rest down.

For each authorize, token, and refresh request, the first active policy by priority that names the client is used; its first active rule that matches the grant type, the user (client credentials match only `EVERYONE`), and every requested scope sets the lifetimes. No match is `access_denied` with "Policy evaluation failed for this request, please check the policy configurations." (a redirect from `/authorize`, `400` from `/token`). Refresh tokens expire after the rule's lifetime, or after the window passes without use.

The `default` server and seeded servers start with Okta's **Default Policy** (all clients) and **Default Policy Rule** (everyone, any scope, one hour access tokens). A server created with `POST /api/v1/authorizationServers` starts with no policies, so it issues no tokens until you add one, as on Okta. Seed `policies` on a server to set its own (rules may name groups and users by name or login). The org server (`/oauth2/v1`) has no access policies.

## Seed config

```json theme={null}
{
  "okta": {
    "users": [
      {
        "login": "testuser@okta.local",
        "email": "testuser@okta.local",
        "first_name": "Test",
        "last_name": "User"
      }
    ],
    "groups": [
      {
        "name": "Everyone",
        "description": "All users",
        "type": "BUILT_IN",
        "okta_id": "00g_everyone"
      }
    ],
    "apps": [
      {
        "name": "My App",
        "label": "My App"
      }
    ],
    "authorization_servers": [
      {
        "id": "default",
        "name": "default",
        "audiences": ["api://default"]
      }
    ],
    "oauth_clients": [
      {
        "client_id": "okta-test-client",
        "client_secret": "okta-test-secret",
        "name": "Sample OIDC Client",
        "redirect_uris": ["http://localhost:3000/callback"],
        "auth_server_id": "default"
      }
    ]
  }
}
```

Keys are snake\_case. An entry that matches an existing record (by `login`, group `name`, app `name`, `client_id`, or server `id`), including the default `testuser@okta.local`, `Everyone`, `okta-test-client`, and `okta-test-app`, updates that record with the fields it sets.

Users take `okta_id`, `status`, `display_name`, `locale`, and `time_zone`; groups take `okta_id` and `type`; apps take `okta_id`, `status`, `sign_on_mode`, `settings`, and `credentials`. OAuth clients take `response_types`, `grant_types`, and `token_endpoint_auth_method`. Authorization servers take `description`, `status`, and `policies` (see [Access policies](#access-policies)). `group_memberships` (`group_okta_id`, `user_okta_id`) and `app_assignments` (`app_okta_id`, `user_okta_id`) link seeded records, and `signing_key` pins the ID token key (see [Signing key](#signing-key)).

## Conformance

| | |
| - | - |
| Routes | 86 supported or partial, 0 planned |
| Route gate | fail |
| Spec gate | no public spec |
| SDKs tested | `@okta/okta-sdk-nodejs@8.2.0`, `jose@6.2.12`, `openid-client@6.8.8` |
| Dual mode | not yet |

See [CONFORMANCE.md](https://github.com/deploybaseline/cli/blob/main/CONFORMANCE.md) for every provider.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.