# Okta Cis To Descope

> Use this skill whenever anyone asks about migrating from Okta Customer Identity Service (CIS) to Descope — whether they're a developer doing it themselves or a technical lead evaluating the move. Triggers on: "how do I migrate from Okta", "replace Okta CIS with Descope", "we're moving off Okta", "Okta to Descope", "switch from Okta", "our app uses okta-auth-js / @okta/okta-react / @okta/okta-angular / @okta/oidc-middleware / okta-jwt-verifier and we want to use Descope instead", or any question about Okta CIS features (Sign-On Policies, Authorization Servers, Authenticators, Identity Providers, Log Streams, Service Apps, scp claim) in the context of Descope. Works for any language or framework with a Descope SDK. Always use this skill before producing migration guidance — do not rely on memory alone.

- Skill: `descope/okta-cis-to-descope` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add descope/okta-cis-to-descope`
- Raw SKILL.md: https://api.skillmd.com/api/skills/descope/okta-cis-to-descope/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: descope (https://skillmd.com/u/descope)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/descope/okta-cis-to-descope

---


# Okta CIS → Descope Migration Skill

This skill guides self-service migrations from Okta Customer Identity Service (CIS) to Descope.
It runs in three parts:

1. **MCP Check** — confirm whether the Descope Docs MCP is available and suggest installing it if not
2. **Migration Plan** — gather context via triage questions, analyze the codebase's auth touchpoints, and produce a human-readable `MIGRATION-PLAN.md` for the user to review
3. **Execution** — if the user confirms they want to proceed, execute the plan

Do not collapse these parts or skip ahead. The plan must be reviewed before code changes begin.

**Primary references** (all in this skill's directory):
- `references/implementation-nuances.md` — verified migration patterns for each JS/TS framework, Okta CIS feature-to-Descope mappings, and known gotchas
- `references/flows-and-widgets.md` — Descope terminology/lingo (Okta→Descope), Flow structure and templates, Widgets, SSO Setup Suite, Console-vs-code decision guide
- `references/backend-sdks.md` — Python and Java backend migration patterns (Flask, FastAPI, Django, Spring Boot, management SDK, M2M)

---

## Guiding Principles

**Console-first.** Before recommending SDK code for any user-facing auth feature, check whether the Console, a Flow, or a Widget covers the use case. Okta CIS is a low-code platform — users configure auth logic through the Okta Sign-In Widget, the visual policy builder (OIE), email customization, and the admin console. Descope has direct equivalents for all of these: Flows replace the visual policy builder, the Descope Flow component replaces the Sign-In Widget, Messaging Templates replace email customization, and Widgets replace custom management UIs. Engineers integrate once (SDK setup + session validation). All subsequent auth evolution — new methods, MFA changes, UI updates, branding — should happen in the Console without code deployments. See `references/flows-and-widgets.md` → Console vs. Code.

**Ask, don't assume.** At any design decision point — especially Inbound Apps vs. Federated Apps (the core Okta strategy fork), Flow vs. custom code, Widget vs. custom page, MFA inline vs. separate enrollment — use `AskUserQuestion` rather than proceeding with an assumption. The cost of a wrong assumption compounds across 20+ files. Always confirm whether the backend validates `scp` claims before recommending the Inbound Apps path.

**MCP over memory.** When the Docs MCP is available (confirmed in Part 1), use `ask-question-about-descope` to verify every SDK method name, option shape, and return type before writing it. Do not fall back to "verify the exact method name in the SDK type declarations" as a hedge — just verify it directly.

---

## Part 1: MCP Check (BLOCKING)

Before doing anything else, check whether the Descope Docs MCP is available by calling
`search-descope-docs` with a simple query (e.g., "session validation").

**If the tool is available:** proceed to Part 2 immediately.

**If the tool is not available**, show this message and use `AskUserQuestion` to ask whether
they want to install it first:

> **Descope Docs MCP is not installed.**
>
> This skill uses the Descope Docs MCP to look up current API signatures, SDK methods, and
> feature availability during migration. Without it, guidance is based on static training data,
> which may be stale and can produce SDK calls that don't exist.
>
> You can install it in a few minutes at **https://docs-mcp.descope.com/** (server URL:
> `https://docs-mcp.descope.com/mcp`). It significantly improves the accuracy of the
> migration output — especially for SDK lookups and flow-specific configuration.
>
> **Would you like to install the MCP before we continue, or proceed without it?**

- If they choose to install: pause and wait. Once they confirm it's installed, re-check by calling `search-descope-docs` again before proceeding.
- If they choose to proceed without it: continue, but flag any SDK-specific answers as "based on last known documentation — verify against the current SDK."

Do not proceed to Part 2 until this step is resolved.

---

## Part 2: Migration Plan

Part 2 has two sub-steps:

1. **Triage** — ask the questions needed to understand scope
2. **Codebase Analysis + Plan File** — scan the project, produce `MIGRATION-PLAN.md`, and pause for review

