skills/okta-cis-to-descope/SKILL.md
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.
npx skillsauth add descope/skills okta-cis-to-descopeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill guides self-service migrations from Okta Customer Identity Service (CIS) to Descope. It runs in three parts:
MIGRATION-PLAN.md for the user to reviewDo 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 gotchasreferences/flows-and-widgets.md — Descope terminology/lingo (Okta→Descope), Flow structure and templates, Widgets, SSO Setup Suite, Console-vs-code decision guidereferences/backend-sdks.md — Python and Java backend migration patterns (Flask, FastAPI, Django, Spring Boot, management SDK, M2M)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.
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?
search-descope-docs again before proceeding.Do not proceed to Part 2 until this step is resolved.
Part 2 has two sub-steps:
MIGRATION-PLAN.md, and pause for reviewAskUserQuestion)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 withokta-auth-jsin 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):
Second AskUserQuestion call — Okta CIS feature usage (use multiSelect: true):
Which Okta CIS features are in use? Present these options:
@okta/okta-signin-widget — embedded login UI component)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.
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
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 1Access and credentials
Codebase scope
token.scp, req.auth.permissions, token.groups)? These need a JWT Template configured before they'll work.Deployment and risk
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.
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.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.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)
Console/Flow/Widget opportunities (flag before codebase analysis, then ask):
@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.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).Summarize any blockers and Console/Flow opportunities before proceeding to codebase analysis.
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):
If all three are true: this is a minimal-config migration. The work is ~80% Console setup:
OKTA_ISSUER → https://api.descope.com/DESCOPE_PROJECT_ID; OKTA_CLIENT_ID → Project ID; OKTA_CLIENT_SECRET → a Descope Access Key@okta/oidc-middleware: replace with openid-client (Okta's middleware is not confirmed to work with non-Okta issuers)scp → scope claim rename in any backend authorization code (see implementation-nuances.md → scp vs. scope claim)email/name claimsSkip 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):
@okta/okta-signin-widget) rather than a custom SDK-based auth flowIf all four are true: this is a minimal-code migration. The work is 90%+ Console-side:
<Descope flowId="sign-up-or-in" /> (or the web component) where the widget wasOKTA_* → DESCOPE_PROJECT_ID)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.
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):
# 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.
# 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:
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.
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:
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 |
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.
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."
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_POLICYto 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.
List every Console setup item as a checkbox. Group into "Required before any testing" and "Required before production":
Required before any testing:
sign-up-or-in flow works for most apps without customization. Use it to start.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)Required before production:
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]."
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:
End with a brief PM-trackable checklist:
GET /api/v1/users or Okta Reports)Write each in plain English with three parts: what it is, what breaks if it's ignored, what to do.
Risk: 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 usesscope(space-separated string or array). Any backend code readingtoken.scp,claims["scp"], orreq.auth.scpwill receiveundefinedafter migration and authorization checks will fail silently — regardless of whether Inbound or Federated Apps are used. Action: Grep for.scpin backend code before testing. Update all references to.scopeand handle both string and array formats. This is separate from configuring Inbound Apps (which is about whether scopes are enforced, not the claim name).
Risk: User profile data won't appear after login until a JWT Template is configured Descope session tokens don't include
nameby 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.)
Risk: 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.
Risk: 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: trueand set user expectations before the cutover date.
Risk: Inbound Apps vs. Federated Apps misclassification If the backend validates
scpclaims from Okta access tokens and Federated Apps are configured instead of Inbound Apps, the backend receives tokens with noscopeclaim 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 risks.
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.
email, name, and any custom claimsPhase 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.
GET /api/v1/policies?type=ACCESS_POLICY — review all rulessign-up-or-in template; add Condition branches per rule)GET /api/v1/policies?type=MFA_ENROLL — note required vs. optional factorsGET /api/v1/policies?type=OKTA_SIGN_ON — note session lifetime valuesPhase 3 — Code Changes (~X–Y hours, 1 engineer)
.env.example and CI config/login page with <Descope> component (or web component)scp → scope claim references in backend codePhase 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)
scope claim (not scp) is present if scopes are usedPhase 6 — Production Cutover
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.mdhas been written to your working directory. It maps every auth touchpoint found, lists what needs Console setup before the first test, and calls out risks 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.
Execute the Execution Plan from MIGRATION-PLAN.md (the final section, Phase 1 through 6). Follow the detailed guidance below for each step.
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.
# 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.
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.
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 TypeScript, async on a shared utility silently breaks callers that omit await. Grep
for all call sites of the changed function and update them in the same pass.
5. Verify published package versions before writing to package.json or running npm install.
npm view @descope/node-sdk version
npm view @descope/nextjs-sdk version
npm view @descope/react-sdk version
If npm is unavailable, leave the version as "latest" and flag it.
Use AskUserQuestion to ask whether they already have a Project ID and working Flow. If
yes, skip to verifying items 5–7.
P (e.g. P2abc123...).NEXT_PUBLIC_DESCOPE_PROJECT_ID. For all server-side SDKs: DESCOPE_PROJECT_ID.Required for: user management API, role/permission management, tenant operations, SCIM configuration.
DESCOPE_MANAGEMENT_KEY. Never expose client-side.references/flows-and-widgets.md → Flows.Okta ID tokens include email and name by default. Descope does not.
email → user.email, name → user.name, picture → user.pictureuser.customAttributes.Xtoken.email will get undefined after migration.Descope roles are referenced by name, not ID. They must be created in the Console before the code that assigns them will work.
Okta user profile custom attributes map to Descope customAttributes. Pre-define them before
setting them via the SDK.
| Variable | Where to get it | Used by |
|---|---|---|
| DESCOPE_PROJECT_ID | Console → Project Settings | All server-side SDKs |
| NEXT_PUBLIC_DESCOPE_PROJECT_ID | Same value as above | Next.js AuthProvider (client-side) |
| DESCOPE_MANAGEMENT_KEY | Console → Company → Management Keys | Management SDK |
Before migrating custom profile pages or user management pages, ask whether a Descope Widget
covers the use case. See references/flows-and-widgets.md → Widgets.
Read references/implementation-nuances.md in two passes before writing any code:
@okta/okta-react → ## React + @okta/okta-react in implementation-nuances.md@okta/okta-angular → ## Angular + @okta/okta-angular in implementation-nuances.md@okta/okta-vue → ## Vue + @okta/okta-vue in implementation-nuances.md@okta/oidc-middleware → ## Node.js / Express + @okta/oidc-middleware in implementation-nuances.md## Backend JWT validation (okta-jwt-verifier) in implementation-nuances.md## Next.js in implementation-nuances.md## Custom / open-source OIDC clients in implementation-nuances.mdreferences/backend-sdks.md → Python sectionreferences/backend-sdks.md → Java sectionScan for Okta references in non-code files after updating source files.
.env.example / .env.template / .env.sample# REMOVE
OKTA_CLIENT_ID=
OKTA_CLIENT_SECRET=
OKTA_ISSUER=
OKTA_DOMAIN=
OKTA_AUDIENCE=
OKTA_REDIRECT_URI=
# ADD
DESCOPE_PROJECT_ID= # Console → Project Settings
NEXT_PUBLIC_DESCOPE_PROJECT_ID= # Next.js only — same value as above
DESCOPE_MANAGEMENT_KEY= # Console → Company → Management Keys (if using management SDK)
Run grep -r "OKTA" to find all env var references — .env.example, Docker, CI, shell scripts.
Search all .md files for Okta references. At minimum, update:
Check Dockerfile, docker-compose.yml, .github/workflows/, and any CI config for
OKTA_* env var declarations. Update them to DESCOPE_*.
Okta has three distinct policy types, each with a different Descope migration target. Treat them separately — don't collapse them into a single "Flows" step.
Sign-On Policies define per-app rule chains: which factors are required, in what order, and under which conditions (group membership, network zone, device trust). Each rule becomes a Condition branch or auth step in a Descope Flow.
Fetch before building:
curl -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
"https://${OKTA_DOMAIN}/api/v1/policies?type=ACCESS_POLICY"
| Okta Sign-On Policy rule | Descope Flow equivalent |
|---|---|
| Require factor X | Auth method step in Flow |
| Condition: user in group Y | Condition branch on user.roles |
| Condition: network zone | Condition branch on IP/request context |
| Post-auth custom logic (Inline Hook) | Flow Scriptlet or Generic HTTP Connector |
One Sign-On Policy typically maps to one Flow. Complex branching logic (multiple rules with different factor requirements per condition) maps to Conditions + subflows.
Authenticator Enrollment Policies control when users must enroll in MFA and which factors are required vs. optional. In Descope, enrollment happens inline in the sign-in Flow, not through a separate enrollment journey.
Fetch before building:
curl -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
"https://${OKTA_DOMAIN}/api/v1/policies?type=MFA_ENROLL"
Read the policy's required vs optional authenticator list, then add an MFA step to the sign-in Flow for required factors, or use a subflow triggered by a condition (e.g., user is in an admin group) for context-sensitive enrollment.
Global Session Policies control session lifetime, idle timeout, and re-authentication frequency. These map to Descope's project-level session settings, not to Flows.
Fetch before configuring:
curl -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
"https://${OKTA_DOMAIN}/api/v1/policies?type=OKTA_SIGN_ON"
In Descope: Console → Project Settings → Session Management. Map Okta fields to Descope fields correctly: maxSessionLifetimeMinutes → Refresh Token Timeout (total logged-in duration); maxSessionIdleMinutes → Session Inactivity (idle timeout). These are different fields — do not conflate them.
| Okta Authenticator | Descope Auth Method | |---|---| | Passkeys (FIDO2 WebAuthn) | Passkeys | | TOTP (Google Authenticator, Okta Verify TOTP) | TOTP | | Password | Password | | Phone (SMS, Voice) | SMS OTP | | Email (magic link or OTP) | Email OTP / Magic Link | | Okta Verify (push) | No direct equivalent — replace with Email Magic Link, TOTP, or Passkeys | | Security Question | No equivalent — plan removal |
Key selling point: Descope can consume the existing IdP response using the same ACS URL already configured in the customer's IdP. Tenant admins do not need to reconfigure their SAML or OIDC settings — the migration is transparent to them. This is handled via DNS redirect at the Okta → Descope cutover. See docs.descope.com/migrate/sso for the full process.
Before migrating any Management SDK SSO calls, ask whether the SSO Setup Suite eliminates the need for that code. See references/flows-and-widgets.md → SSO Setup Suite.
| Okta | Descope |
|---|---|
| SAML Identity Provider (per-org) | management.sso.configureSAMLByTenant(tenantId, settings) |
| OIDC Identity Provider (per-org) | management.sso.configureOIDCByTenant(tenantId, settings) |
| Okta | Descope |
|---|---|
| req.auth.groups.includes('admin') | token.roles.includes('admin') |
| Group membership via Okta | Role assignment via Management SDK or Console |
| groups claim in token | roles array in JWT (built-in) |
SDK: descopeClient.management.role.create(name, description, permissionNames, tenantId)
| Okta | Descope |
|---|---|
| Service App (client ID + secret) | Access Key |
| POST /token (client credentials) | descopeClient.exchangeAccessKey(accessKey) |
| Okta Log Stream | Descope Connector | |---|---| | Splunk Cloud | Splunk Audit Connector (OOTB — Console → Connectors) | | Amazon EventBridge | Custom Audit Webhook Connector | | Datadog (indirect) | Custom Audit Webhook Connector |
Set up before cutover to avoid gaps in event logging.
| Okta Inline Hook type | Descope equivalent | |---|---| | Token Inline Hook (modify claims) | Flow Scriptlet or JWT Template | | Token Inline Hook (call external service) | Generic HTTP Connector |
See docs.descope.com/migrate/okta-cis for the authoritative guide. Three paths:
Export all users from Okta, import into Descope before cutover. Use the Descope Batch Create Users API directly.
# Export users (paginated — max 200 per page)
curl -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
"https://${OKTA_DOMAIN}/api/v1/users?limit=200"
# For next page, use ?after=${lastUserId} from the Link header
Attribute mapping:
| Okta field | Descope field |
|---|---|
| profile.login or profile.email | loginId (required; unique per user) |
| profile.firstName | givenName |
| profile.lastName | familyName |
| profile.email | email |
| Custom profile fields | customAttributes |
Import via Management SDK: management.user.createBatch([...users])
Set freshlyMigrated: true as a custom attribute on import — use this in Flow Conditions to route newly-migrated users through a first-login experience (password reset prompt, re-enrollment for TOTP/passkeys), then flip it to false once done.
Alternative — own data store: If Okta sits in front of your own database (via On-prem SCIM Server Agent or Access Gateway), you own the user data. Connect that same DB to Descope via a Generic HTTP Connector in your Flow and sever Okta from the path — no export/import needed.
Don't bulk-export. When a user signs in, verify their password against Okta's Authentication API, then create or link the user in Descope and issue a Descope session. The user must re-enter credentials once.
curl -X POST \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
-d '{"username": "[email protected]", "password": "..."}' \
"https://${OKTA_DOMAIN}/api/v1/authn"
On success: create or link the user in Descope via Management SDK, then issue a Descope session. The Okta session is retired; subsequent sign-ins go directly through Descope.
The highest-quality zero-disruption path. Deploy a new app version using the Descope SDK with session migration enabled. When a user opens the app with an existing Okta session token, Descope validates it, provisions the user just-in-time, and issues a Descope token — no re-login, no interruption. See docs.descope.com/migrate/session-migration.
Okta does not export password hashes. For full migration, plan one of:
freshlyMigrated condition)Okta does not expose passkey credentials or TOTP seeds. Users who enrolled these in Okta must reprovision them in Descope. Add a re-enrollment step to the sign-in Flow conditioned on freshlyMigrated: true.
Okta email templates map to Descope Messaging Templates, configured per authentication method in the Console.
CNAME auth.example.com → cname.descope.com, verify in Console, then pass baseUrl to
the Descope SDK.
Okta access tokens use scp (JSON array). Descope uses scope (array or space-separated string).
// Okta
token.scp.includes('read:invoices') // JSON array
// Descope — handle both formats
const scopes = Array.isArray(token.scope) ? token.scope : (token.scope || '').split(' ')
scopes.includes('read:invoices')
This is a silent correctness bug — not a compile error. Grep for scp in all backend code.
Descope session JWTs contain sub, amr, drn, tenants, roles, permissions, and dct
by default. They do not contain email, name, or picture. Okta ID tokens include
these by default.
Action required: Configure a JWT Template before any testing.
Descope session tokens have no aud claim by default. Apps using OKTA_AUDIENCE for
API access control must configure a custom aud claim in JWT Templates and pass audience
to validateSession().
descopeClient.logout(refreshToken) to invalidate server-sideDS and DSR cookiesSkipping either step leaves a broken state.
Profile changes via the Management SDK don't update the JWT already in the browser. Options:
useDescope().refresh() client-side — triggers an immediate token refresh.Default: DS (session JWT), DSR (refresh JWT). Configure custom names in the Descope
Console under the Flow's End action when running multiple Descope projects on the same root domain.
Okta issues separate ID tokens and access tokens. Descope has one token: the session JWT
(DS cookie). Forward it as Authorization: Bearer <DS> to API servers.
Descope has no @okta/oidc-middleware equivalent. The middleware is ~20 lines of custom code
(see references/implementation-nuances.md → Node.js / Express section).
cookies() and headers() Are Async in Next.js 15Check package.json for the Next.js version. If ≥ 15: write await cookies() and mark
the containing function async. This cascades to all callers — grep for all call sites.
During any gradual cutover, the backend will receive both Okta JWTs (from users not yet migrated) and Descope tokens (from users already migrated). If you don't handle both, migrated users break on un-updated backends and vice versa.
Inspect the token's issuer (iss) or key ID (kid) to determine the provider, then route to the correct validator:
iss is https://YOUR_DOMAIN.okta.com/oauth2/...iss is https://api.descope.com/YOUR_PROJECT_IDThis dual-validation window can be as short as one deployment cycle or as long as weeks depending on rollout speed. Remove the Okta validator once all sessions have expired or been migrated.
See docs.descope.com/migrate/session-migration#step-1-dual-token-validation-in-your-backend.
Okta: CLIENT_ID, CLIENT_SECRET, ISSUER, AUDIENCE, REDIRECT_URI (5+).
Descope: DESCOPE_PROJECT_ID only (+ DESCOPE_MANAGEMENT_KEY for management ops).
Run the app and verify it works — don't just hand over a checklist.
grep -r "@okta\|okta-auth-js\|okta-jwt-verifier\|okta-signin-widget\|OKTA_" \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" \
--exclude-dir=node_modules --exclude-dir=.next --exclude-dir=dist \
.
If this returns any results, stop and fix them before proceeding.
npm install # or: pip install -r requirements.txt / go mod tidy
npx tsc --noEmit # TypeScript
go build ./... # Go
Do not proceed until compilation exits with zero errors.
If compilation fails, diagnose by error message:
Cannot find module '@okta/...' → stale import; re-run Phase 0Property 'X' does not exist on type 'AuthenticationInfo' → wrapper built against Okta shape; re-derive'await' expression is not allowed in synchronous contexts → async cascade gapObject is possibly 'undefined' on session fields → add null check or early returnnpm run dev # or: go run . / python main.py
npm test # or: pytest / go test ./...
Auth-related test failures usually mean: a mock or fixture still uses Okta shapes, or a
test validates JWT claims that are now missing (e.g., email without a JWT Template), or
scp → scope claim wasn't updated in the test fixture.
# Root path
curl -s -o /dev/null -w "%{http_code}" http://localhost:<port>/
# Unauthenticated protected route (expect 302 or 401)
curl -s -o /dev/null -w "%{http_code}" http://localhost:<port>/dashboard
# Login page loads Descope component
curl -s http://localhost:<port>/login | grep -i "descope"
# Invalid token → 401
curl -s -H "Cookie: DS=invalid_token" http://localhost:<port>/api/me
echo "<DS_cookie_value>" | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool
Check that email, name, and custom claims are present. Verify scope claim (not scp)
is present if scopes are in use.
## Test Results
**Server startup:** ✅ Started successfully on port 3000
**Existing tests:** ✅ 12 passed / ❌ 2 failed (list failures)
**Unauthenticated /dashboard:** ✅ 302 → /login
**Unauthenticated /api/protected:** ✅ 401
**Login page loads Descope component:** ✅
**JWT claims (email, name):** ✅ Present / ❌ Missing — JWT Template not yet configured
**scope claim (not scp):** ✅ Present / ❌ Missing — update backend scope-validation code
**Blockers before going live:**
- [ ] (list anything that failed or needs manual action)
Do not proceed to Step 6 until ALL of the following are true:
Every migration produces a MIGRATION-SUMMARY.md covering what was done, manual setup
remaining, and behavioral differences that matter before production.
What was migrated — a table mapping each Okta CIS concept to its Descope replacement
Behavioral differences and open questions — numbered list of significant differences between the Okta and Descope implementations. For each item: Okta behavior, Descope behavior, action required.
Pre-deploy checklist — actionable checkbox items for everything that must happen before the migrated app can run. Prominently include all Console setup tasks.
Write a numbered migration guide in Markdown, scoped to the user's stack. Use code snippets and direct doc links. Always include the MIGRATION-SUMMARY.md deliverable (Step 6).
For complex migrations (Token Inline Hooks, custom Expression Language claims, complex Sign-On Policy rule chains), flag the high-effort items explicitly with estimated complexity (Low/Medium/High) so the user can plan.
references/implementation-nuances.md — Verified migration patterns, code-level diffs, and edge cases for each JS/TS framework and Okta CIS feature.references/flows-and-widgets.md — Okta→Descope lingo map, Flow structure, 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 access keys).development
Use when building React "Bring Your Own Screen" (BYOS) custom UI on top of a Descope flow — takes exported flow JSONs, extracts the real interaction IDs and outputs, generates BYOS components that match hosted parity, and avoids the rediscovery-the-hard-way failure modes (silent form rejection, shared screen-name collisions, anonymous-session stickiness, nested-form hydration errors, wrong form keys, dead-end buttons, missing OAuth provider field).
development
Set up and manage Descope projects with Terraform. Use when configuring authentication infrastructure as code, managing environments, creating roles/permissions, setting up connectors, or deploying Descope project configurations.
testing
Author, edit, or apply a Descope FGA schema using the ReBAC/ABAC DSL. Use this skill whenever the user asks to create a new FGA schema, modify an existing one, add types/relations/permissions/conditions, review an authorization model, or apply schema changes to a Descope project. Trigger even if the user says things like "set up authorization", "define roles and permissions", "add team-based access", "make this endpoint check FGA", or "update my authz model" — these almost always mean an FGA schema change.
development
Integrate Descope authentication into applications. Use when implementing login, signup, passwordless auth (OTP, Magic Link, Passkeys), OAuth, SSO, or MFA. Detects framework and provides targeted guidance.