skills/auth0-to-descope/SKILL.md
Use this skill whenever anyone asks about migrating from Auth0 to Descope — whether they're a developer doing it themselves or a technical lead evaluating the move. Triggers on: "how do I migrate from Auth0", "replace Auth0 with Descope", "we're moving off Auth0", "Auth0 to Descope", "switch from Auth0", "our app uses express-openid-connect / nextjs-auth0 / auth0-fastapi / other Auth0 SDK and we want to use Descope instead", or any question about Auth0 features (Actions, FGA, Organizations, Token Vault, CIBA) 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 auth0-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 Auth0 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 (both in this skill's directory):
references/implementation-nuances.md — verified migration patterns for each framework, Auth0 feature-to-Descope mappings, and known gotchasreferences/flows-and-widgets.md — Descope terminology/lingo, Flow structure and templates, Widgets, SSO Setup Suite, Console-vs-code decision guideConsole-first. Before recommending SDK code for any user-facing auth feature, check whether the Console, a Flow, or a Widget covers the use case. Engineers integrate once (SDK setup + session validation). All subsequent auth evolution — new methods, MFA changes, UI updates — 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 — embed Flows vs. OIDC compatibility, Flow vs. custom code, Widget vs. custom page, MFA inline vs. separate enrollment, programmatic SSO vs. SSO Setup Suite — use AskUserQuestion rather than proceeding with an assumption. The cost of a wrong assumption compounds across 20+ files. Uncertainty about architecture or intent is always worth a question.
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 based on these answers; getting them wrong wastes the user's
time and produces incorrect guidance.
Do not proceed to Step 0.5 until the user has answered.
First AskUserQuestion call (up to 4 questions):
Second AskUserQuestion call — Auth0 feature usage (use multiSelect: true):
Which Auth0 features are in use? Present the highest-impact categories:
The user can add others via "Other." Follow up on anything selected — e.g., if Organizations is selected, ask about tenant-scoped SSO, SCIM, and invitations. If FGA is selected, ask about the authorization model.
Also surface in a follow-up AskUserQuestion if not yet covered:
After both calls, summarize findings and flag high-complexity items (CIBA, Token Vault, FGA) 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).
Access and credentials
Codebase scope
token.email, req.auth.permissions)? These need a JWT Template configured before they'll work.Deployment and risk
User migration (if they indicated existing users in Step 0)
freshlyMigrated custom attribute (set automatically by the migration script) can be used in Flow conditionals to give first-time post-migration users a special onboarding path.descope/descope-migration script (Step 3) and recommend a dry run (--dry-run) before any live run.Gaps to flag immediately (don't ask — flag these proactively based on Step 0 answers)
@auth0/ai wrappers: flag before going further — these have no Descope equivalent and require custom implementation (see Step 3).Console/Flow/Widget opportunities (flag before codebase analysis, then ask):
Summarize any blockers and Console/Flow opportunities before proceeding to codebase analysis.
Scan the codebase to map every auth touchpoint before writing the plan.
Run these searches (adapt file extensions to the user's language):
# Find all Auth0 import sites
grep -rn "auth0\|@auth0\|express-openid-connect\|nextjs-auth0\|auth0-fastapi\|go-oidc" \
--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 Auth0 env var references
grep -rn "AUTH0_\|auth0\." \
--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 claim / token access patterns (things that may need JWT Template)
grep -rn "token\.\|claims\.\|req\.auth\.\|req\.oidc\.\|session()\." \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.py" --include="*.go" \
--exclude-dir=node_modules --exclude-dir=.next \
. 2>/dev/null
# Find protected route declarations
grep -rn "requiresAuth\|withPageAuthRequired\|withApiAuthRequired\|require_session\|@login_required\|authMiddleware\|isAuthenticated" \
--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 Auth0 dependencies
find . -maxdepth 3 \( -name "package.json" -o -name "go.mod" -o -name "requirements.txt" \) \
! -path "*/node_modules/*" -exec grep -l "auth0" {} \;
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 — SDK swaps and mechanical rewrites are usually faster than they look, and repetitive files in a group after the first go much faster. 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 | Full native migration / OIDC compatibility 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, Auth0 handles everything related to login: it shows the login UI, 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 component embedded in the app. Token validation moves to the Descope SDK. The five Auth0 environment variables are replaced by a single Descope Project ID.
Auth0 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., "11 files across 4 areas"). Group by area, not file path. Each group gets a sentence on what it does and what changes.
Session handling (3 files) — These files read and validate the current user's login state. They'll be updated to use the Descope session SDK instead of Auth0's.
| File | What it does today | What changes |
|---|---|---|
| lib/auth.ts:34 | Returns Auth0 session with isAuthenticated, user, claims | Rewritten to return Descope AuthenticationInfo; a thin adapter layer preserves the shape callers expect |
| middleware.ts:12 | Blocks unauthenticated requests app-wide | Updated to call Descope session validation; logic is identical, SDK call changes |
Login / logout routes (2 files) — These handle the Auth0 redirect-based login flow. Descope replaces this with an embedded UI component; no redirect cycle is needed.
| File | What it does today | What changes |
|---|---|---|
| pages/api/auth/[...auth0].ts | Catch-all handler for OAuth callback, logout, session refresh | Deleted — Descope handles this client-side; no server route needed |
Cover all functional groupings. End with: "Total: N files. Estimated code-change effort: N–N hours."
For each Auth0 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 — reason about the intent, not just the API surface. Only recommend SDK code when programmatic control is genuinely required. Example:
Multi-tenancy (Auth0 Organizations → Descope Tenants) Auth0 Organizations group users by company and scope their permissions. Descope has the same concept, called Tenants, and the migration script transfers them automatically. The main difference is how tenant membership appears in the session token — Auth0 uses a flat
org_idstring, while Descope uses a nestedtenantsobject that includes per-tenant roles. Any backend code that readsreq.auth.org_idwill need to be updated to readtoken.tenants. This is a predictable, mechanical change. Effort: Medium (1–2 hours). The migration script handles the data; code changes are localized to token-reading logic.
Only include confirmed features.
Some Descope behavior is configured in the console, not in code. List every item that must be set up before the app works, as checkboxes with a plain description of what it is, why it's needed, and roughly how long it takes. Group into "Required before any testing" and "Required before production":
Required before any testing:
sign-up-or-in flow
works for most apps and requires no customization to start.Required before production:
admin, member (or whatever the codebase references) — Descope
roles must exist in the console before code that assigns them will work.Diff table with plain-English notes for each removal and addition:
| Remove | Add | Why |
|---|---|---|
| AUTH0_CLIENT_ID | — | Auth0 identifies apps by client ID. Descope uses a Project ID instead — simpler, and shared across all apps in a project. |
| AUTH0_CLIENT_SECRET | — | Not needed. Descope's browser-side flow doesn't require a secret. |
| AUTH0_ISSUER_BASE_URL | — | The Auth0 tenant URL. Replaced by the Project ID. |
| AUTH0_AUDIENCE | — | Used by Auth0 for API access scoping. Can be replicated in Descope via token templates if needed. |
| SECRET | — | Used by Auth0's SDK to encrypt server-side sessions. Descope doesn't use server-side sessions. |
| — | 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 the login component. |
| — | DESCOPE_MANAGEMENT_KEY | Only needed if the app manages users, roles, or tenants server-side. |
Follow with: "Net change: 5 variables removed, 1–3 added. No secrets need to be rotated on the Auth0 side — those credentials stop being used."
Prose strategy first, then steps. Start with: "X existing users need to be in Descope before cutover." Describe:
End with a brief checklist of the migration steps at the level a PM can track:
Things that could affect timeline, user experience, or scope. Write each in plain English with three parts: what it is, what breaks if it's ignored, and what to do. Format each as a named callout:
Risk: User profile data won't appear after login until a token template is configured Descope session tokens don't include name, email, or profile photo by default. Any part of the app that displays user information — profile pages, nav bars, greeting text — will show blank values after migration until the token template is set up in the Descope console. This is a one-time configuration step, not a code change. Action: Configure the token template in the Descope console before running any tests. Estimated time: 10 minutes.
Risk: Password migration requires an Auth0 support request If the app supports password-based login, users' hashed passwords must be exported from Auth0 and imported into Descope. Auth0 only releases these via a support ticket, which can take several days to fulfill. Action: Open the support ticket now, in parallel with development work. Without it, password users will need to reset their passwords after cutover.
Include only applicable risks.
Open with one sentence: phases run in sequence; steps within a phase can run in parallel. Then labeled phases, each with a time estimate:
Phase 1 — Console Setup (~30–45 minutes, no code required) Can be done by any team member with Descope console access, in parallel with other work.
sign-up-or-in to start)Phase 2 — Code Changes (~X–Y hours, 1 engineer) Work through files in the order listed. Run a compile check after each group.
pages/api/auth/[...auth0].ts — no replacement needed (5 min).env.example and CI config (15 min)lib/auth.ts session helper (30 min)_app.tsx — swap UserProvider for Descope AuthProvider (15 min)lib/logout.ts — two-step logout (15 min)Phase 3 — User Migration (~1–2 hours, includes dry run) Run against dev/staging first. Do not run against production until Phase 4 passes.
Phase 4 — Testing (~30–45 minutes)
Phase 5 — Production Cutover
Total estimated engineering effort: N–N hours across N engineers. Blocking dependencies: (list anything on the critical path — support tickets, console access, etc.)
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 plan in MIGRATION-PLAN.md Section 8 order. 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. It's the
source of truth for migration state — keep it current throughout execution.
# 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.]
- Migration path: [Path A: OIDC compat / Path B: Full native]
- Migration goal: [Full cutover / Phased / Evaluating]
## Triage Answers
- Existing users: [Yes — N users / No — greenfield]
- Password migration needed: [Yes / No]
- Auth0 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 |
|---|---|---|
| `pages/api/auth/[...auth0].ts` | Delete | ⬜ Pending |
| `lib/auth.ts` | Rewrite session helper | ⬜ Pending |
| `middleware.ts` | Update session check | ⬜ Pending |
## Console Setup Checklist
- [ ] Descope project created — Project ID: (fill in when done)
- [ ] JWT template configured
- [ ] Roles created: (list roles)
- [ ] Social providers configured: (list providers)
## Decisions Log
_Non-obvious decisions made during migration — preserves rationale if context is lost._
_(none yet)_
## Current Phase
Phase 1 — Console Setup (not started)
## Next Action
Complete console setup per MIGRATION-PLAN.md before making any code changes.
## 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.
If the framework, migration path, triage answers, or next step aren't clear from the conversation, re-read both files before proceeding. Then output a context line:
Migration context: Next.js 14 · Path B · Phase 2, step 3/8 · Next: rewrite lib/auth.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. Do this before the next step.
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. This is faster and more reliable than reading type declarations. 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. For Go, run go doc. For Python, check the SDK stubs.
Prefer local node_modules/ over GitHub when reading type declarations. Installed packages reflect the exact version in use. If the Descope package isn't installed yet, install it first, then read local type declarations. Only fall back to GitHub if the package can't be installed in the current environment.
This applies to every SDK call you write, not just the first import. Field names on
option objects (sendMail vs sendEmail), hook return shapes (useDescope() returns the
SDK directly, not { sdk }), and subpath exports (/client vs root) differ just as often.
1a. After rewriting any module, grep for remaining imports of the removed package.
grep -r "from '@/lib/auth0'\|from '@auth0/" --include="*.ts" --include="*.tsx" .
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. Auth0'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 (or go.mod, requirements.txt) 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. The cascade can
span 10–20 files.
5. Verify published package versions before writing to package.json or running npm install.
Don't reuse Auth0's version number or rely on training data for versions. Before writing any
install command:
npm view @descope/node-sdk version
npm view @descope/nextjs-sdk version
If npm is unavailable, leave the version as "latest" and flag it.
Several steps require Descope Console setup that can't be done in code. The app compiles without them but won't work at runtime.
Use AskUserQuestion to ask whether they already have a Project ID and working Flow. If
yes, skip to verifying items 5–7 — these are easy to miss even for existing projects.
P (e.g. P2abc123...).NEXT_PUBLIC_DESCOPE_PROJECT_ID. For all server-side SDKs, it's DESCOPE_PROJECT_ID.Required for: user management API, role/permission management, tenant operations, ReBAC (FGA), Outbound Apps, SCIM configuration. If the app does any server-side user or tenant management, they need this.
DESCOPE_MANAGEMENT_KEY. Treat like a secret — never expose client-side.A Flow is the auth UI sequence. Reference it by Flow ID in the web component.
references/flows-and-widgets.md → Flows.references/implementation-nuances.md → MFA section.references/implementation-nuances.md → Social login / SSO section.Auth0 includes email, name, and picture in tokens by default. Descope does not.
{"email": "{{user.email}}", "name": "{{user.name}}", "picture": "{{user.picture}}"}token.email
will get undefined after migration.Descope roles are referenced by name, not by ID. They must be created manually in the Console before the code that assigns them will work.
admin, member)Auth0 Organization metadata and User app_metadata map to Descope customAttributes.
Pre-define them in the Console schema 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, Outbound Apps API |
Before migrating custom profile pages, user management pages, or role assignment UI, ask
whether a Descope Widget covers the use case. See references/flows-and-widgets.md → Widgets.
After completing console setup: Update MIGRATION-STATE.md — check off each completed
item in the Console Setup Checklist, record the Project ID in the file, and set Next Action
to the first code change step.
Read references/implementation-nuances.md in two passes before writing any code:
offset to jump directly) — read only the section matching the user's stack:
## Express.js## Flask / Python (FastAPI notes are in the "No drop-in middleware" subsection under General Insights)## Next.js (standalone) + ## Next.js (B2B): Migration Bug Catalog## Next.js (with separate Express API server)## Go + Encore## Agentic AI StacksWhen a new framework is added to the file, add it to this list.
express-openid-connect; add @descope/node-sdk + cookie-parserapp.use(auth(config)) with ~20-line custom middleware reading the DS cookie
and calling descopeClient.validateSession()/login route rendering <descope-wc> web component (EJS, plain HTML, etc.)descopeClient.logout(refreshToken) + clear DS/DSR cookiesKey gotchas:
express-openid-connect handled CSRF and cookie parsing internally. You need
cookie-parser explicitly.req.oidc.user → req.user (set from validated JWT claims after validateSession())requiresAuth() is 3 lines of custom code, not an SDK import.authlib; add descope Python SDK/callback route entirely — no code exchange needed/login renders the Descope web component instead of calling authorize_redirect()descope_client.logout(refresh_token) + delete cookiessession; state lives in DS/DSR cookiesKey gotchas:
authlib stored access_token, id_token, userinfo in Flask server-side session.
Descope doesn't use Flask sessions. Drop APP_SECRET_KEY and session imports.validate_session() returns a dict of JWT claims. Profile fields aren't there by
default — configure a JWT Template first.@auth0/nextjs-auth0 → @descope/nextjs-sdk + @descope/node-sdkUserProvider → AuthProvider (takes projectId prop; must use NEXT_PUBLIC_ prefix)useUser() → useSession() + useUser() (Descope separates session state from user data)pages/api/auth/[...auth0].tsx catch-all — no server-side OIDC handling/login page with <Descope> component rendering sign-up-or-in flow; always wire onSuccess — the component does not auto-redirect (see references/implementation-nuances.md → Next.js section)withPageAuthRequired → manual useSession() check + redirectwithApiAuthRequired → call session() at handler top, return 401 manuallysdk.logout() via useDescope() hook (not a link to /api/auth/logout)Client vs. server session access — common source of errors:
session() from @descope/nextjs-sdk/server — server components, server actions, API routes onlyuseSession() + useUser() from @descope/nextjs-sdk/client — React client components
useSession() returns { isAuthenticated, sessionToken, ... }useUser() returns the user object from the current sessionsession() in a client component compiles but throws at runtime (attempts to read cookies in a browser context). Scan for this pattern before finishing any Next.js migration.Server-side session — exact SDK API (verify before generating):
The @descope/nextjs-sdk/server entry exports exactly:
session(config?) — reads session from request headers/cookies in a server component or server action. No req argument. Returns Promise<AuthenticationInfo | undefined>.getSession(req, config?) — reads from an explicit NextApiRequest. API routes only.authMiddleware(options) — Next.js middleware factory.getServerSession does not exist. The name looks plausible but isn't exported. Before writing any import, open node_modules/@descope/nextjs-sdk/dist/types/server/index.d.ts and confirm the export list.
Session return type — AuthenticationInfo, not an Auth0-style session object:
session() returns AuthenticationInfo | undefined from @descope/node-sdk:
interface AuthenticationInfo {
jwt: string // raw session JWT
token: Token // decoded claims: { sub?, exp?, iss?, [claim: string]: unknown }
cookies?: string[]
}
There is no isAuthenticated, no claims field, and no user wrapper. Write an adapter function instead:
import { session as sdkSession } from "@descope/nextjs-sdk/server"
export async function getDescopeSession() {
const authInfo = await sdkSession()
if (!authInfo) return null
return { isAuthenticated: true as const, jwt: authInfo.jwt, token: authInfo.token }
}
Then generate all server components using getDescopeSession() from this local file, not from the SDK directly.
For apps with a separate API server (Express):
express-jwt + jwks-rsa; replace with descopeClient.validateSession()DS cookie as Authorization: Bearer <DS> from Next.js to the APIauth0-fastapi (AuthConfig, auto-mounted /api/auth/* routes, require_session)TokenVerifier class: reads Authorization header, validates against
Descope JWKS, attaches claims as FastAPI Security() dependencyDescope exposes standard OIDC endpoints. If the app uses an OIDC client library
(express-openid-connect, go-oidc, authlib, @auth0/nextjs-auth0 v4, etc.), it can
point at Descope's OIDC issuer instead of Auth0's with minimal code changes:
| Endpoint | Auth0 | Descope |
|---|---|---|
| Issuer | https://YOUR_DOMAIN.auth0.com | https://api.descope.com |
| Authorization | https://YOUR_DOMAIN.auth0.com/authorize | https://api.descope.com/oauth2/v1/authorize |
| Token | https://YOUR_DOMAIN.auth0.com/oauth/token | https://api.descope.com/oauth2/v1/token |
| UserInfo | https://YOUR_DOMAIN.auth0.com/userinfo | https://api.descope.com/oauth2/v1/userinfo |
| JWKS | https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json | https://api.descope.com/__ProjectID__/.well-known/jwks.json |
@auth0/nextjs-auth0 v4 (Auth0Client) config for Descope:
new Auth0Client({
domain: `https://api.descope.com/${DESCOPE_PROJECT_ID}`,
clientId: DESCOPE_OIDC_CLIENT_ID, // from Descope Console → Applications → OIDC App
clientSecret: DESCOPE_OIDC_CLIENT_SECRET,
logoutStrategy: "oidc", // prevents calling Auth0's /v2/logout
secret: SESSION_ENCRYPTION_SECRET, // still needed for server-side session encryption
})
Auth0-specific params that do not carry over to Descope via OIDC:
screen_hint: "signup" — Auth0-specific, ignored by Descopeorganization: orgId — Auth0 org-scoped login has no OIDC equivalent in DescopeappClient.updateSession() — no equivalent; session is read-only after loginGood for: Teams that want to swap the IdP first, then refactor to Descope-native SDKs later. Preserves existing OIDC client code.
Caveats: Claim shapes differ, token lifetimes may differ, and Auth0 Actions must be rebuilt in Descope Flows regardless of path.
For B2B apps using Auth0 Organizations: Path A preserves roughly 20% of the work (middleware,
getSession(), session routes); the remaining 80% (management SDK, org-scoped login, signup flow, claim mapping) requires full migration regardless. Path A savings are minimal for B2B workloads — account for this when estimating effort.
go-oidc + golang.org/x/oauth2; add descope/go-sdkdescopeClient.Auth.ValidateSessionWithToken(ctx, token) returns
(bool, *descope.Token, error). Token.Claims is map[string]interface{}sub claim maps directly to your auth handler's user IDAfter completing framework code changes: Update MIGRATION-STATE.md — mark each
modified file as Done in the Files Inventory, update Current Phase and Next Action, and
log any non-obvious decisions made (adapter types kept, async cascade scope, etc.).
Scan for Auth0 references in non-code files after updating source files.
.env.example / .env.template / .env.sample# REMOVE
AUTH0_CLIENT_ID=
AUTH0_CLIENT_SECRET=
AUTH0_ISSUER_BASE_URL=
AUTH0_AUDIENCE=
AUTH0_BASE_URL=
SECRET=
# 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 (only if using management SDK)
Run grep -r "AUTH0" to find all env var references — .env.example, Docker, CI, shell scripts.
Search all .md files for Auth0 references. At minimum, update:
Check Dockerfile, docker-compose.yml, .github/workflows/, and any CI config for
AUTH0_* env var declarations. Update them to DESCOPE_*.
Auth0 CLI commands (auth0 tenants patch, auth0 actions create/deploy, auth0 roles create, etc.) have no Descope CLI equivalent. When the migration includes a setup or seed script (e.g., scripts/bootstrap.mjs, scripts/seed.ts), split it into two parts:
MIGRATION-PLAN.md.management.role.create()), tenant creation, access key provisioning. Preserve these as a Node.js/Python script using the Descope Management SDK.Auth0 Actions deployed by the script need to be re-evaluated: each Action's logic maps to a Flow step, Scriptlet, or Connector in the Console — not a code deployment. Use AskUserQuestion if the intent of any Action is ambiguous.
After completing non-code file updates: Update MIGRATION-STATE.md — mark env files,
README, and CI config done in the Files Inventory, and advance Next Action.
| Auth0 pattern | Descope equivalent | |---|---| | Custom claims in tokens | JWT Templates | | Custom logic during auth | Descope Flows | | Post-login webhooks | Flows → Connectors | | Role assignment at login | Flow actions → RBAC role assignment steps |
Before migrating MFA enrollment: Many Auth0 apps have a separate MFA enrollment page because Auth0 Guardian works via a server-generated enrollment ticket URL. That pattern has no Descope equivalent — and a separate page is rarely the right approach in Descope.
The Descope approach: add an MFA step directly to the sign-up/sign-in Flow — enrollment happens inline during the auth journey. Or embed MFA as a subflow (triggered by a condition: user is admin, risk score exceeds a threshold, etc.). Or use the step-up Flow template to gate sensitive operations.
Action: Before writing any MFA enrollment code, use AskUserQuestion to confirm whether MFA can be integrated into the main sign-in Flow. See references/flows-and-widgets.md → MFA enrollment section.
| Auth0 | Descope |
|---|---|
| req.auth.permissions.includes('read:messages') | token.permissions.includes('read:messages') |
| Role claim via namespace in Actions | roles array in JWT (built-in) |
| M2M token for Management API | DESCOPE_MANAGEMENT_KEY for management SDK |
SDK: descopeClient.management.role.create(name, description, permissionNames, tenantId)
org_id (flat string) → Descope tenants (nested object: { tenantId: { roles, permissions } })Preferred approach — SSO Setup Suite: Before migrating management SDK SSO calls, ask whether the SSO Setup Suite removes the need for that code. The SSO Setup Suite is a no-code Console wizard that guides tenant admins through per-tenant SAML/OIDC configuration with step-by-step IdP-specific instructions (Okta, Azure AD, Google Workspace, etc.) — no engineering involvement needed for new tenant SSO onboarding.
Use AskUserQuestion to ask: does this app need programmatic SSO configuration (CI/CD provisioning, API-driven tenant onboarding), or do tenant admins configure SSO themselves through a settings page? If the latter, the SSO Setup Suite + Tenant Profile Widget may eliminate the need for sso.configureSAMLByTenant() / configureOIDCByTenant() calls entirely.
See references/flows-and-widgets.md → SSO Setup Suite.
SDK path (when programmatic SSO is needed):
| Auth0 | Descope |
|---|---|
| connections.create({ strategy: "samlp" }) | management.sso.configureSAMLByTenant(tenantId, settings) |
| connections.create({ strategy: "oidc" }) | management.sso.configureOIDCByTenant(tenantId, settings) |
| Per-org SAML connection | Per-tenant SSO (Console → SSO or Management SDK) |
Schema translation example:
# Auth0/OpenFGA
type doc
relations
define owner: [user]
define viewer: [user, user:*]
define can_view: owner or viewer
# Descope ReBAC DSL
type doc
relation owner: user
relation viewer: user
permission can_view: owner or viewer
API shape differences:
{ user, relation, object } tuples{ target, targetType, relation, resource, resourceType } — explicit typed fields| Operation | Auth0 FGA | Descope ReBAC |
|---|---|---|
| Write relation | fgaClient.write({ writes: [...] }) | descopeClient.management.fga.createRelations([...]) |
| Check | fgaClient.check({ user, relation, object }) | descopeClient.management.fga.check([...]) |
| List objects | fgaClient.listObjects(...) | descopeClient.management.authz.whatCanTargetAccessWithRelation(...) |
Note: FGARetriever from @auth0/ai-langchain has no Descope equivalent. Build a custom
retriever that calls descopeClient.management.fga.check() per candidate document.
Users connect accounts via sdk.outbound.connect(appId, { redirectURL, scopes }) on the client.
Fetch stored tokens server-side:
POST https://api.descope.com/v1/mgmt/outbound/app/user/token
Authorization: Bearer {projectId}:{managementKey}
Body: { "appId": "google-calendar", "userId": "U2abc...", "scopes": [...] }
No AI-framework wrapper exists (withTokenVault() from @auth0/ai has no equivalent).
Build a custom tool wrapper that calls the Outbound Apps API directly.
Descope has no CIBA equivalent. Recommended approach:
This is the highest-complexity migration item.
Auth0 M2M apps use the client credentials grant. Descope's equivalent is
Access Keys — create one in
Console → Access Keys, exchange it for a JWT via descopeClient.auth.exchangeAccessKey(),
and validate the resulting token the same way as user tokens.
Descope provides a Python CLI tool — descope/descope-migration — that handles bulk import of users, roles, permissions, and Auth0 organizations (→ Descope tenants) in one run.
Two import modes:
Setup:
git clone [email protected]:descope/descope-migration.git
cd descope-migration
python3 -m venv venv && source venv/bin/activate
pip3 install -r requirements.txt
cp .env.example .env
Required .env variables:
| Variable | Where to get it |
|---|---|
| AUTH0_TOKEN | Auth0 Management API → token explorer (24h token) |
| AUTH0_TENANT_ID | Your Auth0 dashboard URL |
| DESCOPE_PROJECT_ID | Descope Console → Project Settings |
| DESCOPE_MANAGEMENT_KEY | Descope Console → Company → Management Keys |
Always dry-run first:
python3 src/main.py auth0 --dry-run
python3 src/main.py auth0 --dry-run --from-json ./export.json --with-passwords ./password_hashes.json
What gets migrated: users, roles, permissions, Auth0 organizations → Descope tenants.
Auto-created custom attributes:
connection (text) — the Auth0 connection type for each userfreshlyMigrated (boolean) — set to true on import; use this in Flow conditionals to give newly migrated users a special first-login experience, then flip it to false once doneSession migration (beta): for zero-disruption cutovers, active Auth0 sessions can be exchanged for Descope tokens without re-authenticating. Requires users to already exist in Descope (import first). See session migration docs.
Large user bases (10,000+) — just-in-time migration via Auth0 Action: For large populations where a bulk export/import would be disruptive, Auth0 supports creating an Action that forwards user data to Descope on each login during a cutover window — users migrate gradually, just-in-time, without a forced re-login. This approach requires coordination with the Descope Customer Success team. Flag this if the user wants zero-disruption cutover.
Auth0 email templates map to Descope Messaging Templates, configured per authentication method in the Console.
Auth0 Log Streams map to Descope's Audit Webhook Connector. Set this up before cutover to avoid gaps in event logging.
CNAME auth.example.com → cname.descope.com, verify in Console, then pass baseUrl to
the Descope SDK.
Auth0 Attack Protection maps to Descope Flow steps using security connectors: Arkose Bot Manager, Google reCAPTCHA, Fingerprint, Have I Been Pwned, AbuseIPDB. These are composable (add detection steps to Flows) rather than toggle-based — not configured by default.
After completing feature migration: Update MIGRATION-STATE.md — record which features
were migrated, mark any that were deferred or require follow-up, and advance Next Action to
testing.
Descope session JWTs contain sub, amr, drn, tenants, roles, permissions, and dct by
default. They do not contain email, name, or picture. Auth0 ID tokens include
these by default.
dct (Descope Current Tenant) is a flat string holding the active tenant ID — the direct equivalent of Auth0's org_id. For apps where a user is always in a single tenant context, token.dct is simpler to read than iterating token.tenants. Use token.tenants when you need per-tenant roles or permissions (it is a keyed object: { [tenantId]: { roles, permissions } }); use token.dct when you only need the tenant ID.
Action required: Configure a JWT Template in the Descope Console to add email,
name, and any other profile fields the app reads from the token.
Descope session tokens have no aud claim by default. Apps using AUTH0_AUDIENCE for
API access control must:
aud claim in JWT Templatesaudience to validateSession() on the backenddescopeClient.logout(refreshToken) to invalidate server-sideDS and DSR cookiesSkipping either step leaves a broken state.
Auth0's appClient.updateSession() has no direct server-side equivalent in Descope. Profile changes via the Management SDK don't update the JWT already in the browser. Four options:
useDescope().refresh() client-side — triggers an immediate token refresh. Requires the profile form to be a client component with a useDescope() hook. Full code pattern in references/implementation-nuances.md → Session refresh section.POST /v1/mgmt/user/jwt/update) — server-side; updates stored JWT custom claims for a specific user. Verify current behavior against Descope docs before using — this updates stored claims, not the live session token.references/flows-and-widgets.md → Widgets.Session change event listeners: Instead of calling refresh() imperatively, the Descope client SDK exposes auth state change events — use these to react to session updates across components. See docs.descope.com/client-sdk/auth-helpers#handling-authentication-state-changes.
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.
Auth0 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 express-openid-connect equivalent package. The middleware is ~20 lines
of custom code.
cookies() and headers() Are Async in Next.js 15cookies() and headers() from next/headers return a Promise in Next.js 15+. Before
generating any server-side helper that reads cookies:
package.json for the Next.js version.await cookies() and mark the containing function async.When a shared utility becomes async, TypeScript accepts await on non-Promises without
error — so callers that forget await silently return a Promise object. Always grep for
all call sites of any utility you make async and update them in the same pass.
Auth0: CLIENT_ID, CLIENT_SECRET, ISSUER_BASE_URL, SECRET, AUTH0_AUDIENCE (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 "@auth0\|auth0\|express-openid-connect\|nextjs-auth0" \
--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
mvn compile -q # Java/Maven
./gradlew compileJava compileKotlin # Java/Gradle
dotnet build # .NET
Do not proceed until compilation exits with zero errors.
If compilation fails, diagnose by error message:
Cannot find module '@auth0/...' → stale import; re-run Phase 0Property 'X' does not exist on type 'AuthenticationInfo' → wrapper built against Auth0 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: python main.py / go run . / flask run / etc.
npm test # or: pytest / go test ./... / etc.
Auth-related test failures usually mean: a mock or fixture still uses Auth0 shapes, or a
test validates JWT claims that are now missing (e.g., email without a JWT Template).
# 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 any other expected claims are present.
## 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
**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 Auth0 concept to its Descope replacement
Behavioral differences and open questions — numbered list of significant differences between the Auth0 and Descope implementations. For each item: Auth0 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 — these are the things easiest to forget because the code compiles without them.
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 (FGA, CIBA, AI tooling), 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 several frameworks.development
Use this skill whenever anyone asks about migrating from WorkOS to Descope — whether they're a developer doing it themselves or a technical lead evaluating the move. Triggers on: "how do I migrate from WorkOS", "replace WorkOS with Descope", "we're moving off WorkOS", "WorkOS to Descope", "switch from WorkOS", "our app uses @workos-inc/node / @workos-inc/authkit-nextjs / AuthKit / WorkOS SSO / Directory Sync / SCIM and we want to use Descope instead", or any question about WorkOS features (AuthKit, Organizations, Enterprise SSO, Directory Sync/SCIM, Admin Portal, RBAC, FGA, Audit Logs, Radar, Pipes) 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.
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
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.
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.