### Step 0: Triage (BLOCKING — requires `AskUserQuestion`)

**Use the `AskUserQuestion` tool to gather the information below. Do not infer answers
from memory, prior conversations, or assumptions — even if you think you know.**
The migration path differs significantly based on these answers.

Do not proceed to Step 0.5 until the user has answered.

**Decision 0 — Login mode (resolve this before anything else):**

Ask this as the first `AskUserQuestion`:

> "Is the app using Okta's **hosted/redirect login** — for example, `loginWithRedirect`, `@okta/oidc-middleware`, or users being sent to an Okta-hosted login page to authenticate? Or does it use an **embedded login UI** — the Okta Sign-In Widget embedded in the page, or a custom auth form built with `okta-auth-js` in non-redirect mode?"

Decision tree:
```
Login mode?
├── REDIRECT (hosted Okta page, loginWithRedirect, oidc-middleware, passport-openidconnect)
│     → Default to OIDC path: update OIDC client config to point at Descope endpoints
│       Set up Federated App or Inbound App in Console (Decision 1 determines which)
│       No new login page, no new SDK required — redirect/callback plumbing stays intact
│
└── EMBEDDED (Okta Sign-In Widget in-page, custom okta-auth-js non-redirect flow)
      → Default to embedded Descope Flow component path
        Replace widget/form with <Descope flowId="sign-up-or-in" />
        Still determine Federated vs. Inbound App via Decision 1
```

Do not proceed until this is resolved — it determines the entire migration approach.

---

**Decision 1 — Inbound Apps vs. Federated Apps:**

Ask as the second `AskUserQuestion` (applies to both login modes — it determines which type of app to configure in the Console):

> "Does the backend validate OAuth scopes from the Okta access token? (i.e., is there backend code that reads `token.scp`, `claims["scp"]`, or similar to make authorization decisions?)"

Decision tree:
```
Does any backend service validate token scopes (scp claim)?
├── YES  → Inbound Apps path
│          (Descope enforces scopes; custom claims go in JWT Template on the Inbound App)
├── NO   → Federated Apps + OIDC layer
│          (Okta used for identity only; often just update JWKS URL + Issuer, no scope changes)
└── UNSURE → Ask them to grep: token.scp  claims["scp"]  req.auth.scp
             Then re-ask.
```

Do not proceed until this is resolved.

---

**Remaining triage — first `AskUserQuestion` call (up to 3 questions):**

1. **Backend language / framework** — Present the most likely options based on cues in the conversation (Node.js/Express, Next.js, Angular, React SPA, Go, Python, Java). The user can always pick "Other."
2. **Migration goal** — Full cut-over, incremental/phased migration, or just evaluating.
3. **Existing user base** — Are they migrating an app with active users in Okta, or starting fresh? This determines whether user migration planning is needed.

**Second `AskUserQuestion` call — Okta CIS feature usage (use `multiSelect: true`):**

Which Okta CIS features are in use? Present these options:
- Okta Sign-In Widget (`@okta/okta-signin-widget` — embedded login UI component)
- Sign-On Policies (per-app auth rule chains / visual policy builder)
- Authenticator Enrollment Policies (MFA factor requirements)
- Authorization Servers / APIs (custom OAuth audiences and scopes)
- Identity Providers (external SAML/OIDC SSO per customer org)
- Authenticators (WebAuthn/Passkeys, TOTP, Okta Verify, SMS, etc.)
- Log Streams (Splunk Cloud, Amazon EventBridge)
- Service Apps / API Services (M2M / client credentials)
- Token Inline Hooks (custom logic during auth)
- Groups (used for RBAC/access control)

The user can add others via "Other." Follow up on anything selected — e.g., if Authorization
Servers is selected, ask about custom claims using Okta Expression Language. If Authenticators
is selected, ask which specific types.

After both calls, summarize findings and flag high-complexity items (Token Inline Hooks with
external dependencies, complex Sign-On Policy rule chains, custom Expression Language claims)
before proceeding to Step 0.5.

---

### Step 0.5: Engineer Review Checkpoint (BLOCKING — requires `AskUserQuestion`)

These questions surface blockers the framework doesn't expose. Ask even the ones you think
you know. Use `AskUserQuestion` before proceeding to codebase analysis.

