# Migrate from Okta (https://docs.idra.zone/docs/migrate-from-okta)

What maps from Okta to IdRa and what doesn't, exporting users, groups and apps, issuer changes, cutover and rollback.

IdRa covers much of what a typical Okta workforce org uses, and its `/api/v1` API follows Okta's, but it isn't a drop-in replacement. Check the gaps below against what you use before you plan a date. Move app by app, and keep Okta running until the last app has moved.

## What maps directly

| Okta | IdRa | Notes |
| --- | --- | --- |
| Users and lifecycle states | **Directory → People** | The login must equal the email. |
| Groups, group rules | **Directory → Groups** | Rules use a [subset of the expression language](https://docs.idra.zone/docs/admin/expressions). |
| OIDC web, SPA, native, service apps | **Applications** | New client IDs and secrets. |
| SAML 2.0 apps | **Applications** | New IdP metadata and certificate. |
| Bookmark apps | **Applications** | |
| Authorization servers, scopes, claims | **Security → API** | One shared issuer, told apart by `aud`. |
| Global session and app sign-on policies | **Security → Global Session Policy, Authentication Policies** | Rebuild them; see the API table. |
| Network zones, password policies | **Security → Networks, Password Policies** | |
| Authenticators: TOTP, passkeys, SMS and voice, security question, YubiKey OTP, Duo | **Security → Authenticators** | People enroll again. |
| Okta Verify push | IdRa Verify | A web app with number challenge, not a native app. |
| System Log, event hooks | **Reports → System Log**, **Workflow → Event Hooks** | Event types are IdRa's own names. |
| SCIM provisioning to apps | App → **Provisioning** | Create, update, deactivate. No app imports or password sync. |

## What doesn't

These come from IdRa's own audit against Okta, dated 2026-10-30. If you rely on one, plan a replacement or wait.

| Area | Missing in IdRa today |
| --- | --- |
| Passwords | No password or hash import, and no password import hook. **Everyone sets a new password.** |
| MFA enrollments | Not portable. Everyone enrolls again. |
| Directories | No AD or LDAP agent, Desktop SSO, LDAP interface or RADIUS. |
| Sign-on types | No WS-Federation (commonly used for Office 365) and no SWA password-vaulted apps. |
| Device-bound passwordless | No FastPass equivalent or native Verify app. Passkeys are the nearest option. |
| SAML | No Single Logout or assertion encryption. One signing key per organization. |
| OAuth | No implicit or password grants, token exchange, PAR, JAR or wildcard redirects. |
| Directory model | No user types or linked objects. One global profile schema. |
| Admin | Five fixed roles. No custom roles or resource sets. |
| Automation | No inline hooks, log streaming or no-code workflows. Event hooks are the automation point. |
| Catalog | A small catalog of SAML presets, not thousands of integrations. Most apps are set up by hand. |
| Federation | [Linking existing people](https://docs.idra.zone/docs/admin/identity-providers#account-linking) to an external IdP is opt-in per provider and domain, and never applies to administrators, so set it up before they sign in through the IdP. |
| Language | The UI and emails are English only. |

## API path compatibility

Checked by calling a running IdRa with an `SSWS` token. Create the token under **Security → API → Tokens**.

| Path under `/api/v1` | Status | Differences |
| --- | --- | --- |
| `/users`, `/users/me`, lifecycle, `/users/{id}/groups` | Works | `login` must equal `email`. Profile keys must exist in the Profile Editor, or `400 E0000001`. |
| `/groups`, `/groups/{id}/users`, `/groups/rules` | Works | One target group per rule. |
| `/apps`, `/apps/{id}/users`, `/apps/{id}/groups` | Works | `BOOKMARK`, `OPENID_CONNECT` and `SAML_2_0` only. Unknown fields are rejected, so you can't replay Okta's JSON unchanged. |
| `/logs`, `/zones`, `/eventHooks`, `/authorizationServers` | Works | See [Differences from Okta](https://docs.idra.zone/docs/admin-api#differences-from-okta). |
| `/policies` | Works | Only `PASSWORD` can be created. |
| `/idps`, `/authenticators`, `/meta/schemas/*`, `/meta/types/*`, `/sessions`, `/domains`, `/brands`, `/inlineHooks`, `/logStreams`, `/iam/*`, `/org`, `/features`, `/devices`, `/trustedOrigins`, `/templates/*`, `/threats/configuration`, `/behaviors`, `/apps/{id}/credentials/keys`, `/apps/{id}/connections/*`, `/apps/{id}/features` | `404 E0000007` | Not implemented. |

On 2026-10-31, IdRa answered 121 of the 815 operations in Okta's Node SDK, so test your own scripts. The [API reference](https://docs.idra.zone/docs/api-reference) lists them. The [Terraform provider](https://registry.terraform.io/providers/okta/okta) `okta/okta` 4.20 has been run against IdRa with SSWS tokens for users, groups, group rules, OAuth and bookmark apps, assignments, authorization servers, zones, event hooks and password policies. Its OAuth mode doesn't work, because it calls Okta's token path.

## Okta SDK compatibility

We ran these calls on Node.js against a local IdRa on 2026-10-31. This is a sample of calls, not a full list, and the hosted service wasn't part of the run.

| SDK | Changed | Calls that passed | Calls that failed |
| --- | --- | --- | --- |
| `@okta/okta-sdk-nodejs` 8.2.0 | Org URL, SSWS token | Create, list, get, update, deactivate and delete users; list a user's factors; create, list and delete groups; add, list and remove group members; create, list, get, deactivate and delete OIDC apps; list sign-on policies, network zones, System Log events, event hooks and authorization servers | `listIdentityProviders`, `getOrgSettings` and `listCustomDomains` return 404 |
| `@okta/okta-auth-js` 8.0.1 | Issuer only | Discovery with `getWellKnown`; `exchangeCodeForTokens`; `token.verify`; `token.getUserInfo`; `token.revoke` | `token.introspect` with a client that has no secret; `signInWithCredentials`; `session.exists` returned false |
| `@okta/jwt-verifier` 4.0.2 | Issuer only | `verifyIdToken`; `verifyAccessToken` on a JWT access token from a sign-in that requested an API resource, including refusing a wrong audience | `verifyAccessToken` on the opaque access token from a sign-in with no API resource. `http` issuers are refused unless the SDK's test flag is set, which the local run used |

The `okta-auth-js` run was in Node, with codes from a browser sign-in at the SDK's authorize URL. `getWithRedirect` and `signOut` weren't run.

IdRa access tokens are opaque unless the sign-in requests an API resource. To use `verifyAccessToken`, create the API under **Security → API**, link the client, add an access rule, and send `resource=<identifier>` with one of the API's scopes. Verify against the identifier as the audience.

## Issuer and endpoint changes

Every authorization server in an IdRa organization shares one issuer, so update every app. Okta's paths at the root of the domain and under `/oauth2/default` return 404 on IdRa. Under the issuer, `/oauth2/v1/authorize`, `token`, `userinfo`, `revoke`, `introspect`, `logout` and `keys`, and `/v1/keys`, run the endpoints below, because the Okta SDKs build those URLs from the issuer. Discovery lists the endpoints below.

| Okta | IdRa |
| --- | --- |
| Issuer `https://acme.okta.com` or `https://acme.okta.com/oauth2/default` | `https://<your-org>.idra.team/api/auth` |
| `/.well-known/openid-configuration` under the issuer | `/api/auth/.well-known/openid-configuration` |
| `/oauth2/v1/authorize`, `/oauth2/default/v1/authorize` | `/api/auth/oauth2/authorize` |
| `.../v1/token` | `/api/auth/oauth2/token` |
| `.../v1/userinfo` | `/api/auth/oauth2/userinfo` |
| `.../v1/keys` | `/api/auth/jwks` |
| `.../v1/introspect` | `/api/auth/oauth2/introspect` |
| `.../v1/revoke` | `/api/auth/oauth2/revoke` |
| `.../v1/logout` | `/api/auth/oauth2/end-session` |

Most libraries only need the new issuer, client ID and secret. Also check these, because each one can break an app that worked on Okta:

- **Redirect URIs:** matched exactly, with no wildcards. Web and Single-Page apps need `https`, except for local development: `http` on `localhost`, `127.0.0.1` or `[::1]` with a port, and PKCE on.
- **Claims:** `email` and `name` are in UserInfo, not the ID token. If an app reads them from the ID token, change it.
- **Groups:** request the `groups` scope. The claim has every group name, including `Everyone`, with no filter.
- **`sub`:** IdRa issues new user IDs, so `sub` changes. Apps keyed on `sub` must match on email first, or they create duplicate accounts.
- **APIs that validate tokens:** update the expected issuer, JWKS URL and audience.
- **SAML apps:** give the service provider IdRa's metadata from `/sso/saml/<appId>/metadata`. The entity ID, SSO URL and certificate all change. Set the NameID to what the app already expects.

## Export and import

### Users and groups

The script reads Okta's API and writes to IdRa's. It creates people as **Staged**, so nobody is emailed yet. Run it once on an organization without these groups: it doesn't merge with existing ones, and people who already exist are reported as failures.

```js title="okta-to-idra.mjs"
// Copies groups, users and memberships. Creates users as STAGED, so nobody is emailed yet.
const { OKTA_URL, OKTA_TOKEN, IDRA_URL, IDRA_TOKEN } = process.env;

async function call(base, token, path, init = {}) {
  const res = await fetch(new URL(path, base), {
    ...init,
    headers: {
      authorization: `SSWS ${token}`,
      accept: 'application/json',
      'content-type': 'application/json',
    },
  });
  if (res.status === 429) {
    const reset = Number(res.headers.get('x-rate-limit-reset')) * 1000;
    await new Promise((r) => setTimeout(r, Math.max(reset - Date.now(), 1000)));
    return call(base, token, path, init);
  }
  return res;
}

// Follows the Link: rel="next" header.
async function* all(path) {
  for (let next = path; next; ) {
    const res = await call(OKTA_URL, OKTA_TOKEN, next);
    if (!res.ok) throw new Error(`${next}: ${res.status} ${await res.text()}`);
    yield* await res.json();
    next = res.headers.get('link')?.match(/<([^>]+)>;\s*rel="next"/)?.[1];
  }
}

const post = async (path, body) => {
  const res = await call(IDRA_URL, IDRA_TOKEN, path, { method: 'POST', body: JSON.stringify(body) });
  const json = await res.json();
  return res.ok ? json : Promise.reject(new Error(JSON.stringify(json.errorCauses ?? json)));
};

// Okta group id -> IdRa group id. OKTA_GROUP only: Everyone is built in, app groups stay with their app.
const groups = new Map();
for await (const g of all('/api/v1/groups?limit=200')) {
  if (g.type !== 'OKTA_GROUP') continue;
  const { name, description } = g.profile;
  groups.set(g.id, (await post('/api/v1/groups', { profile: { name, description } })).id);
}

const failed = [];
for await (const u of all('/api/v1/users?limit=200')) {
  const { firstName, lastName, email, login } = u.profile;
  try {
    // Add custom attributes here, once they exist in IdRa's Profile Editor.
    const { id } = await post('/api/v1/users?activate=false', {
      profile: { firstName, lastName, email, login },
    });
    for await (const g of all(`/api/v1/users/${u.id}/groups`)) {
      if (groups.has(g.id))
        await call(IDRA_URL, IDRA_TOKEN, `/api/v1/groups/${groups.get(g.id)}/users/${id}`, { method: 'PUT' });
    }
  } catch (e) {
    failed.push({ login, error: e.message });
  }
}
console.log(`${groups.size} groups copied, ${failed.length} users failed`);
if (failed.length) console.table(failed);
```

```sh
OKTA_URL=https://acme.okta.com OKTA_TOKEN=... \
IDRA_URL=https://<your-org>.idra.team IDRA_TOKEN=idra_... \
node okta-to-idra.mjs
```

Before running it:

- Add custom attributes in **Directory → Profile Editor**, then add them to the script.
- Find logins that differ from the email address. IdRa refuses them, and the script lists them as failures. Decide how those people will sign in before you continue.
- Any status comes across as **Staged**, including Okta's suspended people. Review the list, because the activation step below includes everyone Staged.
- Password hashes aren't imported. IdRa ignores a `credentials.password.hash` without an error, so don't send one.

For a one-off, **Directory → People → Import** takes a CSV of up to 10,000 rows, with a preview before anything is written. See [Import a CSV](https://docs.idra.zone/docs/admin/users#import-a-csv). Groups named in the file must already exist.

### Apps

Recreate apps one at a time. IdRa issues new client IDs and refuses `client_id` on create, so app configuration changes either way.

- **By hand:** **Applications → Create App Integration**, or a preset from the catalog.
- **By API:** read each app from Okta and `POST /api/v1/apps` with only the fields IdRa accepts. For an OIDC web app:

```sh
curl -X POST https://<your-org>.idra.team/api/v1/apps \
  -H "Authorization: SSWS $IDRA_TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "label": "Wiki",
    "name": "oidc_client",
    "signOnMode": "OPENID_CONNECT",
    "settings": { "oauthClient": {
      "application_type": "web",
      "redirect_uris": ["https://wiki.example.com/callback"],
      "grant_types": ["authorization_code"]
    } }
  }'
```

The response holds `credentials.oauthClient.client_id` and `client_secret`, which is the only time you see the secret. Through this API, `application_type` is `web`, `browser` or `service`. Create native apps in the console.

Assign with `PUT /api/v1/apps/{appId}/groups/{groupId}` or `POST /api/v1/apps/{appId}/users`. Assign an app before you switch it, or everyone gets `You are not assigned to this application`.

## Cutover

1. **Inventory.** List every app, policy, hook and API script in Okta. Check each against the tables above, and decide what happens to anything IdRa can't do.
2. **Build.** Set up zones, password policies, authenticators and authentication policies. Copy users and groups as above, and create and assign apps.
3. **Pilot.** Activate a small group with [the activation step](#activate-people), switch one low-risk app, and run a sign-in, sign-out and refused-access test on it. Check **Reports → System Log**.
4. **Lower token lifetimes in Okta** ahead of the move, so old tokens are gone soon after an app switches.
5. **Activate everyone.** People can't sign in to IdRa until they've activated, so do this before their apps switch. Activation links last 7 days.
6. **Switch apps in batches.** Change each app's issuer, client ID and secret, redirect URIs and token validation, and redeploy. Add SCIM on IdRa only after turning it off on Okta for that app: two sources writing to one app conflict.
7. **Point admin automation** at IdRa's `/api/v1`, with new tokens.
8. **Keep Okta for a while** after the last app, with its users left active and unchanged, then decommission it.

### Activate people

This emails every Staged person a link to set a password. Activation through the API always sends the email: `sendEmail=false` has no effect.

```js title="activate.mjs"
// Sends every STAGED person their activation email. Run it at cutover, not before.
const { IDRA_URL, IDRA_TOKEN } = process.env;
const headers = { authorization: `SSWS ${IDRA_TOKEN}`, accept: 'application/json' };

for (let next = '/api/v1/users?filter=status%20eq%20%22STAGED%22&limit=200'; next; ) {
  const res = await fetch(new URL(next, IDRA_URL), { headers });
  for (const u of await res.json()) {
    const r = await fetch(new URL(`/api/v1/users/${u.id}/lifecycle/activate`, IDRA_URL), { method: 'POST', headers });
    console.log(u.profile.login, r.status);
  }
  next = res.headers.get('link')?.match(/<([^>]+)>;\s*rel="next"/)?.[1];
}
```

## Rollback

Migration here is per app, so rollback is too, as long as Okta stays intact:

- **One app:** restore its Okta issuer, client ID and secret, and redeploy. Keep the old values in your secret store until decommission. People sign in to Okta as before.
- **SAML app:** restore the Okta metadata on the service provider.
- **Provisioning:** turn off SCIM on IdRa and back on in Okta. Reconcile accounts created or deactivated during the window, because IdRa doesn't sync them back to Okta.
- **People:** changes made in IdRa during the window, such as new hires and deactivations, don't reach Okta. Log them while you're in the window, and watch deactivations in particular: someone removed only in IdRa stays active in Okta.

Set a rollback deadline for each batch, and test the rollback on the pilot app before moving a critical one.