Batch into calls of up to 4 questions. Skip questions that are clearly inapplicable given
Step 0 answers (e.g., skip user migration planning if they said they're starting fresh).

**Strategy confirmation**
- Does the backend validate `scp` claims from the Okta access token? (If yes → Inbound Apps. If unsure, show them what to grep for: `token.scp`, `claims["scp"]`, `req.auth.scp`.) — skip if already resolved in Decision 1
- For redirect-mode apps: is the migration goal to keep the redirect flow (OIDC endpoint swap only) or eventually move to the embedded Descope Flow component? (The OIDC path is a valid permanent solution — not just a stepping stone.)
- Are Sign-On Policies per-app, global, or both? (Determines scope of Flow migration.)
- Is scope validation in application code or in an API gateway / JWT authorizer? (If gateway → just update JWKS URL and Issuer, no code change.)

**Access and credentials**
- Do they have access to the Descope Console and a Project ID? (If not, see Step 1.5.)
- Do they need a Management Key? (Required for user CRUD, role management, tenant management, SCIM.)

**Codebase scope**
- Are there places in the app that read claims directly from the token (e.g., `token.scp`, `req.auth.permissions`, `token.groups`)? These need a JWT Template configured before they'll work.
- Do they have Token Inline Hooks? Each one needs to be recreated as a Descope Flow Scriptlet or Generic HTTP Connector.
- Are there multiple services or microservices validating Okta tokens? Each needs to be updated to validate Descope JWTs (or have its JWKS URL + Issuer updated if using an API gateway).

**Deployment and risk**
- Do they have multiple environments (dev / staging / prod)? Each needs its own Descope project and Project ID.
- Is there a maintenance window, or does this need to be zero-downtime?

**User migration** (if they indicated existing users in Step 0)

There are three migration paths — pick one or combine them. Confirm which fits before planning.

- **Full migration**: Export all users from Okta (Management API `GET /api/v1/users`, paginated), transform attributes, and bulk-import into Descope before cutover. Use the Batch Create Users Management API directly. Optionally set a `freshlyMigrated` custom attribute to `true` on import to enable first-login Flow logic.
- **JIT (password verification)**: Don't bulk-export. When a user signs in, verify their password against the Okta Authentication API (`POST /api/v1/authn`), then create or link the user in Descope and issue a Descope session. The user must re-enter credentials but no upfront export is needed.
- **Session migration (JIT without re-login)**: The app sends the user's existing Okta session token to Descope; Descope validates it, provisions the user in Descope just-in-time, and issues a Descope token. The user only needs the app to update — no re-login. This is the highest-quality zero-disruption path. See [docs.descope.com/migrate/session-migration](https://docs.descope.com/migrate/session-migration).

**Password constraint (all paths):** Okta does not export password hashes. For full migration, plan for a reset campaign, a first-login "set new password" Flow step, or a full switch to passwordless.

**Dual-token validation (critical for phased rollouts):** During any gradual cutover, the backend will receive both Okta JWTs (from users not yet migrated) and Descope tokens. The backend must validate both — inspect the token issuer or `kid` to route to the correct validator. See `references/implementation-nuances.md` → Dual Token Validation.

**Passkeys and TOTP cannot be migrated** — Okta does not expose these seeds. Users who enrolled passkeys or TOTP in Okta must reprovision them in Descope after migration.

**Gaps to flag immediately** (don't ask — flag these proactively based on Step 0 answers)
- If they're using **Passkeys or TOTP authenticators**: **these cannot be migrated**. Okta does not expose passkey credentials or TOTP seeds. Users will need to reprovision both in Descope after cutover — this requires a user-facing prompt (add a re-enrollment step to the sign-in Flow for affected users). Flag this early; it directly affects the user experience at launch.
- If they're using **Okta Verify push notifications**: there is no direct equivalent in Descope. Recommend replacing with Email Magic Link, TOTP, or WebAuthn/Passkeys.
- If they're using **Smart Card authenticator**: contact Descope support before migrating.
- If they're using **Security Question authenticator**: no equivalent in Descope. Plan removal or replacement.
- If they're using **Okta Workflows** (separate from CIS Policies): flag as out-of-scope for this skill — Workflows require a separate evaluation.
- If they're using **Log Streams to Datadog**: Datadog is NOT a direct Okta Log Stream destination, and Descope has no native Datadog audit connector. Plan for a custom Audit Webhook.

**Console/Flow/Widget opportunities** (flag before codebase analysis, then ask):
- If the app embeds the **Okta Sign-In Widget** (`@okta/okta-signin-widget`): the migration is almost entirely Console-side. Embed the Descope Flow component (`<Descope flowId="sign-up-or-in" />`) in the same location. No redirect required; the same low-code/no-code principle applies.
- If the app uses Okta's **hosted/redirect login** (`loginWithRedirect`, `@okta/oidc-middleware`, or any redirect-based OIDC flow): **default to the OIDC path** — set up a Federated App or Inbound App in Console and update the issuer/client-ID env vars. Do NOT recommend replacing the redirect flow with an embedded Descope component unless the user explicitly wants that. See `references/implementation-nuances.md` → OIDC compatibility path and the Node.js + @okta/oidc-middleware section (Option A).
- If the app has a custom SSO settings page: ask whether the SSO Setup Suite + Tenant Profile Widget replaces that code.
- If the app has a profile edit page or user management UI: ask whether a Descope Widget covers the use case.
- If the app has a separate MFA enrollment page: ask whether MFA should be integrated into the main sign-in Flow as a step or subflow (almost always cleaner in Descope).
- If any server-side code generates emails or runs logic during the auth journey: ask whether that logic can be a Flow Scriptlet or Connector instead.

Summarize any blockers and Console/Flow opportunities before proceeding to codebase analysis.

---

### Step 0.75: Fast-Track Assessment

Before running codebase analysis, determine whether the app qualifies for a minimal-code migration.

**Fast-track A — OIDC redirect swap (all three must be true):**
1. App uses **hosted/redirect login** (Decision 0 = redirect)
2. **Decision 1 resolved to Federated Apps** (no backend scope validation)
3. **No Token Inline Hooks** selected in the feature multiselect

**If all three are true:** this is a minimal-config migration. The work is ~80% Console setup:
- Create a Federated App in Console (Applications → Federated Apps → + Application); register the callback URL
- Configure the Descope Flow linked to the app (auth methods, branding) — this replaces the Okta hosted login page
- Update env vars: `OKTA_ISSUER` → `https://api.descope.com/DESCOPE_PROJECT_ID`; `OKTA_CLIENT_ID` → Project ID; `OKTA_CLIENT_SECRET` → a Descope Access Key
- If using `@okta/oidc-middleware`: replace with `openid-client` (Okta's middleware is not confirmed to work with non-Okta issuers)
- Check `scp` → `scope` claim rename in any backend authorization code (see `implementation-nuances.md` → scp vs. scope claim)
- Configure JWT Template for `email`/`name` claims
- No new login page, no SDK swap, no changes to callback routes

Skip or abbreviate framework-specific code changes in Step 2. Codebase analysis is still useful to find stale Okta references and `scp` usages, but the diff will be small.

---

**Fast-track B — Embedded widget swap (all four must be true):**
1. App embeds the **Okta Sign-In Widget** (`@okta/okta-signin-widget`) rather than a custom SDK-based auth flow
2. **Decision 1 resolved to Federated Apps** (no backend scope validation)
3. **No Token Inline Hooks** selected in the Step 0 feature multiselect
4. **No Authorization Servers** with custom claims or resource policies selected in Step 0

**If all four are true:** this is a minimal-code migration. The work is 90%+ Console-side:
- Embed `<Descope flowId="sign-up-or-in" />` (or the web component) where the widget was
- Configure the Flow in the Console — auth methods, MFA steps, branding
- Update env vars (`OKTA_*` → `DESCOPE_PROJECT_ID`)
- That's most of the migration

Skip or abbreviate Step 2 (framework-specific code changes). Codebase analysis is still useful to find any stale Okta references, but the diff will be small.

**If neither fast-track applies:** proceed with full codebase analysis below.

---

### Step 1: Codebase Analysis

Scan the codebase and fetch Okta policies before writing the plan. Both are required — code analysis finds what changes, policy analysis determines how complex the Flow migration will be.

**Step 1a — Code analysis (adapt file extensions to the user's language):**

```bash
# Find all Okta import sites
grep -rn "okta-auth-js\|@okta/okta-react\|@okta/okta-angular\|@okta/okta-vue\|@okta/oidc-middleware\|okta-jwt-verifier\|@okta/jwt-verifier\|@okta/okta-signin-widget" \
  --include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" \
  --exclude-dir=node_modules --exclude-dir=.next --exclude-dir=dist --exclude-dir=venv \
  . 2>/dev/null

# Find all Okta env var references
grep -rn "OKTA_\|OKTA_CLIENT\|OKTA_ISSUER\|OKTA_DOMAIN\|OKTA_AUDIENCE\|OKTA_REDIRECT" \
  --include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" \
  --include="*.env*" --include="*.yml" --include="*.yaml" --include="Dockerfile" \
  --exclude-dir=node_modules --exclude-dir=.next \
  . 2>/dev/null

# Find scp / scope claim access patterns (things that need scp→scope update or JWT Template)
grep -rn "\.scp\b\|token\.scp\|claims\[.scp.\]\|req\.auth\.scp\|req\.userContext\|token\.claims\b" \
  --include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" \
  --exclude-dir=node_modules --exclude-dir=.next \
  . 2>/dev/null

# Find protected route / auth guard declarations
grep -rn "requiresAuth\|OktaAuthGuard\|loginWithRedirect\|authGuard\|isAuthenticated\$\|oktaAuth\b\|withRequiredAuthInfo\|ensureAuthenticated" \
  --include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" \
  --exclude-dir=node_modules --exclude-dir=.next \
  . 2>/dev/null

# Check package.json / go.mod / requirements.txt for Okta dependencies
find . -maxdepth 3 \( -name "package.json" -o -name "go.mod" -o -name "requirements.txt" \) \
  ! -path "*/node_modules/*" -exec grep -l "okta" {} \;
```

**Step 1b — Policy analysis (required before writing the plan):**

Policy rules determine how complex the Flow migration will be. Retrieve them now so MIGRATION-PLAN.md reflects the actual logic, not a generic template.

```bash
# Sign-On Policies (type=ACCESS_POLICY → called "Sign-On Policies" in the Okta Console)
# Each one becomes a Descope Flow
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "https://${OKTA_DOMAIN}/api/v1/policies?type=ACCESS_POLICY" \
  | jq '[.[] | {name, id, ruleCount: (.rules | length), conditions: .conditions}]'

# Authenticator Enrollment Policies (MFA requirements → Flow MFA steps or subflows)
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "https://${OKTA_DOMAIN}/api/v1/policies?type=MFA_ENROLL" \
  | jq '[.[] | {name, id, rules: [.rules[] | {priority, conditions, actions}]}]'

# Global Session Policies (session lifetime → Console → Project Settings → Session Management)
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
  "https://${OKTA_DOMAIN}/api/v1/policies?type=OKTA_SIGN_ON" \
  | jq '[.[] | {name, id, rules: [.rules[] | {maxSessionIdleMinutes: .actions.signon.session.maxSessionIdleMinutes, maxSessionLifetimeMinutes: .actions.signon.session.maxSessionLifetimeMinutes}]}]'
```

For each Sign-On Policy found, note: number of rules, conditions per rule (group, network zone,
device), factors required per rule, and any post-auth hooks. This becomes the Flow complexity
estimate in the plan.

For each hit, record:
- **File path and line** — where the change happens
- **What it does** — import, route protection, claim access, logout handler, etc.
- **Complexity** — Low (drop-in replacement), Medium (logic rewrite), High (no equivalent)

Read `package.json` (or equivalent) for the exact framework version — this affects async
behavior (Next.js 15 vs 14) and SDK compatibility.

If the Descope Docs MCP is available, use `search-descope-docs` or `ask-question-about-descope`
to verify current SDK method names for anything you plan to reference in the plan.

---

### Step 2: Write MIGRATION-PLAN.md

Write `MIGRATION-PLAN.md` to the working directory using the triage answers and codebase
analysis.

Two audiences: the engineer needs enough technical detail to execute; the PM or tech lead
needs scope, risk, and timeline without decoding jargon. Use plain English. Explain
technical terms on first use. Open each section with a sentence summarizing what it means
before presenting tables or evidence. Say what breaks if a risk is missed, not just that it
exists. Pair complexity labels with time estimates; skew toward the lower bound. Group
execution into phases so parallel vs. sequential work is clear.

The plan must include these sections, in this order:

#### Overview

2–3 sentences: what's being replaced, what replaces it, and the recommended approach with a
one-sentence rationale. Add one sentence on what doesn't change — user-facing login behavior,
sessions, and existing accounts are preserved.

Include a **Migration at a Glance** table:

| | |
|---|---|
| **Approach** | Inbound Apps (full native) / Federated Apps (OIDC layer) |
| **Files changing** | N source files across N areas |
| **Console setup** | N configuration steps before launch |
| **User impact** | No re-login required / Users will need to log in once after cutover |
| **Estimated engineering effort** | N–N hours |
| **Biggest risk** | One sentence naming the highest-complexity item |

---

#### What's Changing and Why

Prose (not a table) describing what each part of the system does today and what it does
after. Example:

> Today, Okta handles everything related to login: it shows the hosted sign-in page, issues
> tokens, and validates them on every API request. After this migration, Descope takes over all
> of those responsibilities. The login UI becomes a Descope Flow embedded in the app. Token
> validation moves to the Descope SDK. The five Okta environment variables are replaced by a
> single Descope Project ID.
>
> Okta CIS features in use that need to carry over: [list in plain English, one clause each].

Tailor to triage findings.

---

#### Auth Touchpoints: What the Code Analysis Found

Open with the scope count (e.g., "9 files across 3 areas"). Group by area, not file path.
Each group gets a sentence on what it does and what changes.

**Session handling (2 files)** — These files read and validate the current user's login
state. They'll be updated to use the Descope session SDK instead of Okta's.

| File | What it does today | What changes |
|---|---|---|
| `middleware/auth.ts:22` | Validates Okta access token via `okta-jwt-verifier` | Rewritten to call `descopeClient.validateSession()`; `scp` → `scope` claim reference updated |
| `lib/session.ts:8` | Returns `req.userContext.userinfo` | Updated to return Descope `AuthenticationInfo.token` |

Cover all functional groupings. End with: "Total: N files. Estimated code-change effort: N–N hours."

---

#### Feature Migration: Okta CIS → Descope

For each Okta CIS feature confirmed in triage, write a short paragraph: what it's trying to
accomplish, the best Descope approach for that goal, what's different, and what action is
required. The best approach may be a Flow, Widget, SSO Setup Suite, or Console configuration
rather than a direct SDK equivalent. Only recommend SDK code when programmatic control is
genuinely required. Example:

> **Sign-On Policies → Descope Flows**
> Okta Sign-On Policies define per-app authentication rule chains: which factors are required,
> in what order, and under which network or group conditions. Descope Flows replace this with a
> visual pipeline where each rule becomes a Condition or step. The Sign-On Policy can be fetched
> via `GET /api/v1/policies?type=ACCESS_POLICY` to understand the exact logic before building
> the corresponding Flow. Most single-rule policies map to one Flow with a Condition branch.
> **Effort: Low–Medium (30 min per simple policy, 2–3 hours for complex branching logic).**

Only include confirmed features.

---

#### Before the Code Can Run: Required Configuration

List every Console setup item as a checkbox. Group into "Required before any testing" and
"Required before production":

**Required before any testing:**
- [ ] **Create a Descope project** — Takes 2 minutes. Produces a Project ID that replaces all Okta credentials in the app's environment variables.
- [ ] **Create an authentication Flow** — The built-in `sign-up-or-in` flow works for most apps without customization. Use it to start.
- [ ] **Configure a JWT Template** — Okta ID tokens include `email` and `name` by default. Descope does not. In Console → **Project Settings → JWT Templates → + JWT Template → User JWT**, add claims with **Type: Dynamic**: `email` → `user.email`, `name` → `user.name`. Without this, any UI reading the user's name or email shows blank values. (~10 minutes)
- [ ] **Configure authentication methods** — Enable the methods that match the Okta Authenticators in use (Passkeys, TOTP, SMS OTP, Email OTP/Magic Link). (~5 minutes each)

**Required before production:**
- [ ] **Create roles** (if using Groups for RBAC) — List actual roles found in codebase.
- [ ] **Configure Tenant SSO** (if migrating Identity Providers) — Per-tenant SAML/OIDC setup via Console → SSO or SSO Setup Suite.
- [ ] **Set up Audit Connector** (if using Log Streams) — Splunk Connector OOTB; custom webhook for EventBridge/Datadog.
- [ ] (continue for each item found in analysis)

---

#### Environment Variables

Diff table with plain-English notes for each removal and addition:

**For the OIDC path (redirect-based login):**

| Remove | Add | Why |
|---|---|---|
| `OKTA_ISSUER` / `OKTA_DOMAIN` | — | Replaced by `DESCOPE_PROJECT_ID` in the issuer URL (`https://api.descope.com/PROJECT_ID`). |
| `OKTA_CLIENT_ID` | `DESCOPE_PROJECT_ID` | For Federated OIDC Apps, the Project ID is the OIDC `client_id`. |
| `OKTA_CLIENT_SECRET` | `DESCOPE_ACCESS_KEY` | For Federated OIDC Apps, an Access Key is the `client_secret`. Generate one in Console → Access Keys. |
| `OKTA_AUDIENCE` | — | Handled by the Inbound App definition, if in use. |
| — | `DESCOPE_MANAGEMENT_KEY` | Only needed if the app manages users, roles, or tenants server-side. |

`OKTA_REDIRECT_URI` / callback URL stays — Descope's OIDC endpoints accept the same callback path.

**For the embedded path (Descope Flow component):**

| Remove | Add | Why |
|---|---|---|
| `OKTA_CLIENT_ID` | — | Okta identifies apps by client ID. Descope uses a Project ID instead. |
| `OKTA_CLIENT_SECRET` | — | Not needed. Descope's embedded flow doesn't require a secret. |
| `OKTA_ISSUER` / `OKTA_DOMAIN` | — | The Okta tenant URL. Replaced by the Project ID. |
| `OKTA_AUDIENCE` | — | Used for API access scoping. Can be replicated via Inbound App + JWT Template if needed. |
| `OKTA_REDIRECT_URI` | — | Descope's embedded flow doesn't use redirect URIs. |
| — | `DESCOPE_PROJECT_ID` | The single identifier for the Descope project. Replaces all of the above. |
| — | `NEXT_PUBLIC_DESCOPE_PROJECT_ID` | Same value, exposed to the browser for Next.js client components. |
| — | `DESCOPE_MANAGEMENT_KEY` | Only needed if the app manages users, roles, or tenants server-side. |

Follow with: "Net change: [N variables removed, N added — use the appropriate table above based on login mode]."

---

#### User Migration (only if existing users need to be migrated)

Prose strategy first, then steps. Start with: "X existing users need to be in Descope before cutover."

Key Okta-specific constraint: **Okta does not export password hashes to third parties.** Users
will need to reset their passwords or switch to passwordless after cutover. Plan for one of:
- A password reset email campaign sent before cutover
- A "set new password on first login" step added to the Descope sign-in Flow
- A full switch to passwordless (magic link, passkeys, TOTP)

End with a brief PM-trackable checklist:
- [ ] Choose password migration strategy — reset campaign, first-login step, or passwordless
- [ ] Export user list from Okta (Management API `GET /api/v1/users` or Okta Reports)
- [ ] Transform user data to Descope import format
- [ ] Run import against the Descope dev project and review output for errors
- [ ] Run import against staging, then production

---

#### Trade-offs and considerations

Write each in plain English with three parts: **what it is**, **what breaks if it's ignored**, **what to do**.

> **Consideration: scp → scope claim rename (code change, always required)**
> This is a JWT claim name change, separate from the Inbound vs. Federated App decision.
> Okta access tokens carry scopes in `scp` (JSON array). Descope uses `scope` (space-separated
> string or array). Any backend code reading `token.scp`, `claims["scp"]`, or `req.auth.scp`
> will receive `undefined` after migration and authorization checks will fail silently —
> regardless of whether Inbound or Federated Apps are used.
> **Action:** Grep for `.scp` in backend code before testing. Update all references to `.scope`
> and handle both string and array formats. This is separate from configuring Inbound Apps
> (which is about *whether* scopes are enforced, not the claim name).

> **Consideration: User profile data won't appear after login until a JWT Template is configured**
> Descope session tokens don't include `email` or `name` by default. Any UI that shows user
> profile information will show blank values after migration.
> **Action:** Configure the JWT Template in the Console before running any tests. (~10 minutes.)

> **Consideration: Password migration is blocked by Okta policy**
> Okta does not release password hashes. Password users will need to reset their passwords after
> cutover.
> **Action:** Decide on a migration strategy (reset campaign, first-login flow step, or switch
> to passwordless) before setting a cutover date.

> **Consideration: Passkeys and TOTP credentials cannot be migrated**
> Okta does not expose passkey credentials or TOTP seeds. Users who enrolled these authenticators
> in Okta must re-provision them after cutover — there is no way to migrate them silently.
> **Action:** Add a re-enrollment step to the sign-in Flow conditioned on `freshlyMigrated: true`
> and set user expectations before the cutover date.

> **Consideration: Inbound Apps vs. Federated Apps misclassification**
> If the backend validates `scp` claims from Okta access tokens and Federated Apps are configured
> instead of Inbound Apps, the backend receives tokens with no `scope` claim and all scope
> checks fail — likely silently.
> **Action:** Confirm before Console setup whether any backend service validates token scopes.
> If yes, configure Inbound Apps with scope definitions matching the Okta Authorization Server.

Include only applicable trade-offs and considerations.

---

#### Execution Plan

Phases run in sequence. Steps within a phase can run in parallel.

**Phase 1 — Console Setup** (~20–30 minutes, no code required)
Project and credentials boilerplate. Nothing here depends on the codebase.

- [ ] Create Descope project, copy Project ID
- [ ] Generate Management Key (only if the app manages users, roles, or tenants server-side)
- [ ] Enable authentication methods: (list actual methods matching Okta Authenticators found)
- [ ] Configure JWT Template with `email`, `name`, and any custom claims
- [ ] Set up Audit Connector: (Splunk / custom webhook, if Log Streams are in use)

**Phase 2 — Flow Migration** (~1–4 hours, Console only, no code required)
The core work of an Okta migration. Translate Okta authentication policies into Descope Flows
entirely through the Console — no code changes yet. Complexity scales with the number and
complexity of policy rules.

- [ ] Fetch Sign-On Policies: `GET /api/v1/policies?type=ACCESS_POLICY` — review all rules
- [ ] Create Descope Flow for each Sign-On Policy (start from `sign-up-or-in` template; add Condition branches per rule)
- [ ] Fetch Authenticator Enrollment Policies: `GET /api/v1/policies?type=MFA_ENROLL` — note required vs. optional factors
- [ ] Add MFA steps or subflows to sign-in Flow for each required factor
- [ ] Fetch Global Session Policies: `GET /api/v1/policies?type=OKTA_SIGN_ON` — note session lifetime values
- [ ] Set session lifetime in Console → Project Settings → Session Management to match
- [ ] Configure Tenant SSO: (list actual IdPs found, if any)
- [ ] Create roles: (list actual roles found)

**Phase 3 — Code Changes** (~X–Y hours, 1 engineer)

- [ ] Update environment variables in `.env.example` and CI config
- [ ] Replace Okta SDK imports with Descope SDK
- [ ] Rewrite session validation middleware
- [ ] Add `/login` page with `<Descope>` component (or web component)
- [ ] Update protected route guards
- [ ] Update logout handler (two steps: SDK call + cookie clear)
- [ ] Update `scp` → `scope` claim references in backend code
- [ ] Compile check and fix any type errors before proceeding

**Phase 4 — User Migration** (~1–2 hours)
Run import against dev/staging before production. Do not run against production until Phase 5 passes.

**Phase 5 — Testing** (~30–45 minutes)

- [ ] Compilation passes with zero errors
- [ ] Server starts, no crashes on startup
- [ ] Unauthenticated routes redirect to login correctly
- [ ] Login flow completes, user profile data appears
- [ ] Logout invalidates session
- [ ] `scope` claim (not `scp`) is present if scopes are used

**Phase 6 — Production Cutover**
- [ ] (cutover-specific steps based on their strategy)

---

Total estimated engineering effort: **N–N hours** across N engineers.
Blocking dependencies: (list anything on the critical path)

---

After writing `MIGRATION-PLAN.md`, **stop and tell the user:**

> `MIGRATION-PLAN.md` has been written to your working directory. It maps every auth
> touchpoint found, lists what needs Console setup before the first test, and calls out
> trade-offs and considerations that could affect the timeline.
>
> Take a look before we start making changes. When you're ready to proceed, say so.

Do not proceed to Part 3 unless the user confirms.

---

## Part 3: Execution

Execute the Execution Plan from `MIGRATION-PLAN.md` (the final section, Phase 1 through 6). Follow the detailed guidance below for each step.

---

### Context Continuity Protocol

Context can be lost between turns. These rules keep the migration coherent.

**Step 3.0 — Create `MIGRATION-STATE.md` before touching any code.**

Write `MIGRATION-STATE.md` to the working directory from the template below.

```markdown
# Migration State

_Last updated: [timestamp of last completed step]_

## Project Context
- Framework: [e.g., Next.js 14, Express + React]
- Language: [TypeScript / Python / Go]
- Package manager: [npm / yarn / pnpm / pip / etc.]
- Login mode: [Redirect (OIDC path) / Embedded (Descope Flow component)]
- App type in Descope Console: [Federated App / Inbound App]
- Migration path: [OIDC endpoint swap / Embedded Flow component / Full SDK replacement]
- Migration goal: [Full cutover / Phased / Evaluating]

## Triage Answers
- Existing users: [Yes — N users / No — greenfield]
- Password migration strategy: [Reset campaign / First-login step / Passwordless]
- Okta features in use: [comma-separated list]
- Multiple environments: [Yes: dev/staging/prod / No]
- Zero-downtime required: [Yes / No]

## Files Inventory
_All files that need to change. Update status after each step._

| File | Change | Status |
|---|---|---|
| `middleware/auth.ts` | Replace okta-jwt-verifier with Descope SDK | ⬜ Pending |
| `src/App.tsx` | Replace Security/OktaAuth provider | ⬜ Pending |

## Console Setup Checklist
- [ ] Descope project created — Project ID: (fill in when done)
- [ ] JWT Template configured
- [ ] Auth methods enabled: (list)
- [ ] Roles created: (list)
- [ ] Tenant SSO configured: (list IdPs)

## Decisions Log
_Non-obvious decisions made during migration._

_(none yet)_

## Current Phase
Phase 1 — Console Setup (not started)

## Next Action
Complete Phase 1 Console Setup, then Phase 2 Flow Migration (policy translation in Console) before touching any code.

## Blockers
_(none)_
```

---

**Rule 1 — Re-read before every turn.**

At the start of every execution turn, re-read `MIGRATION-PLAN.md` and `MIGRATION-STATE.md`
before writing any code or making any decision.

**Rule 2 — Verify context before every code change.**

Output a context line before each code change:

> `Migration context: Next.js 14 · Inbound Apps · Phase 2, step 4/9 · Next: update scp→scope in middleware.ts`

If this line can't be filled in accurately, re-read the files first.

**Rule 3 — Update `MIGRATION-STATE.md` immediately after each step.**

Mark the file done in the Files Inventory, update "Current Phase" and "Next Action", and
append any non-obvious decision to the Decisions Log.

---

## Pre-Generation Protocol (apply before writing any code)

Run before generating any import, wrapper type, or helper. Skipping produces code that
compiles but fails at runtime.

**1. Verify SDK exports before writing any import.**
When the Docs MCP is available, use `ask-question-about-descope` to confirm the exact method name, option shape, and return type before writing any SDK call. Do not write a method name and add a hedge like "verify the exact name" — just verify it.

When the Docs MCP is unavailable: resolve the package's type declarations (`node_modules/<pkg>/dist/types/` or its `package.json` `types` field) and confirm the exact exported name and signature.

**Prefer local `node_modules/` over GitHub** when reading type declarations. Installed packages reflect the exact version in use. Install the Descope package first if not yet installed, then read local type declarations.

**1a. After rewriting any module, grep for remaining Okta imports.**
```bash
grep -r "@okta\|okta-auth-js\|okta-jwt-verifier" --include="*.ts" --include="*.tsx" --include="*.js" .
```
Add remaining hits to the work list.

**2. Derive wrapper types from the actual return type.**
Read the function's declared return type and build the wrapper to match. Okta's field names,
nesting, and flags differ — don't infer from them.

**3. Check dependency versions before generating framework-specific code.**
For Next.js: `cookies()` and `headers()` from `next/headers` are synchronous in v14 and
async in v15. Read `package.json` first.

**4. When making a helper async, propagate to all callers immediately.**
In TypeS

…(truncated)
