From 75433d9f8d2d0bb8e749772ea9863b9ebffc90d2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:15:45 +0000 Subject: [PATCH] chore: sync Mintlify agent skills --- main/.mintlify/skills/auth0/SKILL.md | 24 +- .../auth0/references/feature-mfa/index.md | 17 +- .../references/feature-organizations/index.md | 113 +- .../references/framework-android/index.md | 101 +- .../framework-auth0-auth-js/index.md | 163 ++ .../framework-auth0-server-js/index.md | 176 ++ .../framework-flutter-native/index.md | 2 + .../references/framework-flutter-web/index.md | 1 + .../framework-flutter-windows/index.md | 551 ++++++ .../references/framework-ionic-react/index.md | 2 +- .../references/framework-ionic-vue/index.md | 2 +- .../references/framework-node-auth0/index.md | 49 +- .../framework-node-auth0/migration.md | 1671 +++++++++++++++++ .../auth0/references/framework-react/index.md | 313 +-- .../framework-server-python/index.md | 700 +++++++ .../references/framework-spa-js/index.md | 31 +- .../auth0/references/framework-swift/index.md | 22 +- .../skills/auth0/scripts/validate-skill.sh | 2 +- 18 files changed, 3403 insertions(+), 537 deletions(-) create mode 100644 main/.mintlify/skills/auth0/references/framework-auth0-auth-js/index.md create mode 100644 main/.mintlify/skills/auth0/references/framework-auth0-server-js/index.md create mode 100644 main/.mintlify/skills/auth0/references/framework-flutter-windows/index.md create mode 100644 main/.mintlify/skills/auth0/references/framework-node-auth0/migration.md create mode 100644 main/.mintlify/skills/auth0/references/framework-server-python/index.md diff --git a/main/.mintlify/skills/auth0/SKILL.md b/main/.mintlify/skills/auth0/SKILL.md index 39580c1556..ea0c192402 100644 --- a/main/.mintlify/skills/auth0/SKILL.md +++ b/main/.mintlify/skills/auth0/SKILL.md @@ -4,7 +4,7 @@ description: Use when adding, fixing, or improving how an app authenticates user license: Apache-2.0 metadata: author: Auth0 - version: '2.1.1' + version: '2.2.0' openclaw: emoji: "\U0001F510" homepage: https://github.com/auth0/agent-skills @@ -94,13 +94,20 @@ SDK, so check the `@capacitor/browser` rows before it. | `express-oauth2-jwt-bearer` | `express-jwt` | | `react-native-auth0` + `app.json` or `app.config.js` present | `expo` | | `react-native-auth0` (no Expo files) | `react-native` | +| `@auth0/auth0-server-js` | `auth0-server-js` | +| `@auth0/auth0-auth-js` | `auth0-auth-js` | | `auth0` (the bare package, not `@auth0/*`) | `node-auth0` | ### Python — check `requirements.txt` or `pyproject.toml` +Rows are most-specific first: `auth0-server-python` is the framework-agnostic +server core, so check for a co-installed web framework before falling back to the +bare-SDK row. + | Package | Framework | |---|---| -| `auth0-server-python` | `flask` | +| `auth0-server-python` + `flask` | `flask` | +| `auth0-server-python` (no Flask web framework) | `server-python` | | `auth0-fastapi-api` | `fastapi-api` | ### Java / Kotlin — check `build.gradle` or `pom.xml` @@ -150,6 +157,7 @@ SDK, so check the `@capacitor/browser` rows before it. | `build.gradle(.kts)` + `com.auth0.kmp:auth0` (Kotlin Multiplatform module) | `kmp` | | `Package.swift` or `.xcodeproj` + Auth0.swift | `swift` | | `build.gradle` + `com.auth0.android:auth0` | `android` | +| `pubspec.yaml` + `auth0_flutter` + developer names/targets Windows desktop | `flutter-windows` | | `pubspec.yaml` + `auth0_flutter` + `flutter.web: false` | `flutter-native` | | `pubspec.yaml` + `auth0_flutter` + web enabled | `flutter-web` | @@ -175,13 +183,14 @@ variant is resolved in "Variant disambiguation" below. As in Tier 1, check the | `express` in `package.json` | `express` (variant below) | | `fastify` in `package.json` | `fastify` (variant below) | | `flask` in `requirements.txt`/`pyproject.toml` | `flask` | -| `fastapi` in `requirements.txt`/`pyproject.toml` | `fastapi-api` | +| `fastapi` in `requirements.txt`/`pyproject.toml` | `fastapi-api` (variant below) | | `spring-boot` in `pom.xml`/`build.gradle` | `springboot-api` | | `laravel/framework` in `composer.json` | `laravel` (variant below) | | `composer.json` present (no Laravel) | `php` (variant below) | | `go.mod` present + HTTP server/router | `go` | | `org.jetbrains.kotlin.multiplatform` plugin + `commonMain` source set (shared Android+iOS module) | `kmp` | | `Package.swift` or `.xcodeproj` | `swift` | +| `pubspec.yaml` (Flutter) + developer names/targets Windows desktop | `flutter-windows` | | `pubspec.yaml` (Flutter, web disabled) | `flutter-native` | | `pubspec.yaml` (Flutter, web enabled) | `flutter-web` | | `*.csproj` referencing MAUI | `maui` | @@ -207,11 +216,14 @@ request. **Stop at the first match.** | React SPA (not Next.js) | `react` | | vanilla JS / plain JS / no framework SPA | `spa-js` | | node-auth0 / the `auth0` npm package | `node-auth0` | +| `@auth0/auth0-server-js` / auth0-server-js / server-side Auth0 session SDK | `auth0-server-js` | +| `@auth0/auth0-auth-js` / auth0-auth-js / AuthClient / low-level OAuth OIDC | `auth0-auth-js` | | Express (web app / server-rendered) | `express` | | Express API / protect API routes | `express-jwt` | | Fastify (web) / Fastify API | `fastify` / `fastify-api` | | Flask | `flask` | -| FastAPI | `fastapi-api` | +| FastAPI (web app) / FastAPI API | `server-python` / `fastapi-api` | +| `auth0-server-python` / framework-agnostic Python server SDK / Python OIDC web server with no dedicated reference (Django, Starlette, Sanic, Quart, aiohttp) | `server-python` | | Spring Boot | `springboot-api` | | Java MVC / servlet | `java-mvc` | | ASP.NET Core web app / API | `aspnetcore-auth` / `aspnetcore-api` | @@ -222,7 +234,7 @@ request. **Stop at the first match.** | Kotlin Multiplatform / KMP / shared Android+iOS auth code / `com.auth0.kmp` | `kmp` | | Swift / iOS | `swift` | | Android / Kotlin | `android` | -| Flutter (native / web) | `flutter-native` / `flutter-web` | +| Flutter (native / web / Windows) | `flutter-native` / `flutter-web` / `flutter-windows` | | React Native / Expo | `react-native` / `expo` | | Ionic (Angular/React/Vue) | `ionic-angular` / `ionic-react` / `ionic-vue` | @@ -235,6 +247,7 @@ pin the variant, choose **intent-first**: |---|---|---|---| | express | `express` | `express-jwt` | protecting API routes / validating JWTs, no server-rendered UI | | fastify | `fastify` | `fastify-api` | resource server / JWT validation only | +| fastapi | `server-python` | `fastapi-api` | resource server / JWT validation only; a web app with login/logout UI uses `server-python` | | php | `php` | `php-api` | building/protecting a PHP API, no web UI | | laravel | `laravel` | `laravel-api` | API-only (token guard), no Blade UI | | aspnetcore | `aspnetcore-auth` | `aspnetcore-api` | Web API / JWT bearer, no cookie login UI | @@ -285,7 +298,6 @@ Use references/tooling-{tooling}/index.md for all Auth0 tenant configuration ste ``` Read: references/feature-mfa/index.md Read: references/tooling-{tooling}/index.md -If framework detected: Read references/framework-{framework}/index.md (for SDK-side step-up trigger) ``` ### feature:organizations diff --git a/main/.mintlify/skills/auth0/references/feature-mfa/index.md b/main/.mintlify/skills/auth0/references/feature-mfa/index.md index a7402c480c..967b710a41 100644 --- a/main/.mintlify/skills/auth0/references/feature-mfa/index.md +++ b/main/.mintlify/skills/auth0/references/feature-mfa/index.md @@ -56,13 +56,14 @@ Recipe (do in order): | Your context | Verify with | |---|---| -| Session-managing SDK (web app) | The `amr` claim (contains `mfa` when MFA completed) off the SDK's own session / current-user accessor - already validated, so trust it as-is. Accessor name is SDK-specific -> see the `framework-*` reference. | +| Session-managing SDK (web app) | The `amr` claim (contains `mfa` when MFA completed) off the SDK's own session / current-user accessor - already validated, so trust it as-is. Accessor name is SDK-specific -> see the SDK's own example ("Example code snippets" below). | | Resource API (raw bearer token) | The high-value **scope** (e.g. `transfer:funds`) on the access token, via your *existing* JWT/scope-check middleware - see "Related capabilities". | | Frontend | Nothing - treat any `amr` check as UX, never enforcement. | Notes: a silent token request may instead surface an `mfa_required` error - handle it by re-authenticating interactively. Never re-decode or re-verify the token by hand (see -"Common mistakes"). `amr` is not a reliable contract for *which* factor ran; to enforce a +"Common mistakes"). Some session SDKs persist only a default subset of ID-token claims, so +`amr` can be missing from the accessor until you opt it in (see "Common mistakes"). `amr` is not a reliable contract for *which* factor ran; to enforce a **specific** factor, have the post-login Action read `event.authentication.methods[].type` into a custom claim. Action-defined MFA overrides the Guardian policy (it refines, not replaces) and differs from **Adaptive MFA** (the Guardian `confidence-score` policy, owned @@ -73,7 +74,7 @@ not relax it. The app collects credentials, so no browser runs the challenge: sign-in returns an `mfa_required` error carrying an `mfa_token`. Each step is a method on the SDK's own MFA -client - get exact names from the `framework-*` example; never hand-roll the token grant +client - get exact names from the SDK's own example ("Example code snippets" below); never hand-roll the token grant or the MFA API URLs. Recipe (do in order): @@ -116,7 +117,7 @@ and what the app must get right: SDK-specific symbols (an SDK's own method or option name - e.g. the silent-token call, the `mfa_required`/`MfaRequiredError` handling, the interactive re-auth option, refresh-token -requirements) are **not** listed here; they belong in the relevant `framework-*` reference. +requirements) are **not** listed here; get them from the SDK's own example (see "Example code snippets"). ### `amr` claim values @@ -164,7 +165,7 @@ language-neutral mechanic above. Never substitute a web search for "how to do MF | `@auth0/auth0-angular` (MFA) | https://raw.githubusercontent.com/auth0/auth0-angular/main/EXAMPLES.md | `## Multi-Factor Authentication (MFA)` | | `@auth0/auth0-angular` (step-up) | https://raw.githubusercontent.com/auth0/auth0-angular/main/EXAMPLES.md | `## Step-Up Authentication` | | `@auth0/auth0-spa-js` (step-up) | https://raw.githubusercontent.com/auth0/auth0-spa-js/main/examples/step-up-authentication.md | whole file | -| `@auth0/nextjs-auth0` | https://raw.githubusercontent.com/auth0/nextjs-auth0/main/EXAMPLES.md | `## Multi-Factor Authentication (MFA)` | +| `@auth0/nextjs-auth0` | https://raw.githubusercontent.com/auth0/nextjs-auth0/main/guides/mfa.md | whole file | | `@auth0/auth0-auth-js` | https://raw.githubusercontent.com/auth0/auth0-auth-js/main/packages/auth0-auth-js/examples/mfa.md | whole file | | `@auth0/auth0-server-js` | https://raw.githubusercontent.com/auth0/auth0-auth-js/main/packages/auth0-server-js/MFA.md | whole file | | `Auth0.swift` (iOS/macOS) | https://raw.githubusercontent.com/auth0/Auth0.swift/master/examples/mfa-api.md | whole file | @@ -243,6 +244,7 @@ which uses the `mfa_token` and the MFA API surface instead: | Trusting a frontend MFA check | The client can be bypassed entirely | Enforce server-side: `amr` on a web/session backend, the high-value scope on a resource API | | Checking `amr` on a resource API's access token | Access tokens carry no `amr` by default, so valid stepped-up callers are rejected | Gate the API on the high-value scope; add `amr` as a custom claim only if this API also validates it | | Hand-decoding the token to read `amr` (`jwt.decode`, `PyJWKClient`, `id_token.split`, manual JWKS) | Reinvents validation the SDK already performed, and usually disables `exp`/`iss`/audience checks in the process | Read `amr` from the SDK's session/current-user accessor; its claims are already verified | +| Assuming the session accessor always carries `amr` | Some SDKs persist only a default claim subset, so `amr` is silently absent and the check never passes | Opt the claim in: `@auth0/nextjs-auth0` v4 needs a `beforeSessionSaved` hook to copy `amr` into the session (`session.user.amr`); `express-openid-connect` keeps the full claims on `req.oidc.idTokenClaims`, not the filtered `req.oidc.user` | | Omitting `max_age=0` on step-up | A still-valid session satisfies the request with no fresh challenge | Send `max_age=0` (or the SDK's fresh-auth option) for step-up | | Ignoring `mfa_required` from a silent token call | The step-up silently fails and the action proceeds unverified | Catch it and re-authenticate interactively | | Preferring SMS by default | SMS is vulnerable to SIM-swap | Prefer TOTP or WebAuthn; treat SMS as a fallback | @@ -255,8 +257,9 @@ which uses the `mfa_token` and the MFA API surface instead: - **Tenant setup and Actions** - `tooling-cli` and `tooling-terraform` own Guardian factor/policy configuration and Action deployment (`auth0 actions ...`). -- **SDK-side step-up trigger** - the detected `framework-*` reference owns the SDK's own - step-up call, its `mfa_required` handling, and any refresh-token requirement. +- **SDK-side step-up trigger** - the SDK's own step-up call, its `mfa_required` handling, + and any refresh-token requirement live in that SDK's own example (see "Example code + snippets"), not in a `framework-*` reference. - **Server-side MFA enforcement** - the API `framework-*` references (JWT validation) own the scope/claim-check middleware; on a resource API gate the sensitive endpoint on the high-value scope (access tokens carry no `amr` by default), and on a web/session backend diff --git a/main/.mintlify/skills/auth0/references/feature-organizations/index.md b/main/.mintlify/skills/auth0/references/feature-organizations/index.md index 86ce94f334..9c61d58bd4 100644 --- a/main/.mintlify/skills/auth0/references/feature-organizations/index.md +++ b/main/.mintlify/skills/auth0/references/feature-organizations/index.md @@ -1,6 +1,6 @@ # Auth0 Organizations -Multi-tenant B2B authentication. Organizations let each of your customers have their own isolated user pool, roles, and connections — all within one Auth0 tenant. +Multi-tenant B2B authentication. Organizations let each of your customers have their own isolated user pool, roles, and connections - all within one Auth0 tenant. --- @@ -12,7 +12,7 @@ Use Organizations when you need: - Different login connections per customer (e.g., Okta SSO for CustomerA, Google Workspace for CustomerB) - Organization-scoped invitations and member management -Do NOT use Organizations for consumer apps (B2C). Organizations is a B2B construct — instead, use plain Auth0 connections within a single tenant for B2C, and reserve Organizations for B2B multi-tenant scenarios. +Do NOT use Organizations for consumer apps (B2C). Organizations is a B2B construct - instead, use plain Auth0 connections within a single tenant for B2C, and reserve Organizations for B2B multi-tenant scenarios. --- @@ -32,34 +32,40 @@ Do NOT use Organizations for consumer apps (B2C). Organizations is a B2B constru ### Pass organization at login -All Auth0 SDKs support passing `organization` in `authorizationParams` (or equivalent): +The org-login shape is protocol-level and identical across every SDK. Send the organization +identifier on the `/authorize` request, then read `org_id` back off the returned token. Only +how the SDK takes that identifier differs, and it is one of two modes. Get the exact call from +the **loaded `framework-{framework}/index.md` reference** (the router loads it alongside this +file), not from memory: -**React:** -```javascript -loginWithRedirect({ - authorizationParams: { organization: 'org_xxxxx' } -}); -``` +- **As a login argument** - pass `organization` inside the login call's authorization params, + e.g. `loginWithRedirect({ authorizationParams: { organization: 'org_xxx' } })`. Some SDKs + surface it as a URL param the handler forwards, e.g. `/auth/login?organization=org_xxx`. +- **As a builder option** - set it on the auth request builder before starting login, e.g. + `.organization("org_xxx")` or `.withOrganization("org_xxx")`. -**Next.js (nextjs-auth0 v4):** -```javascript -// Pass org via URL param: /auth/login?organization=org_xxx -// The nextjs-auth0 handler forwards it automatically -``` +Match the detected SDK to whichever mode its own reference uses; do not infer the mode from the +SDK's platform. -**Vue:** -```javascript -loginWithRedirect({ - authorizationParams: { organization: 'org_xxxxx' } -}); -``` +For richer per-SDK examples (org switching, reading org claims) read the SDK's own file, only +the named section (from that heading to the next heading of the same or higher level): -**Express:** -```javascript -app.get('/login/:orgId', (req, res) => { - res.oidc.login({ authorizationParams: { organization: req.params.orgId } }); -}); -``` +| SDK | Raw example file (markdown) | Find section | +|---|---|---| +| `@auth0/auth0-react` | https://raw.githubusercontent.com/auth0/auth0-react/main/EXAMPLES.md | `## Use with Auth0 organizations` | +| `@auth0/auth0-spa-js` | https://raw.githubusercontent.com/auth0/auth0-spa-js/main/examples/organizations.md | `## Organizations` | +| `@auth0/auth0-vue` | https://raw.githubusercontent.com/auth0/auth0-vue/main/EXAMPLES.md | `## Organizations` | +| `@auth0/auth0-angular` | https://raw.githubusercontent.com/auth0/auth0-angular/main/EXAMPLES.md | `## Organizations` | +| `@auth0/nextjs-auth0` | https://raw.githubusercontent.com/auth0/nextjs-auth0/main/EXAMPLES.md | `## Passing authorization parameters` | +| `express-openid-connect` | https://raw.githubusercontent.com/auth0/express-openid-connect/master/EXAMPLES.md | `9. Validate Claims from an ID token before logging a user in` | +| `react-native-auth0` | https://raw.githubusercontent.com/auth0/react-native-auth0/master/EXAMPLES.md | `## Organizations` | +| `Auth0.swift` | https://raw.githubusercontent.com/auth0/Auth0.swift/master/examples/advanced-features/organizations.md | `Log in to an organization` | +| `Auth0.Android` | https://raw.githubusercontent.com/auth0/Auth0.Android/main/examples/organizations.md | `Organizations` | +| `auth0-server-python` | https://raw.githubusercontent.com/auth0/auth0-server-python/main/README.md | `#### Organizations` | + +No matching row? The framework reference loaded alongside this file carries the SDK-specific +org login syntax; fall back to it plus the protocol shape above. +Never hand-roll the authorize URL or decode the token by hand. ### Reading the organization back @@ -92,9 +98,8 @@ app.get('/api/data', checkJwt, (req, res) => { ## Tenant Configuration (via chosen tooling) -See your tooling reference file for the full command syntax. The Auth0 MCP server -exposes **no** organizations tool, so for an MCP-only session fall back to the CLI -or Terraform. +The Auth0 MCP server exposes **no** organizations tool, so use the CLI or Terraform (full +command syntax lives in your tooling reference). | Operation | CLI | Terraform | |---|---|---| @@ -105,21 +110,17 @@ or Terraform. | Assign an org-scoped role | `auth0 api post "organizations//members//roles" --data '{"roles":[""]}'` | `auth0_organization_member_roles` | | Create an invitation | `auth0 orgs invitations create` (see below) | not covered | -Check `auth0 commands orgs --detailed` for the current subcommand surface before -falling back to `auth0 api`, and read flag names off `--help` rather than -inferring them from the field they set. - +Verify subcommands with `auth0 commands orgs --detailed` and read flag names off `--help` +rather than inferring them; use `auth0 api` for anything without a dedicated subcommand. Reading connections back returns a **bare array**, so use `jq '.[]'`, not `jq '.enabled_connections[]'`. ### Finding or creating a login connection -An organization with no enabled connection has no way for its members to log in. -Reuse an existing database connection when the tenant has one, and only create -one when it does not: +Reuse an existing database connection when the tenant has one; create one only if it does not: ```bash -# List database connections in the tenant and pick one explicitly by name — +# List database connections in the tenant and pick one explicitly by name - # the API defines no ordering, so `.[0]` silently grabs an arbitrary connection. auth0 api get "connections?strategy=auth0" | jq -r '.[] | select(.name=="") | .id' @@ -127,11 +128,11 @@ auth0 api get "connections?strategy=auth0" | jq -r '.[] | select(.name=="","strategy":"auth0"}' -# Enable it for the organization — without this, org members have no way to log in. +# Enable it for the organization - without this, org members have no way to log in. auth0 api post "organizations//enabled_connections" \ --data '{"connection_id":"","assign_membership_on_login":true}' -# Enable it for each app that will use it — status false disables. Max 50 per call. +# Enable it for each app that will use it - status false disables. Max 50 per call. auth0 api patch "connections//clients" \ --data '[{"client_id":"","status":true}]' @@ -139,17 +140,16 @@ auth0 api patch "connections//clients" \ auth0 api get "connections//clients" | jq -r '.clients[].client_id' ``` -The read is checkpoint-paginated: `take` defaults to 50 (max 1000), and a `next` -token comes back while more remain, so pass it as `from` until `next` is absent. -If listing to find a match automatically rather than by a known name, page -through all results and fail rather than guess unless exactly one connection -matches. +Both connection reads are checkpoint-paginated (`take` defaults to 50): omit `from` on the +first call, then while the response carries a `next` value pass it as `from` until it is +absent. The lookup above only inspects the first page, so page through all results before +concluding a connection is absent, and fail unless exactly one matches rather than guessing. -`connections//clients` only enables the connection for applications — -it does not make the connection usable by the organization. An org whose -connection is enabled for clients but never added to -`organizations//enabled_connections` still has no way for members to -log in. +A connection added to the organization's `enabled_connections` is what appears at that org's +login prompt and lets members authenticate. Enabling the connection for a client +(`connections//clients`) is a separate setting - it governs the connection's +availability to the app outside the organization context - and is not what enables organization +login. --- @@ -167,7 +167,7 @@ auth0 api patch "clients/" \ --data '{"organization_usage":"allow","organization_require_behavior":"no_prompt"}' # 2. Without this: "A default login route is required to generate the invitation url." -# Read the current value FIRST — the setting is tenant-wide, and you may need +# Read the current value FIRST - the setting is tenant-wide, and you may need # to restore it. An empty response means it's currently unset. auth0 api get "tenants/settings" | jq -r '.default_redirection_uri // ""' @@ -182,7 +182,7 @@ either scheme, so a local-dev URL will not satisfy it. `default_redirection_uri` is **tenant-wide**, not per app or per organization, so setting it changes login behaviour for everything in the tenant. Put the captured value back afterwards if the invitation was the only reason you set -it — restore it to an empty string (`{"default_redirection_uri":""}`), not the +it - restore it to an empty string (`{"default_redirection_uri":""}`), not the literal text `"unset"`, if it was empty before. If you keep the new value, say so in your summary. Silently repointing a shared @@ -206,12 +206,12 @@ The invite link lands on your app carrying **both** an `invitation` and an `orga https://your-app.com/login?invitation={ticket_id}&organization={org_id} ``` -Your app must read **both** params from the URL and forward **both** to the `/authorize` request (the SDK's login call). This is protocol-level behavior — it holds for SPA, mobile, and Regular Web App SDKs alike; only *where* you wire it differs: +Your app must read **both** params from the URL and forward **both** to the `/authorize` request (the SDK's login call). This is protocol-level behavior - it holds for SPA, mobile, and Regular Web App SDKs alike; only *where* you wire it differs: - **SPA / mobile** (public client): read from the browser URL, pass in `authorizationParams` on the login call. - **Regular Web App** (confidential client): read from the server request, pass through the OIDC middleware / challenge params. -**Forward the invitation's own `organization` — do not substitute your app's configured default org.** The invite is scoped to the org it was issued for, which may differ from your default. +**Forward the invitation's own `organization` - do not substitute your app's configured default org.** The invite is scoped to the org it was issued for, which may differ from your default. --- @@ -221,10 +221,9 @@ Your app must read **both** params from the URL and forward **both** to the `/au |---|---| | Forgetting `organization` in `authorizationParams` | Always pass the org identifier at login time | | Not forwarding the `invitation` param when accepting an invite | Read `invitation` + `organization` from the callback URL and forward both to `/authorize` | -| Using your default org for an invitation link | Forward the invite's own `organization` param — it may differ from your configured default | -| Using `org_id` from ID token on backend | Validate from the access token, not ID token | -| Reading the display `org_id` from the access token in a client app | Web apps read `org_id` from the ID token; reserve the access token's `org_id` for API validation | -| Hand-decoding a token to read `org_id` | Use the SDK's claim accessor (`getUser()` / `getIdTokenClaims()` / session user) — the claim is already exposed | +| Using your default org for an invitation link | Forward the invite's own `organization` param - it may differ from your configured default | +| Reading `org_id` from the wrong token | Web/client apps read it from the ID token (display); APIs validate it from the access token (authorization) | +| Hand-decoding a token to read `org_id` | Use the SDK's claim accessor (`getUser()` / `getIdTokenClaims()` / session user) - the claim is already exposed | | Mixing up org `id` (org_xxx) and `name` (slug) | `id` for API calls, `name` for display | | Granting global roles instead of org-level roles | Use the org member roles endpoint, not the user roles endpoint | | Not enabling a connection for the org | `auth0 api post "organizations//enabled_connections"`, or Dashboard → Organization → Connections | diff --git a/main/.mintlify/skills/auth0/references/framework-android/index.md b/main/.mintlify/skills/auth0/references/framework-android/index.md index f6bcfa36be..a383c1dd0c 100644 --- a/main/.mintlify/skills/auth0/references/framework-android/index.md +++ b/main/.mintlify/skills/auth0/references/framework-android/index.md @@ -141,6 +141,7 @@ Add authentication to Android applications using `com.auth0.android:auth0`. | Missing `` | Add the INTERNET permission to `AndroidManifest.xml`. The SDK requires network access for authentication. | | Custom scheme in lowercase | Android requires scheme names to be lowercase. Use `https` (recommended) or lowercase custom scheme like `myapp://callback`. | | Forgetting `.validateClaims()` on direct auth calls | Always call `.validateClaims()` when using `AuthenticationAPIClient` directly (for database, passwordless, or API login). Web Auth validates automatically. | +| Reading ID-token claims via a `credentials.claims` map | No such property exists on Auth0.Android `Credentials`. Standard claims are accessors on `credentials.user` (a `UserProfile`); custom / namespaced claims come from `credentials.user.getExtraInfo()`. | | Storing tokens in SharedPreferences without encryption | Use `SecureCredentialsManager` to store credentials. Never store tokens manually in plain text. The manager encrypts tokens at rest. | | Missing manifest placeholders | Add `manifestPlaceholders = [auth0Domain: "@string/com_auth0_domain", auth0Scheme: "@string/com_auth0_scheme"]` to your `build.gradle` `defaultConfig` block. | @@ -159,7 +160,7 @@ Add authentication to Android applications using `com.auth0.android:auth0`. |-------|---------| | `Auth0` | Entry point for SDK, holds app credentials | | `WebAuthProvider` | OAuth 2.0 login/logout via browser | -| `AuthenticationAPIClient` | Direct API calls (database login, passwordless, MFA) | +| `AuthenticationAPIClient` | Direct API calls (database login, passwordless) | | `SecureCredentialsManager` | Secure storage and retrieval of credentials | | `Credentials` | User tokens and expiration | @@ -171,7 +172,6 @@ Add authentication to Android applications using `com.auth0.android:auth0`. - Require biometric authentication (see the Biometric-Protected Credentials section below) - Database login (see the Database Login section below) - Passwordless authentication (see the Passwordless Authentication section below) -- Handle MFA (see the MFA Handling section below) - Call protected APIs (see the Calling Protected APIs section below) ## References @@ -375,7 +375,6 @@ authentication.login(email, password, realm) authentication.signUp(email, password, username, connection) authentication.passwordlessWithEmail(email, type) authentication.loginWithEmail(email, code) -authentication.mfaClient(mfaToken) ``` ### SecureCredentialsManager @@ -405,11 +404,11 @@ val expiresAt = credentials.expiresAt // Expiration timestamp val scope = credentials.scope // Granted scopes val type = credentials.type // "Bearer" -// ID token claims -val sub = credentials.claims["sub"] // Subject (user ID) -val name = credentials.claims["name"] -val email = credentials.claims["email"] -val emailVerified = credentials.claims["email_verified"] +// ID token claims — Auth0.Android has NO credentials.claims property. +// Standard claims are accessors on credentials.user (a UserProfile); +// custom / namespaced claims come from getExtraInfo(). +val sub = credentials.user.getId() // Subject (user ID) +val roles = credentials.user.getExtraInfo()["https://myapp.example.com/roles"] as? List<*> ``` ### LocalAuthenticationOptions @@ -684,7 +683,7 @@ authentication.login( override fun onFailure(error: AuthenticationException) { when { error.isMultifactorRequired -> { - // MFA required - see MFA Handling section + // MFA required — see the feature:mfa reference } error.statusCode == 403 -> { // Invalid credentials @@ -839,86 +838,6 @@ manager.getCredentials(object : Callback { - override fun onFailure(error: AuthenticationException) { - if (error.isMultifactorRequired) { - val mfaToken = error.mfaRequiredErrorPayload?.mfaToken - // Proceed to enrollment or challenge screen - } - } - }) -``` - -### Enroll in MFA - -```kotlin -val mfaToken = error.mfaRequiredErrorPayload?.mfaToken ?: return -val mfaClient = authentication.mfaClient(mfaToken) - -// Enroll in OTP -mfaClient.enroll(MfaEnrollmentType.Otp) - .start(object : Callback { - override fun onSuccess(enrollment: MfaEnrollment) { - val recoveryCode = enrollment.recoveryCode - val secret = enrollment.secret // For OTP app - // Show QR code to user - } - - override fun onFailure(error: AuthenticationException) { - Log.e("MFA", error.message.orEmpty()) - } - }) -``` - -### Challenge MFA - -```kotlin -mfaClient.challenge( - authenticatorId = "dev_abc123", // From enrollments list - challengeType = MfaChallengeType.OTP -) - .start(object : Callback { - override fun onSuccess(challenge: MfaChallenge) { - val challengeId = challenge.challengeId - // Show user OTP input screen - } - - override fun onFailure(error: AuthenticationException) { - Log.e("MFA", error.message.orEmpty()) - } - }) -``` - -### Verify Challenge - -```kotlin -mfaClient.verifyChallenge( - challengeId = "Fe26...session_id", - otp = "123456" // User's one-time password -) - .validateClaims() - .start(object : Callback { - override fun onSuccess(result: Credentials) { - // MFA verified - user now authenticated - manager.saveCredentials(result) - } - - override fun onFailure(error: AuthenticationException) { - // Invalid OTP or expired challenge - Log.e("MFA", error.message.orEmpty()) - } - }) -``` - ## Organizations Use Organizations for enterprise SSO and multi-tenancy: @@ -932,7 +851,7 @@ WebAuthProvider.login(account) .start(this, object : Callback { override fun onSuccess(result: Credentials) { // User authenticated to organization - val orgId = result.claims["org_id"] + val orgId = result.user.getExtraInfo()["org_id"] as? String } override fun onFailure(error: AuthenticationException) { @@ -963,7 +882,7 @@ authentication.login(...) override fun onFailure(error: AuthenticationException) { when { error.isMultifactorRequired -> { - // MFA enrollment or challenge required + // MFA required — see the feature:mfa reference } error.isBrowserAppNotAvailable -> { // No browser available diff --git a/main/.mintlify/skills/auth0/references/framework-auth0-auth-js/index.md b/main/.mintlify/skills/auth0/references/framework-auth0-auth-js/index.md new file mode 100644 index 0000000000..a18e8403a9 --- /dev/null +++ b/main/.mintlify/skills/auth0/references/framework-auth0-auth-js/index.md @@ -0,0 +1,163 @@ + +# Auth0 auth0-auth-js Integration + +Add OAuth 2.0 / OpenID Connect to a server-side Node.js or edge runtime with the +`@auth0/auth0-auth-js` package: a low-level, stateless, headless authentication +client built around the `AuthClient` class. It builds authorization URLs, +exchanges codes for tokens, refreshes tokens, runs the client-credentials and +password grants, reads a user profile from `/userinfo`, and builds logout URLs. + +> **Stateless by design.** auth0-auth-js holds no session, sets no cookies, and +> stores no state. You own where the tokens and the PKCE `code_verifier` live. +> For managed server sessions with cookies use `@auth0/auth0-server-js`; for a +> framework you already run (Next.js, Express, Nuxt, and so on) use that +> framework's Auth0 SDK instead - it wraps this package for you. + +> **Agent instruction:** Before providing SDK setup instructions, fetch the +> latest version by running: +> ``` +> npm view @auth0/auth0-auth-js version +> ``` +> Use the returned version instead of any version shown below. + +## Critical rules + +- **`AuthClient` is server-side only.** It needs the Client Secret, so never + import it into browser or mobile client code and never ship the secret to a + client bundle. +- **Never read the contents of `.env*` during setup** - it may contain secrets + that should not be exposed in the LLM context. Before writing to any env file + you MUST ask the user for explicit confirmation and wait for it. +- **`domain` must be a bare hostname** - no `https://`, no path, no trailing + slash. +- **The PKCE `codeVerifier` is single-use and per-request.** Generate it with + `buildAuthorizationUrl()`, carry it through the redirect, and pass the same + value to `getTokenByCode()`. Never reuse one across requests or store it in a + long-lived, shared location. + +## Prerequisites + +- Node.js `^20.19.0 || ^22.12.0 || ^24.0.0` (confirm with + `npm view @auth0/auth0-auth-js engines` when building). +- An Auth0 application whose grant type and callback URL match the flow: a + Regular Web Application for the authorization-code flow, a Machine-to-Machine + application for client credentials. Create and configure it with the loaded + tooling reference. +- `domain`, `clientId`, `clientSecret` (and a redirect URI for the code flow) in + environment variables. + +## When NOT to use + +auth0-auth-js is a low-level token/OAuth building block. Route elsewhere for: + +- **Managed server sessions** - login cookies, session persistence, "keep the + user logged in" - use `@auth0/auth0-server-js`. +- **A framework you already run** - if an Auth0 SDK exists for the app's stack + (Next.js, Express, Nuxt, Fastify, and so on), use it. Those wrap this package + and handle the redirect, callback, and storage for you. auth0-auth-js is for + building a new integration where none exists. +- **Management API operations** - reading or writing users, applications, + connections - use `node-auth0` (the `auth0` npm package). +- **Multi-factor authentication how-to** - ask for MFA (feature:mfa). + +## Quick start workflow + +### 1. Install the SDK + +```bash +npm install @auth0/auth0-auth-js +``` + +### 2. Configure credentials + +Put the tenant Domain, Client ID, Client Secret, and (for the code flow) the +redirect URI in `.env`. For all Auth0 tenant configuration (create the app, set +the callback URL, choose grant types), use the loaded tooling reference - do not +inline setup here. + +```env +AUTH0_DOMAIN= +AUTH0_CLIENT_ID= +AUTH0_CLIENT_SECRET= +AUTH0_REDIRECT_URI= +``` + +### 3. Instantiate AuthClient + +```ts +import { AuthClient } from "@auth0/auth0-auth-js"; + +const authClient = new AuthClient({ + domain: process.env.AUTH0_DOMAIN!, // bare hostname, e.g. tenant.us.auth0.com + clientId: process.env.AUTH0_CLIENT_ID!, + clientSecret: process.env.AUTH0_CLIENT_SECRET!, + authorizationParams: { redirect_uri: process.env.AUTH0_REDIRECT_URI! }, +}); +``` + +### 4. Authorization code + PKCE + +`buildAuthorizationUrl()` returns the URL to redirect to along with the +`codeVerifier` you must persist for the callback. After Auth0 redirects back, +exchange the code: + +```ts +const { authorizationUrl, codeVerifier } = await authClient.buildAuthorizationUrl(); +// redirect the user to authorizationUrl; persist codeVerifier for the callback + +// in the callback handler, with the full callback URL: +const tokens = await authClient.getTokenByCode(callbackUrl, { codeVerifier }); +``` + +### 5. Other grants and token operations + +```ts +// Machine-to-machine +const m2m = await authClient.getTokenByClientCredentials({ audience }); + +// Refresh +const refreshed = await authClient.getTokenByRefreshToken({ refreshToken }); + +// User profile from an access token +const user = await authClient.getUserInfo({ accessToken }); +``` + +### 6. Logout + +`buildLogoutUrl()` returns the URL to send the user to - the SDK does not issue +the redirect itself. + +```ts +const logoutUrl = authClient.buildLogoutUrl({ returnTo }); +``` + +## Sub-clients + +`AuthClient` exposes feature sub-clients that share its constructor config: +`authClient.mfa`, `authClient.passkey`, `authClient.passwordless`, and +`authClient.database`. Their usage is covered in the package's examples (see +References). + +## Common mistakes + +| Mistake | Fix | +|---|---| +| `domain: "https://tenant.auth0.com/"` | Bare hostname only - `tenant.auth0.com`. | +| Reusing a `codeVerifier` across requests | Generate a new one per `buildAuthorizationUrl()` call. | +| Calling `getTokenByCode()` without the verifier | Pass `{ codeVerifier }` as the second argument. | +| Expecting cookies or a session | auth0-auth-js is stateless - use `@auth0/auth0-server-js` for sessions. | +| Importing `AuthClient` in browser/mobile code | Server-side only - it needs the Client Secret. | +| Using it for Management API calls | Use `node-auth0` (the `auth0` npm package). | + +## Related capabilities + +- Managed server sessions with cookies - use `@auth0/auth0-server-js`. +- Full login integration for an existing framework - use that framework's Auth0 + SDK (Next.js, Express, Nuxt, and so on). +- Management API (users, apps, roles) - use `node-auth0` (the `auth0` npm + package). +- Multi-factor authentication - ask for MFA (feature:mfa). + +## References +- [`@auth0/auth0-auth-js` usage examples](https://raw.githubusercontent.com/auth0/auth0-auth-js/main/packages/auth0-auth-js/EXAMPLES.md) +- [Source and README](https://github.com/auth0/auth0-auth-js/tree/main/packages/auth0-auth-js) diff --git a/main/.mintlify/skills/auth0/references/framework-auth0-server-js/index.md b/main/.mintlify/skills/auth0/references/framework-auth0-server-js/index.md new file mode 100644 index 0000000000..c783a7a21b --- /dev/null +++ b/main/.mintlify/skills/auth0/references/framework-auth0-server-js/index.md @@ -0,0 +1,176 @@ + +# Auth0 auth0-server-js Integration + +Add managed server-side authentication sessions to a Node.js backend with the +`@auth0/auth0-server-js` package: the `ServerClient` class wraps +`@auth0/auth0-auth-js` with cookie and state handling, so it runs the login +redirect, completes the callback, persists the session, hands back the user and +access token, and builds the logout URL. + +> **This is a building block.** The README calls auth0-server-js "a building +> block for building framework-specific authentication SDKs." If an Auth0 SDK +> exists for your framework - `@auth0/nextjs-auth0`, `express-openid-connect`, +> `@auth0/auth0-nuxt`, `@auth0/auth0-fastify`, and others - use it instead. +> Those are built on top of this package and give you an idiomatic adapter. +> Reach for auth0-server-js directly only when no higher-level SDK covers your +> stack and you are prepared to write the request/response adapter yourself. + +> **Agent instruction:** Before providing SDK setup instructions, fetch the +> latest version by running: +> ``` +> npm view @auth0/auth0-server-js version +> ``` +> Use the returned version instead of any version shown below. + +## Critical rules + +- **The Client Secret and the cookie secret are server secrets.** Load + `clientSecret` and the store `secret` from environment variables; never + hardcode them or expose them to a browser. +- **Never read the contents of `.env*` during setup** - it may contain secrets + that should not be exposed in the LLM context. Before writing to any env file + you MUST ask the user for explicit confirmation and wait for it. +- **`domain` must be a bare hostname** - no `https://`, no path, no trailing + slash. +- **`authorizationParams.redirect_uri` is required** for the interactive login + flow and must match an Allowed Callback URL in the Auth0 app. +- **Every session method takes a trailing `storeOptions`.** + `startInteractiveLogin`, `completeInteractiveLogin`, `getUser`, `getSession`, + `getAccessToken`, and `logout` all need it - a small object shaped by your + framework's request/response so the store can read and write cookies. + +## Prerequisites + +- Node.js `^20.19.0 || ^22.12.0 || ^24.0.0` (confirm with + `npm view @auth0/auth0-server-js engines` when building). +- An Auth0 Regular Web Application with the redirect URI in Allowed Callback URLs + and the logout URL in Allowed Logout URLs. Configure it with the loaded + tooling reference. +- A strong random string for the store `secret` (at least 32 random bytes); + generate it once and store it in an environment variable. +- A `stateStore` and a `transactionStore`. The SDK ships `StatelessStateStore`, + `StatefulStateStore`, and `CookieTransactionStore` as built-ins; pick a pair or + implement the abstract interfaces. + +## When NOT to use + +- **A framework SDK exists for your stack** - `@auth0/nextjs-auth0`, + `express-openid-connect`, `@auth0/auth0-nuxt`, `@auth0/auth0-fastify`, and + others - use it. They wrap this package and remove the adapter work. +- **Stateless token operations only** - no session needed - use + `@auth0/auth0-auth-js` directly. +- **Management API operations** - reading or writing users, applications, + connections - use `node-auth0` (the `auth0` npm package). +- **Multi-factor authentication how-to** - ask for MFA (feature:mfa). + +## Quick start workflow + +### 1. Install the SDK + +```bash +npm install @auth0/auth0-server-js +``` + +### 2. Configure credentials + +Put the tenant Domain, Client ID, Client Secret, redirect URI, and a generated +cookie secret in environment variables. Use the loaded tooling reference for all +Auth0 tenant configuration. + +```env +AUTH0_DOMAIN= +AUTH0_CLIENT_ID= +AUTH0_CLIENT_SECRET= +AUTH0_REDIRECT_URI= +AUTH0_COOKIE_SECRET=<32+ random bytes> +``` + +### 3. Instantiate ServerClient + +Both built-in stores take a second argument: a `CookieHandler` you implement +for your framework (`setCookie`, `getCookie`, `getCookies`, `deleteCookie`) so +the store can read and write cookies on the current request/response. + +```ts +import { + ServerClient, + CookieTransactionStore, + StatelessStateStore, +} from "@auth0/auth0-server-js"; + +const cookieHandler = new MyCookieHandler(); // a CookieHandler you implement; see EXAMPLES.md for a Fastify adapter + +const serverClient = new ServerClient({ + domain: process.env.AUTH0_DOMAIN!, // bare hostname + clientId: process.env.AUTH0_CLIENT_ID!, + clientSecret: process.env.AUTH0_CLIENT_SECRET!, + authorizationParams: { redirect_uri: process.env.AUTH0_REDIRECT_URI! }, + transactionStore: new CookieTransactionStore({ secret: process.env.AUTH0_COOKIE_SECRET! }, cookieHandler), + stateStore: new StatelessStateStore({ secret: process.env.AUTH0_COOKIE_SECRET! }, cookieHandler), +}); +``` + +### 4. Login, callback, session, logout + +Each call takes a trailing `storeOptions` built from the current request/response +so the store can set and read cookies. + +```ts +// Login route: redirect the user to the returned URL +const url = await serverClient.startInteractiveLogin({}, storeOptions); + +// Callback route: completes the login, then redirect into the app +await serverClient.completeInteractiveLogin(callbackUrl, storeOptions); + +// Read the session user (undefined when not logged in) +const user = await serverClient.getUser(storeOptions); + +// Get an access token for calling an API +const { accessToken } = await serverClient.getAccessToken(storeOptions); + +// Logout: redirect the user to the returned URL +const logoutUrl = await serverClient.logout({ returnTo }, storeOptions); +``` + +## Storage exports + +- `CookieTransactionStore` - holds the short-lived login transaction (state, + PKCE verifier) in a cookie. +- `StatelessStateStore` - keeps the whole session in an encrypted cookie; no + server store needed. +- `StatefulStateStore` - keeps a session ID in the cookie and the session in an + external store you provide. +- `AbstractStateStore` / `AbstractTransactionStore` - interfaces to implement a + custom store. + +## Multi-domain / multi-tenant + +For per-request domain resolution, pass a `DomainResolver` function as `domain` +instead of a static hostname string. Note that the `serverClient.mfa` and +`serverClient.authClient` sub-clients are only available in static-domain mode. + +## Common mistakes + +| Mistake | Fix | +|---|---| +| Omitting `authorizationParams.redirect_uri` | Required for interactive login; must match Allowed Callback URLs. | +| Hardcoding the store `secret` | Load it from an env variable; treat it as a server secret. | +| `domain: "https://tenant.auth0.com/"` | Bare hostname only - `tenant.auth0.com`. | +| Forgetting the trailing `storeOptions` | Every session method needs it; its shape depends on your framework. | +| Hand-wiring `ServerClient` when a framework SDK exists | Use `@auth0/nextjs-auth0`, `express-openid-connect`, and so on. | +| Accessing `serverClient.mfa` with a `DomainResolver` set as `domain` | Those sub-clients are unavailable in multi-domain mode. | + +## Related capabilities + +- Stateless token/OAuth operations without a session - use + `@auth0/auth0-auth-js`. +- Full login integration for Next.js, Express, Nuxt, Fastify, and so on - use + that framework's Auth0 SDK. +- Management API (users, apps, roles) - use `node-auth0` (the `auth0` npm + package). +- Multi-factor authentication - ask for MFA (feature:mfa). + +## References +- [`@auth0/auth0-server-js` usage examples](https://raw.githubusercontent.com/auth0/auth0-auth-js/main/packages/auth0-server-js/EXAMPLES.md) +- [`@auth0/auth0-server-js` MFA examples](https://raw.githubusercontent.com/auth0/auth0-auth-js/main/packages/auth0-server-js/MFA.md) +- [Source and README](https://github.com/auth0/auth0-auth-js/tree/main/packages/auth0-server-js) diff --git a/main/.mintlify/skills/auth0/references/framework-flutter-native/index.md b/main/.mintlify/skills/auth0/references/framework-flutter-native/index.md index 81f9f98360..3b81d7d385 100644 --- a/main/.mintlify/skills/auth0/references/framework-flutter-native/index.md +++ b/main/.mintlify/skills/auth0/references/framework-flutter-native/index.md @@ -25,6 +25,7 @@ ## When NOT to Use - **Flutter web**: Use the Auth0 Flutter web integration — web uses a different platform interface (`Auth0Web`) wrapping Auth0 SPA JS +- **Flutter Windows desktop**: Use the Auth0 Flutter Windows integration — Windows uses `windowsWebAuthentication()`, a custom URL scheme + Registry protocol handler, and has no `CredentialsManager` - **Native iOS (Swift, no Flutter)**: Use the Auth0 Swift integration - **Native Android (Kotlin/Java, no Flutter)**: Use the Auth0 Android integration - **React Native**: Use the Auth0 React Native integration @@ -409,6 +410,7 @@ For complete patterns with Riverpod, Bloc, biometrics, and advanced scenarios, s - Initial Auth0 setup and account creation — if Auth0 isn't set up yet, set it up first with the Auth0 CLI (`auth0 login`, then `auth0 apps create`) - Same SDK on the web platform — ask for the Auth0 Flutter web integration +- Same SDK on Windows desktop — ask for the Auth0 Flutter Windows integration - Native iOS (Swift) — ask for the Auth0 Swift integration - Native Android (Kotlin/Java) — ask for the Auth0 Android integration diff --git a/main/.mintlify/skills/auth0/references/framework-flutter-web/index.md b/main/.mintlify/skills/auth0/references/framework-flutter-web/index.md index 9bce72a77f..5da95c04ad 100644 --- a/main/.mintlify/skills/auth0/references/framework-flutter-web/index.md +++ b/main/.mintlify/skills/auth0/references/framework-flutter-web/index.md @@ -14,6 +14,7 @@ ## When NOT to Use - **Flutter mobile (iOS/Android)**: use the Auth0 integration workflow for Flutter native — mobile uses a different platform interface with Web Auth via system browser +- **Flutter Windows desktop**: use the Auth0 integration workflow for Flutter Windows — Windows uses `windowsWebAuthentication()` and a custom URL scheme protocol handler, not the browser-embedded `Auth0Web` API - **Native iOS (Swift)**: use the Auth0 integration workflow for Swift - **Native Android (Kotlin/Java)**: use the Auth0 integration workflow for Android - **React SPA**: use the Auth0 integration workflow for React diff --git a/main/.mintlify/skills/auth0/references/framework-flutter-windows/index.md b/main/.mintlify/skills/auth0/references/framework-flutter-windows/index.md new file mode 100644 index 0000000000..2deca27749 --- /dev/null +++ b/main/.mintlify/skills/auth0/references/framework-flutter-windows/index.md @@ -0,0 +1,551 @@ +# Auth0 Flutter Windows Desktop Integration + +`auth0_flutter` is the official Auth0 SDK for Flutter applications. **Windows +desktop** support reached General Availability in SDK v2.1.0. It uses a +different API surface than mobile/web: `windowsWebAuthentication()` instead +of `webAuthentication()`, a custom URL scheme registered as a Windows +protocol handler instead of Info.plist/AndroidManifest registration, a native +C++ runner integration to receive the callback, and **no built-in credential +storage** — the app must persist the returned `Credentials` itself. + +> **Agent instruction:** Before providing SDK setup instructions, fetch the +> latest published version from pub.dev: +> +> ```bash +> curl -s https://pub.dev/api/packages/auth0_flutter | python3 -c "import sys,json;print(json.load(sys.stdin)['latest']['version'])" +> ``` +> +> This returns a bare semver (e.g. `2.2.0`) — no `v` prefix or GitHub tag +> formatting. Use that exact value as `` in the Step 1 dependency +> line below instead of any hardcoded version. Windows support requires +> v2.1.0 or later; if the fetched version is older, use `2.1.0` and flag it +> to the user. + +## When NOT to Use + +- **Flutter mobile (iOS/Android)**: use the Auth0 integration workflow for Flutter native — mobile uses `webAuthentication()` and the built-in `CredentialsManager`, neither of which apply here +- **Flutter web**: use the Auth0 integration workflow for Flutter web — browser platform, different API (`Auth0Web`) +- **Flutter macOS**: macOS goes through the same mobile-style `webAuthentication()` flow documented in the Flutter native reference, not the Windows-specific API in this file +- **Flutter Linux**: `auth0_flutter` has no official Linux support (only an unofficial third-party plugin exists) — flag this to the user rather than guessing at an implementation +- **.NET MAUI, WinForms, or WPF desktop apps**: those use the `Auth0.OidcClient.*` NuGet packages, a completely different SDK family — use the Auth0 integration workflow for MAUI/WinForms/WPF +- **Native iOS (Swift, no Flutter)** or **native Android (Kotlin/Java, no Flutter)**: use the Auth0 Swift or Android integration + +## Prerequisites + +| Flutter | Windows | Native tooling | +|---|---|---| +| SDK 3.24.0+ | Windows 10+ | C++17, Visual Studio 2022 | +| Dart 3.5.0+ | | [vcpkg](https://vcpkg.io/) (for native dependencies) | + +- Auth0 account — [Sign up free](https://auth0.com/signup) +- Auth0 CLI — [install instructions](https://github.com/auth0/auth0-cli) (used to create and configure the Auth0 application) + +## Quick Start Workflow + +> **Agent instruction:** Follow these steps in order. If you encounter an error at any step, attempt to fix it up to 5 times before calling `AskUserQuestion` to ask the user for guidance. Always search existing code first — if there are existing login/logout handlers, hook into them rather than creating new ones. + +### Step 1 — Install SDK + +> **Agent instruction:** Check the project directory for `pubspec.yaml` and a `windows/` platform directory. If neither is present, this is not a Flutter Windows project — ask the user. + +```bash +flutter pub add auth0_flutter:^ +``` + +Replace `` with the pub.dev version fetched above (e.g. +`auth0_flutter:^2.2.0`). + +### Step 2 — Choose a callback pattern and configure Auth0 + +Windows has no OS-level equivalent of an iOS Universal Link or Android App +Link — the app registers a **custom URL scheme** (e.g. `myapp://callback`) +as a Windows protocol handler, and Auth0 redirects to it after login. There +are two supported patterns: + +- **Option A — Direct custom-scheme redirect (recommended for most apps).** + Register the custom scheme directly as the callback/logout URL. Simpler, + but the browser may leave a blank tab open after redirecting (a browser + behavior, not an error). +- **Option B — Intermediary HTTPS server.** Register an HTTPS URL you + control as the callback/logout URL; that server 302s onward to the custom + scheme, letting it show a "Returning you to the app…" page and close + cleanly. Requires you to run and validate a small server endpoint. + +> **Agent instruction:** +> - **If Auth0 credentials (domain AND client ID) are already in the user's prompt:** use those values directly. +> - **If no credentials are provided:** ask the user whether to create the Auth0 application automatically (Auth0 CLI) or manually (Dashboard), and which callback pattern (A or B) they want. Default to Option A unless the user specifically wants to avoid a lingering browser tab. +> - Choose a scheme name unique to the app (e.g. `myapp`, `com.company.myapp`) and use it consistently across the Auth0 Dashboard, the Windows Registry, and the Dart `appCustomURL` value. + +Create a **Native** application, then register the callback and logout URLs: + +```bash +# Option A — direct custom scheme +auth0 apps update CLIENT_ID \ + --callbacks "myapp://callback" \ + --logout-urls "myapp://callback" \ + --no-input + +# Option B — intermediary HTTPS server +auth0 apps update CLIENT_ID \ + --callbacks "https://your-app.example.com/callback" \ + --logout-urls "https://your-app.example.com/logout" \ + --no-input +``` + +### Step 3 — Install native dependencies via vcpkg + +The Windows plugin is implemented in native C++ and depends on `cpp-httplib`, +`nlohmann-json`, and `openssl`, managed via vcpkg. The plugin's own +`vcpkg.json` manifest lives inside the plugin package, not the app's +`windows/` root, so vcpkg's manifest mode won't auto-install these — install +them explicitly into the vcpkg instance: + +```powershell +git clone https://github.com/microsoft/vcpkg +.\vcpkg\bootstrap-vcpkg.bat +$env:VCPKG_ROOT = "$PWD\vcpkg" +``` + +```powershell +.\vcpkg\vcpkg.exe install --recurse "cpp-httplib[core,openssl]:x64-windows" nlohmann-json:x64-windows openssl:x64-windows +``` + +### Step 4 — Configure `windows/CMakeLists.txt` + +The app's top-level `windows/CMakeLists.txt` must enable vcpkg toolchain +integration so the plugin's native dependencies resolve. Add this **before** +the first `project()` call: + +```cmake +# windows/CMakeLists.txt + +cmake_minimum_required(VERSION 3.14) + +# --- vcpkg integration (required for auth0_flutter) --- +if(DEFINED ENV{VCPKG_ROOT} AND EXISTS "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake") + set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + CACHE STRING "Vcpkg toolchain file") +endif() + +project(your_app LANGUAGES CXX) + +# ... rest of your CMakeLists.txt ... +``` + +> **Agent instruction:** the `CMAKE_TOOLCHAIN_FILE` assignment must come before `project()`. If it's added after, CMake will already have configured the compiler and the build fails with errors like `Could not find a package configuration file provided by "httplib"`. Insert it into the existing file rather than overwriting other content. + +### Step 5 — Register the custom URL scheme + +Windows routes `myapp://callback` back to the app via a Registry protocol +handler entry. Pick the registration method based on where the app is in its +lifecycle: + +**Development — manual `.reg` file:** + +```reg +Windows Registry Editor Version 5.00 + +[HKEY_CURRENT_USER\Software\Classes\myapp] +@="URL:myapp Protocol" +"URL Protocol"="" + +[HKEY_CURRENT_USER\Software\Classes\myapp\shell] + +[HKEY_CURRENT_USER\Software\Classes\myapp\shell\open] + +[HKEY_CURRENT_USER\Software\Classes\myapp\shell\open\command] +@="\"C:\\Path\\To\\Your\\App\\your_app.exe\" \"%1\"" +``` + +Replace `myapp` with the chosen scheme and the path with the built +executable (e.g. `build\windows\x64\runner\Debug\your_app.exe` during +development, the installed path in production). + +**Production — installer or first-run registration:** if the app ships via +MSIX, declare the protocol in `Package.appxmanifest`: + +```xml + + + + My App Callback + + + +``` + +For other installers (Inno Setup, WiX) or a first-run self-registration +approach, write the same Registry keys shown above from the installer script +or from `main.cpp` on first launch. + +### Step 6 — Update the Windows runner (`windows/runner/main.cpp`) + +The plugin does not automatically receive OS protocol-scheme activations — +the app's runner must capture the callback URI and hand it to the plugin via +the `PLUGIN_STARTUP_URL` environment variable. Without this, `login()` always +times out with `USER_CANCELLED` because the callback never reaches the +waiting plugin. + +> **Agent instruction:** this involves security-sensitive Win32 code (a +> named pipe with a restricted DACL, single-instance mutex handling, prefix +> validation of the forwarded URI). Do not hand-write this logic from +> memory — fetch the canonical reference implementation and adapt only the +> callback-prefix constant to the chosen scheme: +> +> ```bash +> curl -sL https://raw.githubusercontent.com/auth0/auth0-flutter/main/auth0_flutter/example/windows/runner/main.cpp +> ``` +> +> The three required pieces, copied from that file into the app's +> `wWinMain`: +> 1. **Single-instance mutex** — a second launch triggered by the OS +> protocol handler forwards its URI to the already-running instance +> instead of opening a new window. +> 2. **Named-pipe server** (`\\.\pipe\_auth0_pipe`) — the running +> instance listens for the forwarded URI, validates it starts with the +> app's callback prefix (e.g. `myapp://callback`), and writes it to +> `PLUGIN_STARTUP_URL`. The pipe's security descriptor must restrict +> access to the current user only — do not use a `NULL` security +> descriptor, which would let any process on the machine inject an +> arbitrary startup URL. +> 3. **Startup URI capture** — on first launch, `argv[1]` (the +> protocol-scheme URI, if present) is written to `PLUGIN_STARTUP_URL` +> before Flutter starts, after the same prefix check. +> +> The example file's mutex and pipe names are fixed strings shared by every +> app that copies them unmodified — two different Auth0-Flutter-Windows apps +> installed on the same machine would collide on the same mutex/pipe, and one +> app's callback could be forwarded into the other's window. Rename both to +> include the app's own custom scheme, e.g. `Local\_auth0_singleton` +> for the mutex and `\\.\pipe\_auth0_pipe` for the pipe, and update +> `kCallbackPrefix` to match the chosen scheme (e.g. `L"myapp://callback"`). + +### Step 7 — Implement Authentication + +> **Agent instruction:** search the project for the main app entry point (`lib/main.dart`). Create an `AuthService` class that stores `Credentials` manually — there is no `CredentialsManager` on Windows, so the app owns persistence via secure storage (e.g. `flutter_secure_storage`), since the credentials can include a refresh token. + +```bash +flutter pub add flutter_secure_storage +``` + +```dart +// lib/auth_service.dart +import 'dart:convert'; +import 'package:auth0_flutter/auth0_flutter.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class AuthService { + late final Auth0 _auth0; + Credentials? _credentials; + final String _appCustomURL; + final String? _redirectUrl; + final String? _returnTo; + static const _storage = FlutterSecureStorage(); + static const _credentialsKey = 'auth0_credentials'; + + AuthService({ + required String domain, + required String clientId, + required String appCustomURL, + String? redirectUrl, + String? returnTo, + }) : _appCustomURL = appCustomURL, + _redirectUrl = redirectUrl, + _returnTo = returnTo { + if ((redirectUrl == null) != (returnTo == null)) { + throw ArgumentError( + 'Set both redirectUrl and returnTo when using the intermediary HTTPS callback.', + ); + } + _auth0 = Auth0(domain, clientId); + } + + bool get isAuthenticated => _credentials != null; + UserProfile? get user => _credentials?.user; + + /// Restore credentials saved from a previous session, if still valid. + /// There's no refresh-token renewal here — an expired or unreadable + /// record is dropped, leaving the user to log in again. + Future restoreSession() async { + final stored = await _storage.read(key: _credentialsKey); + if (stored == null) return; + try { + final credentials = Credentials.fromMap(jsonDecode(stored)); + if (credentials.expiresAt.isAfter(DateTime.now())) { + _credentials = credentials; + } else { + await _storage.delete(key: _credentialsKey); + } + } catch (_) { + // Malformed or outdated schema from a previous app version. + await _storage.delete(key: _credentialsKey); + } + } + + /// Launch Web Auth via the system browser and persist the result. + /// No credentials are stored automatically on Windows. + Future login() async { + final webAuth = _auth0.windowsWebAuthentication(); + if (_redirectUrl == null) { + _credentials = await webAuth.login( + appCustomURL: _appCustomURL, + scopes: {'openid', 'profile', 'email', 'offline_access'}, + ); + } else { + _credentials = await webAuth.login( + appCustomURL: _appCustomURL, + redirectUrl: _redirectUrl!, + scopes: {'openid', 'profile', 'email', 'offline_access'}, + ); + } + await _storage.write( + key: _credentialsKey, + value: jsonEncode(_credentials!.toMap()), + ); + } + + /// Clear the browser session and drop persisted + in-memory credentials. + /// Local state is cleared even if the remote logout call fails, so the + /// app never gets stuck showing an authenticated screen with a stale + /// persisted record; the error is rethrown for the caller to surface. + Future logout() async { + try { + final webAuth = _auth0.windowsWebAuthentication(); + if (_returnTo == null) { + await webAuth.logout(appCustomURL: _appCustomURL); + } else { + await webAuth.logout( + appCustomURL: _appCustomURL, + returnTo: _returnTo!, + ); + } + } finally { + _credentials = null; + await _storage.delete(key: _credentialsKey); + } + } +} +``` + +> **Note:** `login()` requests the `offline_access` scope, so the persisted +> `Credentials` can contain a refresh token — store it with +> `flutter_secure_storage` (DPAPI-backed on Windows), never with +> `shared_preferences`, which is plaintext on disk. + +```dart +// lib/main.dart +import 'package:flutter/material.dart'; +import 'package:auth0_flutter/auth0_flutter.dart'; // for WebAuthenticationException +import 'auth_service.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final _authService = AuthService( + domain: 'YOUR_AUTH0_DOMAIN', + clientId: 'YOUR_AUTH0_CLIENT_ID', + appCustomURL: 'com.example.app://callback', + // For Option B, also set: + // redirectUrl: 'https://your-app.example.com/callback', + // returnTo: 'https://your-app.example.com/logout', + ); + bool _restoring = true; + + @override + void initState() { + super.initState(); + _authService.restoreSession().then((_) { + setState(() => _restoring = false); + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: _restoring + ? const Scaffold(body: Center(child: CircularProgressIndicator())) + : _authService.isAuthenticated + ? HomeScreen(authService: _authService, onChanged: _refresh) + : LoginScreen(authService: _authService, onChanged: _refresh), + ); + } + + void _refresh() => setState(() {}); +} + +class LoginScreen extends StatelessWidget { + final AuthService authService; + final VoidCallback onChanged; + const LoginScreen({super.key, required this.authService, required this.onChanged}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: ElevatedButton( + onPressed: () async { + final messenger = ScaffoldMessenger.of(context); + try { + await authService.login(); + onChanged(); + } on WebAuthenticationException catch (e) { + messenger.showSnackBar( + SnackBar(content: Text('Login failed: ${e.message}')), + ); + } + }, + child: const Text('Log In'), + ), + ), + ); + } +} + +class HomeScreen extends StatelessWidget { + final AuthService authService; + final VoidCallback onChanged; + const HomeScreen({super.key, required this.authService, required this.onChanged}); + + @override + Widget build(BuildContext context) { + final user = authService.user; + return Scaffold( + appBar: AppBar( + title: const Text('Home'), + actions: [ + IconButton( + onPressed: () async { + final messenger = ScaffoldMessenger.of(context); + try { + await authService.logout(); + } on WebAuthenticationException catch (e) { + messenger.showSnackBar( + SnackBar(content: Text('Logout error: ${e.message}')), + ); + } finally { + // Local credentials are always cleared by logout(), so + // refresh to LoginScreen even if the remote call failed. + onChanged(); + } + }, + icon: const Icon(Icons.logout), + ), + ], + ), + body: Center(child: Text('Welcome, ${user?.name ?? 'User'}!')), + ); + } +} +``` + +### Step 8 — Verify Build + +```bash +flutter build windows --debug +flutter run -d windows +``` + +If the build fails, review error messages and fix up to 5 times before +asking the user. Common failures at this step are usually a missing vcpkg +toolchain line (Step 4) or missing vcpkg packages (Step 3). + +## Callback URL Configuration + +| Pattern | `appCustomURL` | `redirectUrl` (login) / `returnTo` (logout) | Allowed Callback/Logout URL registered in Dashboard | +|---|---|---|---| +| Option A — direct | required, e.g. `myapp://callback` | omit | `myapp://callback` | +| Option B — intermediary server | required | required, e.g. `https://your-app.example.com/callback` | the HTTPS URL | + +`appCustomURL` is always required — it's the scheme the app listens on and, +in Option A, doubles as the `redirect_uri`/`returnTo` value automatically. + +Minimal intermediary-server implementation (Node.js/Express) for Option B — +validate `state` against the value stored when the auth request was initiated +before forwarding, since this endpoint is an open-redirect target otherwise: + +```javascript +app.get('/callback', (req, res) => { + const { code, state, error, error_description } = req.query; + const expectedState = pendingStates.get(state); // stored when the login request started + if (!state || !expectedState) { + return res.status(400).send('Invalid or missing state'); + } + pendingStates.delete(state); + if (error) { + res.redirect(`myapp://callback?error=${encodeURIComponent(error)}&error_description=${encodeURIComponent(error_description)}`); + } else { + res.redirect(`myapp://callback?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`); + } +}); +``` + +## Security Considerations + +- Custom URL schemes are vulnerable to app-impersonation: another app could + register the same scheme on the same device. PKCE is enabled automatically + by the SDK and is the primary mitigation — no extra configuration needed. +- In Option B, validate `state` on the intermediary server before + redirecting onward; the SDK also validates `state` client-side as part of + PKCE, but server-side validation is defense-in-depth against open-redirect + abuse of that endpoint. +- The named pipe used for single-instance URI forwarding (Step 6) must + restrict its DACL to the current user — an unrestricted pipe lets any + local process inject an arbitrary startup URL. + +## Done When + +- [ ] `auth0_flutter` added to `pubspec.yaml` (version pinned, Windows GA v2.1.0+) +- [ ] vcpkg installed with `cpp-httplib`, `nlohmann-json`, `openssl` for `x64-windows` +- [ ] `windows/CMakeLists.txt` sets `CMAKE_TOOLCHAIN_FILE` before `project()` +- [ ] Custom URL scheme registered in the Windows Registry (or installer/MSIX manifest) +- [ ] `windows/runner/main.cpp` updated with the mutex + named-pipe + startup-URI logic, adapted from the SDK's example runner +- [ ] Callback/logout URLs registered in the Auth0 Dashboard matching the chosen pattern (A or B) +- [ ] `AuthService` calls `windowsWebAuthentication().login()` / `.logout()` with `appCustomURL` +- [ ] Returned `Credentials` are persisted manually (no `CredentialsManager` on Windows) +- [ ] `flutter build windows` succeeds +- [ ] Login → redirect → callback → app foregrounded → credentials received tested end-to-end + +## Common Mistakes + +| Mistake | Fix | +|---------|-----| +| `login()` always times out with `USER_CANCELLED` | The runner isn't forwarding the callback URI — verify `windows/runner/main.cpp` has the mutex/pipe/`PLUGIN_STARTUP_URL` integration from Step 6 | +| Build fails with `Could not find a package configuration file provided by "httplib"` | `CMAKE_TOOLCHAIN_FILE` was set after `project()` in `windows/CMakeLists.txt`, or vcpkg packages weren't installed — redo Steps 3–4 | +| Assuming `credentialsManager` exists on Windows | It doesn't — Windows has no `CredentialsManager`; store and restore `Credentials` manually | +| Using `webAuthentication()` instead of `windowsWebAuthentication()` | Windows requires the Windows-specific API, which also requires `appCustomURL` | +| Scheme registered in the Registry doesn't match `appCustomURL` in Dart, or the Dashboard callback URL | All three (Registry scheme, `appCustomURL`, Dashboard Allowed Callback/Logout URLs) must use the exact same scheme string | +| Using Option B without validating `state` server-side | The intermediary endpoint becomes an open redirect — validate `state` before forwarding to the custom scheme | +| Hand-writing the pipe/mutex C++ logic instead of copying the example | The DACL and prefix-validation details are security-sensitive; adapt the SDK's example runner rather than reimplementing from memory | + +## Related capabilities + +- Same SDK on iOS/Android — ask for the Auth0 Flutter native integration +- Same SDK on the web platform — ask for the Auth0 Flutter web integration +- .NET desktop apps (not Flutter) — ask for the Auth0 WPF, WinForms, or MAUI integration +- Initial Auth0 setup and account creation — if Auth0 isn't set up yet, set it up first with the Auth0 CLI (`auth0 login`, then `auth0 apps create`) + +## Quick Reference + +| API | Purpose | +|-----|---------| +| `Auth0(domain, clientId)` | Create the SDK client | +| `auth0.windowsWebAuthentication().login(appCustomURL: ..., redirectUrl: ...)` | Launch Universal Login in the system browser (Windows) | +| `auth0.windowsWebAuthentication().logout(appCustomURL: ..., returnTo: ...)` | Clear the browser session (Windows) | +| `PLUGIN_STARTUP_URL` | Environment variable the runner uses to hand the callback URI to the plugin | + +## References + +- [auth0_flutter on pub.dev](https://pub.dev/packages/auth0_flutter) +- [auth0_flutter GitHub](https://github.com/auth0/auth0-flutter) +- [Example Windows runner (main.cpp)](https://github.com/auth0/auth0-flutter/blob/main/auth0_flutter/example/windows/runner/main.cpp) +- [Flutter Windows Quickstart](https://auth0.com/docs/quickstart/native/flutter-windows) +- [Auth0 Dashboard](https://manage.auth0.com) diff --git a/main/.mintlify/skills/auth0/references/framework-ionic-react/index.md b/main/.mintlify/skills/auth0/references/framework-ionic-react/index.md index 06c3dafba2..81e971ce85 100644 --- a/main/.mintlify/skills/auth0/references/framework-ionic-react/index.md +++ b/main/.mintlify/skills/auth0/references/framework-ionic-react/index.md @@ -814,7 +814,7 @@ const App: React.FC = () => { | `consent_required` | User hasn't consented to requested scopes | Re-trigger login with `prompt: 'consent'` | | `invalid_grant` | Refresh token expired or revoked | Clear session and re-authenticate | | `access_denied` | User denied consent or rule blocked access | Check Auth0 Actions/Rules for blocks | -| `mfa_required` | MFA is required for the user | Handle MFA enrollment flow | +| `mfa_required` | MFA is required for the user | See the feature:mfa reference | ## Testing Patterns diff --git a/main/.mintlify/skills/auth0/references/framework-ionic-vue/index.md b/main/.mintlify/skills/auth0/references/framework-ionic-vue/index.md index a5cd3ef1d8..374b00e8bd 100644 --- a/main/.mintlify/skills/auth0/references/framework-ionic-vue/index.md +++ b/main/.mintlify/skills/auth0/references/framework-ionic-vue/index.md @@ -933,7 +933,7 @@ const { error, isLoading } = useAuth0(); | `consent_required` | User hasn't consented to requested scopes | Re-trigger login with `prompt: 'consent'` | | `invalid_grant` | Refresh token expired or revoked | Clear session and re-authenticate | | `access_denied` | User denied consent or rule blocked access | Check Auth0 Actions/Rules for blocks | -| `mfa_required` | MFA is required for the user | Handle MFA enrollment flow | +| `mfa_required` | MFA is required for the user | See the feature:mfa reference | ## Testing Patterns diff --git a/main/.mintlify/skills/auth0/references/framework-node-auth0/index.md b/main/.mintlify/skills/auth0/references/framework-node-auth0/index.md index b9036c77d5..20019c4925 100644 --- a/main/.mintlify/skills/auth0/references/framework-node-auth0/index.md +++ b/main/.mintlify/skills/auth0/references/framework-node-auth0/index.md @@ -6,10 +6,11 @@ Administer an Auth0 tenant from server-side Node.js / TypeScript with the connections, roles, organizations, actions, resource servers, and everything else the Auth0 Dashboard exposes, through the Management API. -> **Authentication lives in separate packages.** node-auth0 covers the -> Management API only - it does not sign users in or issue tokens. For -> authentication (OAuth token grants, database signup, passwordless, MFA -> challenges, or reading a user's profile from an access token) use +> **Session management lives in separate packages.** node-auth0 provides +> `ManagementClient` for tenant administration plus the stateless +> `AuthenticationClient` and `UserInfoClient` auth APIs. It does NOT own +> browser sessions, cookies, token storage, or automatic token refresh — those +> are what the migration targets add. For session-bearing authentication use > `@auth0/auth0-auth-js` for stateless token/OAuth operations or > `@auth0/auth0-server-js` for stateful server sessions. Both sit alongside > node-auth0 in the same project when you need administration and @@ -22,6 +23,30 @@ else the Auth0 Dashboard exposes, through the Management API. > ``` > Use the returned version instead of any version shown below. +## Migrating node-auth0 authentication usage + +If the task is migrating an existing app OFF the legacy `auth0` package's +Authentication API surface (`AuthenticationClient`, `oauth.*`, `database.*`, +`passwordless.*`, `backchannel.*`, `tokenExchange.*`, id-token validation) to +`@auth0/auth0-auth-js` / `@auth0/auth0-server-js`, dispatch to the migration +guide below. `ManagementClient` administration stays on this package and is not +part of that migration. + +**Version gate — check the installed `auth0` major version first.** This +migration targets node-auth0 **v6**. Read the `auth0` version from the app's +`package.json` / lockfile. If it is **v4 or v5**, do NOT run the migration — +return this message and stop: + +> This migration guide targets node-auth0 v6. Your app is on node-auth0 +> v{major}. Upgrade to v6 first (its Authentication API surface is stable across +> v4–v6, so the upgrade is low-risk), then re-run this migration. + +Only when the app is on **v6** proceed to the guide: + +| Load | +|---| +| `Read: references/framework-node-auth0/migration.md` | + ## Critical rules - **Never read the contents of `.env*` during setup** - it may contain secrets @@ -47,17 +72,19 @@ else the Auth0 Dashboard exposes, through the Management API. ## When NOT to use -node-auth0 is a Management-API-only SDK. It does not sign users in, hold -sessions, or validate access tokens for your endpoints. Route elsewhere for: +node-auth0 provides ManagementClient (tenant administration) plus the stateless +AuthenticationClient and UserInfoClient auth APIs. It does not hold sessions, +manage cookies, store tokens, or auto-refresh them. Route elsewhere for: - **End-user login, signup, or sessions** - use the app's framework SDK. Use the Auth0 integration workflow for Next.js, React, Vue, Angular, Express, the mobile SDKs, and so on; those own the redirect, callback, cookies, and token storage. -- **Authentication API operations** - token grants, database signup, - passwordless, MFA challenges, or reading a user's profile from an access token - - use `@auth0/auth0-auth-js` or `@auth0/auth0-server-js` (see the callout at - the top). +- **Session-bearing authentication** - if you need cookies, server-managed + sessions, automatic token refresh, or browser login redirects, use + `@auth0/auth0-auth-js` (stateless token grants) or `@auth0/auth0-server-js` + (full session management). node-auth0's `AuthenticationClient` is stateless + and does not cover these concerns. - **Protecting an API by validating incoming JWTs** - use `express-oauth2-jwt-bearer` (the Express API integration workflow) or the framework's resource-server SDK. @@ -265,7 +292,7 @@ the full resource graph. | Passing a resource id as `{ id }` to `get`/`update`/`delete` | The id is a positional string - `management.users.get(userId)`, `management.users.update(userId, body)`. | | `domain: "https://tenant.auth0.com/"` | Bare hostname only - `tenant.auth0.com`. | | Using node-auth0 to log users into an app | Wrong SDK - use the app's framework SDK for login/sessions. | -| Using node-auth0 for authentication (token grants, signup, MFA, `/userinfo`) | Use `@auth0/auth0-auth-js` or `@auth0/auth0-server-js`. | +| Using node-auth0 for session-bearing auth (cookies, token storage, auto-refresh) | node-auth0 is stateless — use `@auth0/auth0-auth-js` or `@auth0/auth0-server-js`. | | Granting the M2M app broad Management scopes | Grant only what the task needs (e.g. just `read:users`). | | Hardcoding the client secret in source | Load from an env variable; keep `.env` out of git. | | Per-record loops for large jobs | The Management API is rate limited - keep `maxRetries` on and prefer bulk/Import-Users jobs. | diff --git a/main/.mintlify/skills/auth0/references/framework-node-auth0/migration.md b/main/.mintlify/skills/auth0/references/framework-node-auth0/migration.md new file mode 100644 index 0000000000..ec3ff9d068 --- /dev/null +++ b/main/.mintlify/skills/auth0/references/framework-node-auth0/migration.md @@ -0,0 +1,1671 @@ +# node-auth0 v6 Migration Guide — auth0-auth-js / auth0-server-js + +**Applies to:** `auth0` (node-auth0) **v6** migrating to `@auth0/auth0-auth-js` >= v1.13.0 +and/or `@auth0/auth0-server-js` >= v1.13.0. Apps on v4/v5 must upgrade to v6 before running this +migration (the Authentication API is stable across v4-v6, so that upgrade is low-risk; note v5 +raised the minimum Node.js to ^20.19.0). The framework router gates this: a non-v6 app is turned +away with an upgrade message rather than migrated. + +**Out of scope:** The `ManagementClient` +(Management API v2) is also out of scope — it stays on the `auth0` package and is not migrated +here. Application routes, view/controller logic, database code, and any non-auth use of the +`auth0` package are likewise out of scope. + +This guide covers a **surgical rewrite of the authentication layer only**. You replace +node-auth0's `AuthenticationClient` (and `UserInfoClient`) call sites with modern SDK +equivalents. Routes, controllers, business logic, data access, and framework wiring stay as they +are. The goal is to touch the smallest possible surface: the files that import and call +node-auth0's Authentication API. + +**In scope:** any code that imports from the `auth0` package and uses: + +- `AuthenticationClient` and its sub-clients: `.oauth`, `.database`, `.passwordless`, + `.backchannel`, `.tokenExchange` +- `UserInfoClient` +- Auth error types (`AuthApiError`) and token-validation types (`IDTokenValidateOptions`, + `IdTokenValidatorError`) + +> If a file uses `ManagementClient`, leave that code alone. Only rewrite the +> `AuthenticationClient` / `UserInfoClient` parts. It is normal and correct for a file to keep +> importing `auth0` for management while importing `@auth0/auth0-auth-js` for authentication +> during and after the migration. Only remove the `auth0` import from files where it was used +> solely for `AuthenticationClient` / `UserInfoClient`. + +--- + +## SDK versions + +| Role | Package | Version | +|---|---|---| +| Source (being migrated) | `auth0` (node-auth0) | v6 | +| Target — token layer | `@auth0/auth0-auth-js` | >= v1.13.0 | +| Target — session layer | `@auth0/auth0-server-js` | >= v1.13.0 | + +Both target SDKs require **Node.js 20 LTS or newer**. Verify the project's runtime before +installing. Install the latest published versions: +`npm install @auth0/auth0-auth-js@latest @auth0/auth0-server-js@latest`. + +--- + +## Which target SDK? + +node-auth0's `AuthenticationClient` is a **stateless HTTP client**. Each method is a single call +to an Auth0 Authentication API endpoint. It has no notion of a logged-in user, no session, no +cookie, no token store, no automatic refresh. Anything stateful in a node-auth0 app was written +by the customer *around* node-auth0 — typically with `express-openid-connect`, `express-session`, +custom middleware, or a bespoke token cache. + +The modern stack separates those two concerns into two packages: + +- **`@auth0/auth0-auth-js`** — the stateless token/primitive layer. Direct successor to + `AuthenticationClient`: same "one method = one API call = one result" model with modern + ergonomics (camelCase, typed errors, direct return values, per-request options). +- **`@auth0/auth0-server-js`** — a stateful **session layer built on top of** auth0-auth-js. + Owns the login redirect flow, a pluggable state/transaction store, cookie handling, automatic + token refresh, and logout. Successor to the *session code the customer hand-rolled*, not to + `AuthenticationClient` itself. + +### Decision procedure + +``` +Is the customer's node-auth0 usage purely stateless? +(only token grants / DB signup / passwordless send / userinfo, and they store + tokens + manage the logged-in user themselves — or it is a machine-to-machine + backend with no user at all) +│ +├─ YES ─────────────────────────────────► migrate to @auth0/auth0-auth-js +│ (near 1:1, lowest-risk, smallest diff) +│ +└─ NO — they have a redirect login flow, persist a user session across requests, + manage cookies, refresh tokens on a timer, and would rather the SDK + did all of that + │ + ├─ They want to keep their current session mechanism ─► @auth0/auth0-auth-js + │ (port the grant calls only; + │ leave their session code) + │ + └─ They want the SDK to own the session ──────────────► @auth0/auth0-server-js + (rewrite the session layer; + see Section 7 of this guide) +``` + +**Signals that point to auth0-auth-js:** +- Predominant use is `clientCredentialsGrant` (M2M) — no user, so no session to own. +- The app already has a session framework and only calls node-auth0 for token grants. +- The app is an API/worker/CLI, not a browser-facing web server. +- The customer wants the smallest, most mechanical, lowest-risk migration. + +**Signals that point to auth0-server-js:** +- The app performs a browser redirect login and reads `req.session.user` (or equivalent) on later requests. +- The customer wrote refresh-on-expiry logic, a token cache, or logout-with-revocation by hand. +- They use `express-openid-connect` today and want a first-party, framework-agnostic replacement. +- They are on a server framework (Express, Fastify, Hono, Next.js) and want the SDK to manage cookies. + +**Mixing both:** a single app can use both — auth0-server-js for the user-facing login/session, +and auth0-auth-js directly for a separate M2M `clientCredentialsGrant` to call another API. +`ServerClient` exposes the underlying `AuthClient` via `serverClient.authClient` for occasional +low-level needs. + +> **Caveat:** `serverClient.authClient` is a public getter that throws `InvalidConfigurationError` +> at runtime when the client is constructed with a domain-resolver function (the multi-tenant +> pattern, where the domain is resolved per request). It works with a static string domain. Use +> the ServerClient's own methods for per-request operations in multi-tenant setups. + +--- + +## Section 1: Preflight and git safety + +Before touching any code, verify the environment is safe for an in-place rewrite. + +### 1a. Git safety check and stash-based backup + +```bash +# Detect whether this is a git repository. +# Distinguishes "not a repo" (output is not "true") from other git failures. +GIT_CHECK=$(git rev-parse --is-inside-work-tree 2>&1) +if [ "$GIT_CHECK" != "true" ]; then + echo "Not inside a git repository — skipping git safety steps." +else + CHANGES=$(git status --porcelain) + if [ -n "$CHANGES" ]; then + echo "Uncommitted changes detected. Creating a stash-based backup..." + STASH_NAME="pre-migration-backup-$(date +%Y%m%d-%H%M%S)" + git stash push --message "$STASH_NAME" --include-untracked + echo "Backup stash created: $STASH_NAME" + echo "To restore the original state: git stash pop" + echo "To inspect all stashes: git stash list" + else + echo "Working tree clean — no backup needed." + fi +fi +``` + +> The stash preserves the pre-migration state. If the rewrite goes wrong, `git stash pop` brings +> back the original files. This is preferred over creating a backup branch because eval harnesses +> often run in fresh temporary directories that are not git repositories, and a new branch requires +> a valid HEAD commit that may not exist. + +### 1b. Verify the source SDK + +Confirm node-auth0 v6 is present in the project's dependencies: + +```bash +node -e " +const p = require('./package.json'); +const v = (p.dependencies || {})['auth0'] || (p.devDependencies || {})['auth0']; +console.log('auth0 version:', v || 'NOT FOUND'); +" +``` + +### 1c. Verify target SDK versions + +`@auth0/auth0-auth-js` >= v1.13.0 and `@auth0/auth0-server-js` >= v1.13.0 are required: + +```bash +node -e " +const pkgs = ['@auth0/auth0-auth-js', '@auth0/auth0-server-js']; +pkgs.forEach(pkg => { + try { + const v = require(pkg + '/package.json').version; + console.log(pkg + ': ' + v); + } catch { + console.log(pkg + ': NOT INSTALLED'); + } +}); +" +``` + +--- + +## Section 2: Discover every call site + +Run the following discovery scan against the customer's source root. This is **read-only** — it +greps and counts, changes nothing. Replace `` with the source directory before +running. + +```bash +set -euo pipefail +ROOT="${1:-}" + +if [ ! -d "$ROOT" ]; then + echo "error: '$ROOT' is not a directory" >&2 + echo "usage: bash " >&2 + exit 2 +fi + +FILE_GLOBS=(--include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \ + --include='*.mjs' --include='*.cjs' --include='*.cts' --include='*.mts') +EXCLUDES=(--exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build \ + --exclude-dir=.git --exclude-dir=coverage) + +# grep helper: recursive, line-numbered, extended regex. Never fail on "no matches". +scan() { grep -rEn "${FILE_GLOBS[@]}" "${EXCLUDES[@]}" "$1" "$ROOT" 2>/dev/null || true; } +count() { scan "$1" | wc -l | tr -d ' '; } +section() { printf '\n=== %s ===\n' "$1"; } + +echo "node-auth0 -> auth0-auth-js / auth0-server-js — usage scan" +echo "root: $ROOT" + +section "Imports from the 'auth0' package" +scan "from ['\"]auth0['\"]|require\(['\"]auth0['\"]\)" + +section "AuthenticationClient construction (MIGRATE)" +scan "new[[:space:]]+AuthenticationClient" + +section "UserInfoClient construction (MIGRATE -> claims / getUser / direct fetch)" +scan "new[[:space:]]+UserInfoClient|\.getUserInfo\(" + +section "oauth.* calls (MIGRATE -> AuthClient methods)" +scan "\.oauth\.(authorizationCodeGrant|authorizationCodeGrantWithPKCE|refreshTokenGrant|passwordGrant|clientCredentialsGrant|revokeRefreshToken|tokenForConnection|pushedAuthorization)" + +section "database.* calls (MIGRATE -> authClient.database.*)" +scan "\.database\.(signUp|changePassword)" + +section "passwordless.* calls (MIGRATE -> authClient.passwordless.* + grant methods)" +scan "\.passwordless\.(sendEmail|sendSMS|loginWithEmail|loginWithSMS)" + +section "backchannel.* calls (MIGRATE -> CIBA methods)" +scan "\.backchannel\.(authorize|backchannelGrant)" + +section "tokenExchange.* calls (MIGRATE -> exchangeToken)" +scan "\.tokenExchange\.exchangeToken" + +section "AuthApiError / id-token validation types (MIGRATE -> typed errors / claims)" +scan "AuthApiError|IDTokenValidateOptions|IdTokenValidatorError" + +section "High-risk residue: expires_in arithmetic (INSPECT — expiresAt is absolute)" +scan "expires_in" + +section "ManagementClient usage (DO NOT MIGRATE — stays on 'auth0')" +scan "new[[:space:]]+ManagementClient|ManagementClient" + +section "Summary counts" +printf '%-45s %s\n' "AuthenticationClient constructions:" \ + "$(count 'new[[:space:]]+AuthenticationClient')" +printf '%-45s %s\n' "UserInfoClient / getUserInfo:" \ + "$(count 'new[[:space:]]+UserInfoClient|\.getUserInfo\(')" +printf '%-45s %s\n' "oauth.* calls:" \ + "$(count '\.oauth\.(authorizationCodeGrant|authorizationCodeGrantWithPKCE|refreshTokenGrant|passwordGrant|clientCredentialsGrant|revokeRefreshToken|tokenForConnection|pushedAuthorization)')" +printf '%-45s %s\n' "database.* calls:" \ + "$(count '\.database\.(signUp|changePassword)')" +printf '%-45s %s\n' "passwordless.* calls:" \ + "$(count '\.passwordless\.(sendEmail|sendSMS|loginWithEmail|loginWithSMS)')" +printf '%-45s %s\n' "backchannel.* calls:" \ + "$(count '\.backchannel\.(authorize|backchannelGrant)')" +printf '%-45s %s\n' "tokenExchange.* calls:" \ + "$(count '\.tokenExchange\.exchangeToken')" +printf '%-45s %s\n' "expires_in occurrences (inspect):" \ + "$(count 'expires_in')" +printf '%-45s %s\n' "ManagementClient refs (leave alone):" \ + "$(count 'ManagementClient')" + +echo +echo "Next: use the routing decision above and the constructor mapping in Section 3," +echo "then apply the four structural changes from Section 4 before rewriting each call site." +``` + +Use the output to decide: +1. **Which call sites to migrate** — everything AuthenticationClient/UserInfoClient-related. +2. **Which target SDK** — see the decision procedure in the introduction above. +3. **What NOT to touch** — ManagementClient usage stays on `auth0`. + +--- + +## Section 3: Install the target SDK and rewrite the constructor + +### Import rewrites + +```ts +// before +import { AuthenticationClient, UserInfoClient, AuthApiError } from 'auth0'; + +// after — auth-js target +import { AuthClient, TokenByCodeError, TokenByPasswordError } from '@auth0/auth0-auth-js'; + +// after — server-js target +import { ServerClient } from '@auth0/auth0-server-js'; +``` + +### node-auth0 `AuthenticationClient` → `@auth0/auth0-auth-js` `AuthClient` + +```ts +// before +new AuthenticationClient({ + domain: 'tenant.us.auth0.com', + clientId: '...', + clientSecret: '...', // OR clientAssertionSigningKey + clientAssertionSigningKey: '...', + clientAssertionSigningAlg: 'RS256', + idTokenSigningAlg: 'RS256', // for manual id_token validation + clockTolerance: 60, // seconds, for validation + useMTLS: false, + telemetry: true, + headers: { 'X-Custom': '...' }, // sent on every request + timeoutDuration: 10000, // ms + retry: { ... }, + agent: dispatcher, // an undici Dispatcher instance + fetch: customFetch, + middleware: [...], +}); + +// after +import { AuthClient } from '@auth0/auth0-auth-js'; + +new AuthClient({ + domain: 'tenant.us.auth0.com', // same (no https:// scheme) + clientId: '...', // same + clientSecret: '...', // same + clientAssertionSigningKey: '...', // same (string | CryptoKey) + clientAssertionSigningAlg: 'RS256', // same + authorizationParams: { // NEW: default scope/audience/redirect_uri for URL builders + scope: 'openid profile email', + audience: 'https://api.example.com', + redirect_uri: 'https://app.example.com/callback', + }, + useMtls: false, // RENAMED from useMTLS (lowercase tls) + customFetch: fetch, // RENAMED from fetch + telemetry: { ... }, // structured TelemetryConfig + discoveryCache: { ttl, maxEntries }, // NEW: OIDC discovery / JWKS cache +}); +``` + +**Option-by-option:** + +| node-auth0 | auth0-auth-js | Notes | +|---|---|---| +| `domain` | `domain` | Unchanged. No `https://` scheme. | +| `clientId` | `clientId` | Unchanged. | +| `clientSecret` | `clientSecret` | Unchanged. | +| `clientAssertionSigningKey` | `clientAssertionSigningKey` | Unchanged. Now also accepts a `CryptoKey`. | +| `clientAssertionSigningAlg` | `clientAssertionSigningAlg` | Unchanged. | +| `useMTLS` | `useMtls` | **Renamed** (lowercase `tls`). | +| `fetch` | `customFetch` | **Renamed.** | +| `telemetry: boolean` | `telemetry: TelemetryConfig` | Now a structured object. | +| `headers` (global) | per-call `RequestOptions.headers` | Moved to per-request options; set per call site rather than globally. | +| `timeoutDuration` | per-call `RequestOptions.signal` (AbortSignal timeout) | Use `AbortSignal.timeout(ms)` on the call. | +| `retry` | (configure via `customFetch`) | Wrap your fetch with retry if needed. | +| `agent` | (configure via `customFetch`) | Set the dispatcher inside your custom fetch. | +| `middleware` | `customFetch` | Compose behavior in the fetch wrapper. | +| `idTokenSigningAlg` | — (internal) | ID-token validation is internal; read `TokenResponse.claims`. | +| `clockTolerance` | — (internal) | Handled internally during validation. | + +### Migrating global config to per-request options + +node-auth0's global `headers`, `timeoutDuration`, `agent`, `retry`, and `middleware` options +do not have direct constructor equivalents in auth0-auth-js. Methods accept a trailing +`RequestOptions` parameter: + +```ts +import type { RequestOptions } from '@auth0/auth0-auth-js'; + +const tokens = await authClient.getTokenByClientCredentials( + { audience: 'https://api.example.com' }, + { + headers: { 'X-Custom': 'value' }, + signal: AbortSignal.timeout(5000), // timeout in ms + } satisfies RequestOptions +); +``` + +**Type import:** `@auth0/auth0-server-js` re-exports `RequestOptions`, `ApiResponse`, and +`FullResponseOption` from `@auth0/auth0-auth-js`, so you can import any of them from either +package. + +**Arity rule:** MFA methods (`authClient.mfa.*`) take `requestOptions` as the 2nd argument; +store-first methods on `serverClient` take it as the 3rd argument after the store context; cache +hits ignore it entirely. + +**Common patterns:** + +- **Global headers:** Apply via `RequestOptions.headers` on each call, or wrap `customFetch` + once to inject everywhere. +- **Timeout:** Replace `timeoutDuration: 10000` with `signal: AbortSignal.timeout(10000)` on + the call. +- **Agent (Node.js dispatcher):** Wrap `customFetch` to inject the agent into the HTTP transport. +- **Retry / middleware:** Compose behavior in a `customFetch` wrapper passed at construction or + per request. + +### `@auth0/auth0-server-js` `ServerClient` constructor + +ServerClient wraps an `AuthClient` and adds the session machinery. It shares the auth options +and **adds required stores**: + +```ts +import { ServerClient } from '@auth0/auth0-server-js'; + +new ServerClient({ + domain: 'tenant.us.auth0.com', // string, or a DomainResolver for multi-tenant + clientId: '...', + clientSecret: '...', // or clientAssertionSigningKey / mTLS + authorizationParams: { + scope: 'openid profile email offline_access', // offline_access => refresh tokens + audience: 'https://api.example.com', + redirect_uri: 'https://app.example.com/callback', + }, + transactionStore, // REQUIRED — holds the in-flight login (state, PKCE verifier) + stateStore, // REQUIRED — holds the established session (user + tokens) + stateIdentifier: '__a0_session', // cookie/store key (default) + transactionIdentifier: '__a0_tx', // cookie/store key (default) + customFetch: fetch, + useMtls: false, + telemetry: { ... }, +}); +``` + +The `transactionStore` and `stateStore` have **no node-auth0 counterpart** — they are the +session substrate. See Section 7 for how to construct them. + +--- + +## Section 4: The four cross-cutting structural changes + +Every call-site rewrite is subject to four changes that cut across all methods. They cause the +overwhelming majority of migration defects. All four are silent in plain JavaScript — the code +runs but produces wrong behavior at runtime. In TypeScript, the compiler catches the +return-shape and casing changes at compile time; only the `expires_in`→`expiresAt` expiry +change compiles clean in both JS and TS, so that is the one to hunt manually. Apply each change +deliberately, not just the method rename. + +1. Return shape: `JSONApiResponse` → domain object directly +2. Casing: snake_case wire shape → camelCase +3. Token expiry: `expires_in` (relative) → `expiresAt` (absolute) — **most dangerous** +4. Error model: `AuthApiError` → typed per-operation errors + +--- + +### 4.1 Return shape: `JSONApiResponse` → domain object + +#### What changed + +node-auth0 wraps most Authentication API results in a response envelope: + +- `JSONApiResponse` — has `.data` (the payload), `.status` (number), `.statusText`, + `.headers` (a `Headers` object). +- `VoidApiResponse` — same envelope, `.data` is `undefined` (used by `sendEmail`, + `revokeRefreshToken`, etc.). +- `TextApiResponse` — `.data` is a `string` (used by `database.changePassword`). + +**Exception:** `backchannel.authorize`, `backchannel.backchannelGrant`, and +`tokenExchange.exchangeToken` return domain objects directly (no `.data` wrapper) in node-auth0. + +The new SDKs **drop the envelope** and return the domain object directly: + +- Token grants return a `TokenResponse` instance. +- `database.signUp` returns a `SignUpResult` object. +- `database.changePassword` returns a `string`. +- `sendEmail` / `sendSms` / `revokeToken` return `void`. + +HTTP metadata (status code, headers such as `x-request-id`, rate-limit headers) is available +through the per-operation error objects on failure paths. On **success paths**, metadata is +available via the opt-in `fullResponse` envelope (see "Reading HTTP response metadata" below). + +#### The rewrite + +Delete `.data` indirection on every success path: + +```ts +// before +const resp = await auth0.oauth.clientCredentialsGrant({ audience }); +const token = resp.data.access_token; +const status = resp.status; + +// after +const tokens = await authClient.getTokenByClientCredentials({ audience }); +const token = tokens.accessToken; +``` + +```ts +// before — changePassword returned TextApiResponse +const resp = await auth0.database.changePassword({ email, connection }); +console.log(resp.data); + +// after — returns the string directly +const message = await authClient.database.changePassword({ email, connection }); +console.log(message); +``` + +#### Reading HTTP response metadata (fullResponse) + +When your node-auth0 code reads HTTP response metadata on a **success path**, migrate to the +opt-in envelope rather than dropping the read. This is most common for rate-limit tracking, +request-id logging, or retry-after telemetry: + +```ts +// before (node-auth0): metadata on the success envelope +const resp = await auth0.oauth.clientCredentialsGrant({ audience }); +const remaining = resp.headers.get('x-ratelimit-remaining'); +const token = resp.data.access_token; + +// after: opt in to the envelope +const { data, response } = await authClient.getTokenByClientCredentials( + { audience, fullResponse: true }); +const remaining = response.headers.get('x-ratelimit-remaining'); +const token = data.accessToken; +``` + +The same opt-in covers non-token Authentication API methods: + +| Method | Bare return | `fullResponse: true` return | +|---|---|---| +| `database.signUp` | `SignUpResult` | `ApiResponse` | +| `database.changePassword` | `string` | `ApiResponse` | +| `passwordless.sendEmail` | `void` | `ApiResponse` (`data` is `undefined`) | +| `passwordless.sendSms` | `void` | `ApiResponse` (`data` is `undefined`) | + +```ts +// before (node-auth0): read the request id off the signup envelope +const resp = await auth0.database.signUp({ email, password, connection }); +const reqId = resp.headers.get('x-request-id'); + +// after: opt in to the envelope +const { data, response } = await authClient.database.signUp( + { email, password, connection, fullResponse: true }); +const reqId = response.headers.get('x-request-id'); + +// void-returning methods expose the Response with an undefined `data` +const { response: sendResp } = await authClient.passwordless.sendEmail( + { email, fullResponse: true }); +const rateLimit = sendResp.headers.get('x-ratelimit-remaining'); +``` + +**Caveats:** + +- Pass `fullResponse: true` as a literal, not a variable. Using spread — + `{ ...opts, fullResponse: true }` — widens `true` to `boolean`, causing TypeScript overload + resolution to fall back to the bare return type. Fix: `{ ...opts, fullResponse: true as const }`. +- When `fullResponse: true` is requested on a **token** method and a cached token is still valid, + the cache is bypassed to force a token-endpoint round-trip. Repeated calls with this flag on a + hot cache will trigger repeated exchanges. +- Reserved headers: a caller `Authorization` header is ignored and the `Auth0-Client` telemetry + header always wins; `RequestOptions.headers` cannot override them. +- Per-request `customFetch` replaces the base transport for that call but does not inherit mTLS; + if you rely on mTLS, the supplied fetch must itself be mTLS-capable. + +**Decision guidance:** default to the bare return type. Reach for `fullResponse` only where the +customer actually consumed response metadata on success. `MissingCapturedResponseError` is an +internal-bug sentinel; customers do not normally catch it. + +#### Gotchas + +- **Void methods.** Code that did `const r = await auth0.passwordless.sendEmail(...)` and then + checked `r.status === 200` must drop that check — by default the method returns `void` and + throws on failure. Rely on the thrown error instead. If success status/headers are genuinely + needed, opt into `fullResponse: true` to get an `ApiResponse` whose `response` carries + the status/headers. +- **Header reads.** Any code reading `resp.headers.get('x-ratelimit-remaining')` on a success + path needs the opt-in `fullResponse` envelope. Search the customer's code for `.headers` on + response values. +- **Do not hand-roll a compatibility shim.** Resist reintroducing a custom `{ data, status }` + shape. Let the domain object flow through; it keeps the migration honest and avoids a + compatibility shim you would have to maintain. + +--- + +### 4.2 Casing: snake_case wire shape → camelCase + +#### What changed + +node-auth0's public API exposes the **snake_case wire shape** verbatim on both inputs and +outputs. The new SDKs use **camelCase** for the public API and only translate to snake_case at +the HTTP boundary internally. + +#### Input parameters — field map + +| node-auth0 (snake_case) | new SDK (camelCase) | +|---|---| +| `client_id` | `clientId` | +| `client_secret` | `clientSecret` | +| `refresh_token` | `refreshToken` | +| `redirect_uri` | (via `authorizationParams.redirect_uri` on config / builder) | +| `code_verifier` | `codeVerifier` | +| `phone_number` | `phoneNumber` | +| `auth_req_id` | `authReqId` | +| `binding_message` | `bindingMessage` | +| `subject_token` / `subject_token_type` | `subjectToken` / `subjectTokenType` | +| `given_name` / `family_name` | `givenName` / `familyName` | +| `user_metadata` | `userMetadata` | +| `login_hint` | `loginHint` | + +#### Output fields — `TokenResponse` field map + +| node-auth0 `TokenSet` (snake_case) | new SDK `TokenResponse` (camelCase) | +|---|---| +| `access_token` | `accessToken` | +| `refresh_token` | `refreshToken` | +| `id_token` | `idToken` | +| `token_type` | `tokenType` | +| `expires_in` (relative seconds) | `expiresAt` (**absolute Unix timestamp — see §4.3**) | +| `scope` | `scope` | +| — (had to decode id_token yourself) | `claims` (already-decoded ID token claims) | +| `authorization_details` | `authorizationDetails` | + +#### The rewrite + +Rename fields on both the arguments you pass in and the fields you read out. Watch nested objects +(`user_metadata`, `authorization_details`) — the nested keys inside `userMetadata` are your own +application data and are **not** re-cased by the SDK; only the SDK's own known fields change. + +```ts +// before +const resp = await auth0.oauth.refreshTokenGrant({ refresh_token: rt }); +const newRt = resp.data.refresh_token; +const idToken = resp.data.id_token; + +// after +const tokens = await authClient.getTokenByRefreshToken({ refreshToken: rt }); +const newRt = tokens.refreshToken; +const idToken = tokens.idToken; +``` + +> **Keys that look renamed but are your data:** `user_metadata` → `userMetadata` is a rename of +> the SDK's parameter. The object *inside* it (e.g. `{ plan: 'free' }`) is passed through +> untouched. Do not rename the customer's own metadata keys. + +--- + +### 4.3 Token expiry: `expires_in` (relative) → `expiresAt` (absolute) + +**This is the highest-risk change in the migration. It is silent, it compiles, and it corrupts +session lifetimes.** + +#### What changed + +- node-auth0 `TokenSet.expires_in` = the token's **lifetime in seconds relative to now** + (e.g. `86400` for a 24-hour token). This is the raw OAuth `expires_in` from the wire. +- new SDK `TokenResponse.expiresAt` = an **absolute Unix timestamp in seconds** (e.g. + `1786000000`) computed by the SDK as roughly `now + expires_in`. + +#### Why it bites + +Existing node-auth0 code almost always converts the relative value to an absolute deadline: + +```ts +// before — very common node-auth0 pattern +const resp = await auth0.oauth.refreshTokenGrant({ refresh_token: rt }); +const expiresAtMs = Date.now() + resp.data.expires_in * 1000; // stored deadline +``` + +If you mechanically rename `expires_in` → `expiresAt` and leave the arithmetic: + +```ts +// WRONG — double-counts "now" +const tokens = await authClient.getTokenByRefreshToken({ refreshToken: rt }); +const expiresAtMs = Date.now() + tokens.expiresAt * 1000; // ~ now + (now + lifetime) → far future +``` + +The stored deadline lands decades in the future, so the token is treated as valid long after it +has actually expired → 401s in production that the app never proactively refreshes. + +#### The rewrite + +`expiresAt` is *already* the deadline. Do not add `Date.now()`. + +```ts +// after — correct +const tokens = await authClient.getTokenByRefreshToken({ refreshToken: rt }); +const expiresAtMs = tokens.expiresAt * 1000; // absolute; convert s → ms only if you store ms +``` + +If downstream code genuinely needs the *relative* remaining lifetime (e.g. to set a cookie +`Max-Age`), compute it from the absolute value: + +```ts +const secondsRemaining = tokens.expiresAt - Math.floor(Date.now() / 1000); +``` + +#### How to find every instance + +The residue check in Section 8 flags `expires_in` arithmetic, but also grep the customer's code +for these patterns and inspect each by hand: + +- `expires_in` +- `Date.now() +` near a token result +- `+ expires` / `* 1000` near a token result +- any stored field named `expiresAt`, `expires_at`, `expiry`, `tokenExpiry` fed from a grant + +Every one of these is a candidate for the double-count bug. + +--- + +### 4.4 Error model: `AuthApiError` → typed per-operation errors + +#### What changed + +node-auth0 throws a single error type for Authentication API failures: + +```ts +class AuthApiError extends Error { + name: 'AuthApiError'; + error: string; // OAuth error code, e.g. 'invalid_grant' + error_description: string; + statusCode: number; + body: string; + headers: Headers; +} +``` + +The new SDKs throw **typed, per-operation error classes** — `TokenByCodeError`, +`TokenByRefreshTokenError`, `TokenByClientCredentialsError`, `TokenByPasswordError`, +`TokenExchangeError`, `TokenRevocationError`, `PasswordlessStartError`, +`PasswordlessChallengeError`, `PasswordlessDbGetTokenError`, `MfaEnrollmentError`, etc. Each +carries a structured `.cause` (the underlying OAuth2 error) rather than flat `error` / +`error_description` strings. + +#### The rewrite — generic catch + +```ts +// before +try { + await auth0.oauth.refreshTokenGrant({ refresh_token: rt }); +} catch (e) { + if (e instanceof AuthApiError && e.error === 'invalid_grant') { + // refresh token revoked/expired + } +} + +// after +import { TokenByRefreshTokenError } from '@auth0/auth0-auth-js'; +try { + await authClient.getTokenByRefreshToken({ refreshToken: rt }); +} catch (e) { + if (e instanceof TokenByRefreshTokenError && e.cause?.error === 'invalid_grant') { + // refresh token revoked/expired + } +} +``` + +Import the specific error class for the operation you are calling. If the customer had one broad +`catch (e instanceof AuthApiError)` around several different operations, either widen to catch +each operation's error type or check the shared base behavior — but prefer the specific type per +call site, since it documents which operation can fail. + +#### MFA detection + +```ts +// before +try { + await auth0.oauth.passwordGrant({ username, password }); +} catch (e) { + if (e instanceof AuthApiError && e.error === 'mfa_required') { + // start MFA flow using e (mfa_token is in the body) + } +} + +// after +import { TokenByPasswordError } from '@auth0/auth0-auth-js'; +try { + await authClient.getTokenByPassword({ username, password }); +} catch (e) { + if (e instanceof TokenByPasswordError && e.cause?.error === 'mfa_required') { + // drive the MFA challenge via authClient.mfa.* + } +} +``` + +> After detecting `mfa_required`, the MFA enroll/challenge/verify flow that node-auth0 handled +> ad hoc now lives on `authClient.mfa.*` (`listAuthenticators`, `enrollAuthenticator`, +> `challengeAuthenticator`, `verify`). In server-js, `serverClient.mfa.verify()` also persists +> the resulting tokens to the session. + +#### ID-token validation types + +node-auth0 exposed `IDTokenValidateOptions` and `IdTokenValidatorError` for callers doing manual +ID-token validation. The new SDK validates ID tokens internally and exposes the decoded, validated +result as `TokenResponse.claims`. + +`IDTokenValidateOptions` and `IdTokenValidatorError` are removed in the new SDK. Delete all +manual ID-token validation code — the new SDK handles nonce, max_age, and other validation +internally during the grant call and exposes the decoded, verified claims as +`TokenResponse.claims`. There is no caller-facing `nonce` or `maxAge` option on +`getTokenByCode`; delete those call sites entirely: + +```ts +// before — manual ID-token validation +import { IDTokenValidateOptions, IdTokenValidatorError } from 'auth0'; +// ... validator.validate(idToken, { nonce: 'abc', maxAge: 300 }) ... + +// after — delete all manual ID-token validation; read the claims the SDK already verified +const tokens = await authClient.getTokenByCode(callbackUrl, { codeVerifier }); +const claims = tokens.claims; // decoded, validated ID token claims +// Remove IDTokenValidateOptions, IdTokenValidatorError, and any IDTokenValidator imports. +``` + +--- + +### 4.5 Checklist per call site + +For every node-auth0 auth call you rewrite, confirm all four: + +- [ ] **Return shape** — removed `.data` / `.status` / `.headers` access on the success path. +- [ ] **Casing** — renamed every snake_case field on input args and output reads to camelCase. +- [ ] **Expiry** — any code using the old `expires_in` now uses `expiresAt` as an *absolute* + timestamp; no `Date.now() +` was left in front of it. +- [ ] **Errors** — `AuthApiError` catches replaced with the specific typed error (`.cause.error`); + for `mfa_required`, check `e.cause?.error === 'mfa_required'` on the typed error. + +--- + +## Section 5: Apply the four structural changes at every call site + +The four structural changes from Section 4 are **mandatory at every call site** — apply them +simultaneously with the method rename in Section 6. That combination is the source of nearly all +migration bugs. + +--- + +## Section 6: Method-by-method API mapping + +**How to read this section:** unless a row explicitly routes to `@auth0/auth0-server-js`, the +replacement lives on the `@auth0/auth0-auth-js` `AuthClient` (or one of its sub-clients: +`authClient.database`, `authClient.passwordless`, `authClient.mfa`, `authClient.passkey`). + +**Before you touch any method,** internalize the four structural changes in Section 4 — return +shape, casing, `expires_in` → `expiresAt`, and error model. This section shows the method and +parameter mapping; Section 4 shows the field-level and error-level mapping. You need both. + +--- + +### `AuthenticationClient.oauth.*` → `AuthClient` methods + +node-auth0's OAuth sub-client is the largest surface. All of these move onto the `AuthClient` +instance directly (not a sub-client). + +#### `oauth.authorizationCodeGrant` → `getTokenByCode` + +The single most important semantic change in the whole migration. + +> **Server-js routing:** If your routing decision is `@auth0/auth0-server-js`, do NOT use +> `getTokenByCode` for the callback handler. Use +> `serverClient.completeInteractiveLogin(callbackUrl, storeOptions)` instead (covered in +> Section 7). Skip the rest of this entry. + +**node-auth0** — you pass the raw authorization `code` (and `redirect_uri`) that you extracted +from the callback query string yourself: + +```ts +import { AuthenticationClient } from 'auth0'; + +const auth0 = new AuthenticationClient({ domain, clientId, clientSecret }); + +// You parsed `code` out of the callback URL yourself. +const resp = await auth0.oauth.authorizationCodeGrant({ + code, + redirect_uri: 'https://app.example.com/callback', +}); +const accessToken = resp.data.access_token; +const expiresIn = resp.data.expires_in; // relative seconds +const reqId = resp.headers.get('x-request-id'); // metadata on success envelope +``` + +**auth0-auth-js** — you pass the **entire callback `URL`**. The SDK extracts +`code` and performs the PKCE exchange. `redirect_uri` comes from the `AuthClient` +config / `authorizationParams`, not the call. + +> **CSRF: validate `state` before calling `getTokenByCode` or `completeInteractiveLogin`.** +> Neither `AuthClient.getTokenByCode` nor `ServerClient.completeInteractiveLogin` compares +> the callback `state` against your stored transaction state. `completeInteractiveLogin` +> deletes (consumes) the pending transaction after the exchange, but performs no explicit +> state comparison. Validate the callback `state` against your stored value before calling +> either method; abort on mismatch. PKCE binds the authorization code to the stored +> `codeVerifier`, so a substituted code is rejected at the token endpoint, but that is +> not a substitute for explicit state validation. + +```ts +import { AuthClient } from '@auth0/auth0-auth-js'; + +const authClient = new AuthClient({ domain, clientId, clientSecret }); + +// `callbackUrl` is a URL object for the full incoming request URL, +// e.g. new URL(req.url, `https://${req.headers.host}`) + +// 1. Validate state BEFORE exchanging the code (CSRF protection). +const callbackState = callbackUrl.searchParams.get('state'); +if (!callbackState || callbackState !== storedState) { + throw new Error('State mismatch — possible CSRF attack; abort the flow.'); +} + +// 2. Exchange the code for tokens. +const tokens = await authClient.getTokenByCode(callbackUrl, { + codeVerifier: verifier, // PKCE: the verifier you persisted before the redirect +}); +const accessToken = tokens.accessToken; +const expiresAt = tokens.expiresAt; // absolute Unix seconds (not relative) +``` + +> **PKCE is the only supported flow for `getTokenByCode`.** Pass the `codeVerifier` you +> persisted before the redirect. + +> **Gotcha:** if the customer's code manually parses `req.query.code`, that parsing is now the +> SDK's job. Delete it and hand the SDK the full URL. **Header reads on success:** if the +> node-auth0 code read `resp.headers.get(...)` on success (for rate-limit telemetry or +> request-id logging), see Section 4.1 "Reading HTTP response metadata (fullResponse)" for the +> opt-in envelope. + +#### `oauth.authorizationCodeGrantWithPKCE` → `getTokenByCode` (with verifier) + +**node-auth0:** + +```ts +const resp = await auth0.oauth.authorizationCodeGrantWithPKCE({ + code, + code_verifier: verifier, + redirect_uri: 'https://app.example.com/callback', +}); +``` + +**auth0-auth-js** — PKCE is folded into the same method; supply the code verifier via options. +Typically the verifier was produced earlier by `buildAuthorizationUrl` (see below), which returns +a `codeVerifier` for you to persist: + +```ts +const tokens = await authClient.getTokenByCode(callbackUrl, { + codeVerifier: verifier, +}); +``` + +> If the customer builds the authorization URL themselves today, prefer switching them to +> `authClient.buildAuthorizationUrl()` (below) so the SDK generates and returns the +> `codeVerifier`, then persist it and pass it back to `getTokenByCode`. + +#### `oauth.refreshTokenGrant` → `getTokenByRefreshToken` + +```ts +// node-auth0 +const resp = await auth0.oauth.refreshTokenGrant({ refresh_token: rt }); +// auth0-auth-js +const tokens = await authClient.getTokenByRefreshToken({ refreshToken: rt }); +``` + +#### `oauth.passwordGrant` → `getTokenByPassword` + +```ts +// node-auth0 +const resp = await auth0.oauth.passwordGrant({ + username, password, realm: 'Username-Password-Authentication', audience, scope, +}); +// auth0-auth-js +const tokens = await authClient.getTokenByPassword({ + username, password, realm: 'Username-Password-Authentication', audience, scope, +}); +``` + +#### `oauth.clientCredentialsGrant` → `getTokenByClientCredentials` + +The canonical M2M grant. This is the most common reason to stay on **auth0-auth-js** rather than +adopt server-js — there is no user session involved. + +```ts +// node-auth0 +const resp = await auth0.oauth.clientCredentialsGrant({ audience: 'https://api.example.com' }); +const token = resp.data.access_token; +// auth0-auth-js +const tokens = await authClient.getTokenByClientCredentials({ audience: 'https://api.example.com' }); +const token = tokens.accessToken; +``` + +#### `oauth.revokeRefreshToken` → `revokeToken` + +Renamed, and simplified return (was `VoidApiResponse`, now `void`). + +```ts +// node-auth0 +await auth0.oauth.revokeRefreshToken({ token: rt }); +// auth0-auth-js +await authClient.revokeToken({ token: rt }); +``` + +> **Session apps:** if you are migrating to server-js and this revoke was part of logout, use +> `serverClient.revokeRefreshToken()` (it reads the refresh token from the session) instead of +> the low-level `revokeToken`. See Section 7. + +#### `oauth.tokenForConnection` → `exchangeToken` (Token Vault) + +`getTokenForConnection` also exists on `AuthClient` but is **deprecated**; prefer `exchangeToken`. + +```ts +// node-auth0 +const resp = await auth0.oauth.tokenForConnection({ + connection: 'google-oauth2', + subject_token: refreshToken, + subject_token_type: 'urn:ietf:params:oauth:token-type:refresh_token', + login_hint: userId, +}); +// auth0-auth-js (Token Vault exchange overload) +const tokens = await authClient.exchangeToken({ + connection: 'google-oauth2', + subjectToken: refreshToken, + subjectTokenType: 'urn:ietf:params:oauth:token-type:refresh_token', + loginHint: userId, +}); +``` + +#### `oauth.pushedAuthorization` (PAR) → `buildAuthorizationUrl({ pushedAuthorizationRequests: true })` + +There is **no standalone PAR method** in the new SDK. Pushed Authorization is a flag on the +authorization-URL builder. The SDK performs the PAR POST and returns an authorization URL that +references the resulting `request_uri`. + +```ts +// node-auth0 — explicit PAR call returning { request_uri, expires_in } +const resp = await auth0.oauth.pushedAuthorization({ + response_type: 'code', + redirect_uri: 'https://app.example.com/callback', +}); +// build the /authorize URL yourself from resp.data.request_uri ... + +// auth0-auth-js — PAR is handled inside buildAuthorizationUrl +const { authorizationUrl, codeVerifier } = await authClient.buildAuthorizationUrl({ + pushedAuthorizationRequests: true, + authorizationParams: { redirect_uri: 'https://app.example.com/callback' }, +}); +// redirect the user to authorizationUrl; persist codeVerifier for the callback +``` + +> Requires the tenant to expose a `pushed_authorization_request_endpoint`. The SDK throws if PAR +> is requested but unsupported by tenant metadata. + +#### Authorization-URL and logout-URL builders + +node-auth0 left `/authorize` URL construction to the caller (or to `express-openid-connect`). +The new SDK gives you `buildAuthorizationUrl()` and `buildLogoutUrl()`: + +```ts +const { authorizationUrl, codeVerifier } = await authClient.buildAuthorizationUrl({ + authorizationParams: { redirect_uri, scope: 'openid profile email', audience }, +}); +// ... later, on logout: +const logoutUrl = await authClient.buildLogoutUrl({ returnTo: 'https://app.example.com' }); +``` + +> **Breaking change — both builders are async.** Unlike the manual URL construction common +> in node-auth0 apps (which was synchronous), both `buildAuthorizationUrl` and `buildLogoutUrl` +> return `Promise<...>` because they perform OIDC discovery to resolve the authorization and +> end-session endpoints. Any Express route or helper that calls either method must be made +> `async` and must `await` the result. A synchronous wrapper will silently return a `Promise` +> object instead of a URL string. + +--- + +### `AuthenticationClient.database.*` → `authClient.database.*` + +Database connection operations move to the `authClient.database` sub-client. Names and required +params are unchanged; only casing and return shape change. + +#### `database.signUp` → `authClient.database.signUp` + +```ts +// node-auth0 +const resp = await auth0.database.signUp({ + email, password, connection: 'Username-Password-Authentication', + given_name: 'Ada', family_name: 'Lovelace', user_metadata: { plan: 'free' }, +}); +const userId = resp.data.id; +// auth0-auth-js +const result = await authClient.database.signUp({ + email, password, connection: 'Username-Password-Authentication', + givenName: 'Ada', familyName: 'Lovelace', userMetadata: { plan: 'free' }, +}); +const userId = result.id; +``` + +> **ID normalization is preserved.** node-auth0 mapped the server's `_id | user_id | id` onto a +> single `id`. The new SDK does the same, so `result.id` is always present. Do not add your own +> `_id` fallback. +> +> **Header reads on success:** node-auth0's `resp.headers`/`resp.status` on the signup envelope +> map to `fullResponse: true`, which returns `ApiResponse` (`{ data, response }`). +> See Section 4.1 "Reading HTTP response metadata (fullResponse)". + +#### `database.changePassword` → `authClient.database.changePassword` + +Note the return type: node-auth0 returned a `TextApiResponse` (read via `.data`); the new SDK +returns the plain `string` directly. + +```ts +// node-auth0 +const resp = await auth0.database.changePassword({ + email, connection: 'Username-Password-Authentication', +}); +const message = resp.data; // plain-text confirmation +// auth0-auth-js +const message = await authClient.database.changePassword({ + email, connection: 'Username-Password-Authentication', +}); +``` + +> **Header reads on success:** if the node-auth0 code read response headers on the success path, +> see Section 4.1 "Reading HTTP response metadata (fullResponse)". + +--- + +### `AuthenticationClient.passwordless.*` → split: `authClient.passwordless.*` + grant methods + +node-auth0 lumped "start" (send the code/link) and "login" (redeem the code) onto one sub-client. +The new SDK **splits** them: starting stays on `authClient.passwordless`; redeeming a code +becomes a top-level grant method on `AuthClient`. + +#### `passwordless.sendEmail` → `authClient.passwordless.sendEmail` + +```ts +// node-auth0 +await auth0.passwordless.sendEmail({ email, send: 'code' }); +// auth0-auth-js +await authClient.passwordless.sendEmail({ email, send: 'code' }); +``` + +> **Default changed.** node-auth0 defaulted `send` to `'link'` (magic link). The new SDK defaults +> `send` to `'code'` (OTP). If the customer relied on the implicit default to send magic links, +> set `send: 'link'` explicitly. + +#### `passwordless.sendSMS` → `authClient.passwordless.sendSms` + +Note the casing change: `sendSMS` → `sendSms`, and `phone_number` → `phoneNumber`. + +```ts +// node-auth0 +await auth0.passwordless.sendSMS({ phone_number: '+15551234567' }); +// auth0-auth-js +await authClient.passwordless.sendSms({ phoneNumber: '+15551234567' }); +``` + +> **Header reads on success:** `sendEmail` and `sendSms` return `void` by default. If the +> node-auth0 code read `resp.headers`/`resp.status` off the `VoidApiResponse`, opt into +> `fullResponse: true` to get an `ApiResponse`. See Section 4.1. + +#### `passwordless.loginWithEmail` → `getTokenByPasswordlessEmail` + +Redeeming the OTP is now a **grant method on `AuthClient`**, not on the passwordless sub-client. + +```ts +// node-auth0 +const resp = await auth0.passwordless.loginWithEmail({ email, code, audience, scope }); +const token = resp.data.access_token; +// auth0-auth-js +const tokens = await authClient.getTokenByPasswordlessEmail({ email, code, audience, scope }); +const token = tokens.accessToken; +``` + +#### `passwordless.loginWithSMS` → `getTokenByPasswordlessSms` + +```ts +// node-auth0 +const resp = await auth0.passwordless.loginWithSMS({ phone_number, code }); +// auth0-auth-js +const tokens = await authClient.getTokenByPasswordlessSms({ phoneNumber, code }); +``` + +> **Session apps:** server-js exposes `startPasswordless` / `completePasswordless` / +> `completePasswordlessMagicLink`, which both send the code and establish a session. Use those +> instead of the two-step auth-js flow when the SDK owns the session. + +--- + +### `AuthenticationClient.backchannel.*` (CIBA) → `AuthClient` backchannel methods + +#### `backchannel.authorize` → `initiateBackchannelAuthentication` + +```ts +// node-auth0 +const resp = await auth0.backchannel.authorize({ + binding_message: 'ABC123', scope: 'openid', userId: 'auth0|123', +}); +const authReqId = resp.auth_req_id; +// auth0-auth-js +const { authReqId, expiresIn, interval } = await authClient.initiateBackchannelAuthentication({ + bindingMessage: 'ABC123', loginHint: { sub: 'auth0|123' }, authorizationParams: { scope: 'openid' }, +}); +``` + +#### `backchannel.backchannelGrant` → `backchannelAuthenticationGrant` + +```ts +// node-auth0 +const resp = await auth0.backchannel.backchannelGrant({ auth_req_id: authReqId }); +// auth0-auth-js +const tokens = await authClient.backchannelAuthenticationGrant({ authReqId }); +``` + +> **One-shot convenience:** `authClient.backchannelAuthentication({ ... })` initiates *and* polls +> to completion, returning a `TokenResponse`. Use it if the customer's code did the +> initiate-then-poll loop by hand. +> +> **Session apps:** server-js exposes `loginBackchannel(...)` which runs CIBA and establishes a +> session in one call. + +--- + +### `AuthenticationClient.tokenExchange.*` (RFC 8693) → `exchangeToken` + +```ts +// node-auth0 +const resp = await auth0.tokenExchange.exchangeToken({ + subject_token_type: 'urn:example:custom', + subject_token: token, + audience: 'https://api.example.com', + scope: 'read', +}); +// auth0-auth-js +const tokens = await authClient.exchangeToken({ + subjectTokenType: 'urn:example:custom', + subjectToken: token, + audience: 'https://api.example.com', + scope: 'read', +}); +``` + +> `exchangeToken` is overloaded: a custom-exchange profile shape (`subjectTokenType` + +> `subjectToken` + `audience`) and a Token-Vault shape (`connection` present). Presence of +> `connection` routes to the vault path. The custom-exchange profile is the RFC 8693 replacement +> for `tokenExchange.exchangeToken`. +> +> **Session apps:** server-js exposes `loginWithCustomTokenExchange` (exchange and establish +> session) and `customTokenExchange` (exchange, return tokens, no session). + +--- + +### `UserInfoClient` → `TokenResponse.claims` / `authClient.getUserInfo` / `serverClient.getUser` + +The standalone `UserInfoClient` from node-auth0 does not exist in the new SDK. Choose the +replacement based on what the app needs: + +| Customer's intent | Replacement | +|---|---| +| Wanted user profile claims right after login | Read `TokenResponse.claims` — the SDK already decodes the ID token. No extra `/userinfo` round-trip needed. **Preferred.** | +| Wanted a live `/userinfo` response for an arbitrary access token (auth-js) | `await authClient.getUserInfo({ accessToken })` — direct method on `AuthClient`. | +| Wanted the profile in a server-rendered app with a session | `await serverClient.getUser()` returns the stored user claims from the session. | +| Genuinely needs a raw `/userinfo` fetch | Call the `/userinfo` endpoint directly with `fetch`. The endpoint is in the tenant's server metadata (`getServerMetadata()`). | + +**Before (node-auth0):** + +```ts +import { UserInfoClient } from 'auth0'; +const userInfo = new UserInfoClient({ domain }); +const resp = await userInfo.getUserInfo(accessToken); +const profile = resp.data; // { sub, name, email, ... } +``` + +**After — preferred, use the claims you already have:** + +```ts +const tokens = await authClient.getTokenByCode(callbackUrl, { codeVerifier }); +const profile = tokens.claims; // { sub, name, email, ... } decoded from the id_token +``` + +**After — direct method (auth0-auth-js), when you only have an access token:** + +```ts +// authClient.getUserInfo() takes an options object: { accessToken, expectedSubject? } +const profile = await authClient.getUserInfo({ accessToken }); +// { sub, name, email, ... } +``` + +**After — raw fetch fallback:** + +```ts +const metadata = await authClient.getServerMetadata(); +const resp = await fetch(metadata.userinfo_endpoint, { + headers: { Authorization: `Bearer ${accessToken}` }, +}); +const profile = await resp.json(); +``` + +> Prefer reading `claims` over any `/userinfo` call: it avoids a network round-trip and the +> claims are already validated by the SDK. + +--- + +### Quick lookup table + +| node-auth0 | New SDK equivalent | Layer | +|---|---|---| +| `oauth.authorizationCodeGrant` | `authClient.getTokenByCode(url, opts)` (auth-js) or `serverClient.completeInteractiveLogin(url, storeOpts)` — see the Session section below (server-js) | auth-js / server-js | +| `oauth.authorizationCodeGrantWithPKCE` | `authClient.getTokenByCode(url, { codeVerifier })` | auth-js | +| `oauth.refreshTokenGrant` | `authClient.getTokenByRefreshToken({ refreshToken })` | auth-js | +| `oauth.passwordGrant` | `authClient.getTokenByPassword({ ... })` | auth-js | +| `oauth.clientCredentialsGrant` | `authClient.getTokenByClientCredentials({ audience })` | auth-js | +| `oauth.revokeRefreshToken` | `authClient.revokeToken({ token })` / `serverClient.revokeRefreshToken()` | auth-js / server-js | +| `oauth.tokenForConnection` | `authClient.exchangeToken({ connection, ... })` | auth-js | +| `oauth.pushedAuthorization` | `authClient.buildAuthorizationUrl({ pushedAuthorizationRequests: true })` | auth-js | +| `database.signUp` | `authClient.database.signUp({ ... })` | auth-js | +| `database.changePassword` | `authClient.database.changePassword({ ... })` | auth-js | +| `passwordless.sendEmail` | `authClient.passwordless.sendEmail({ ... })` | auth-js | +| `passwordless.sendSMS` | `authClient.passwordless.sendSms({ phoneNumber })` | auth-js | +| `passwordless.loginWithEmail` | `authClient.getTokenByPasswordlessEmail({ ... })` | auth-js | +| `passwordless.loginWithSMS` | `authClient.getTokenByPasswordlessSms({ ... })` | auth-js | +| `backchannel.authorize` | `authClient.initiateBackchannelAuthentication({ ... })` | auth-js | +| `backchannel.backchannelGrant` | `authClient.backchannelAuthenticationGrant({ authReqId })` | auth-js | +| `tokenExchange.exchangeToken` | `authClient.exchangeToken({ subjectTokenType, subjectToken, audience })` | auth-js | +| `UserInfoClient.getUserInfo` | `TokenResponse.claims` (preferred) / `authClient.getUserInfo({ accessToken })` / `serverClient.getUser()` / raw `/userinfo` fetch | auth-js / server-js | +| (no equivalent) — build `/authorize` URL | `authClient.buildAuthorizationUrl({ ... })` | auth-js | +| (no equivalent) — build `/v2/logout` URL | `authClient.buildLogoutUrl({ returnTo })` | auth-js | +| `ManagementClient.*` | **not migrated — stays on `auth0`** | — | + +--- + +## Section 7: Session layer (auth0-server-js only) + +**Skip this section** if your routing decision is `@auth0/auth0-auth-js`. + +This section applies only when the customer wants the SDK to own the login redirect flow, +session storage, cookies, token refresh, and logout. **This is a rewrite of the session +handling, not a method-for-method port.** node-auth0 had no session concept, so there is +nothing to translate line-for-line. Instead you *replace* the customer's existing session code +(their `express-session` wiring, their token cache, their refresh-on-expiry logic, their logout +handler) with the ServerClient lifecycle. Routes, views, and business logic stay put. + +--- + +### Mental model + +A ServerClient login has three durable pieces: + +1. **Transaction store** — short-lived. Holds the in-flight login: the OAuth `state` and the + PKCE `code_verifier` between the moment you redirect the user to Auth0 and the moment they + come back to your callback. Created at `startInteractiveLogin`, consumed at + `completeInteractiveLogin`. +2. **State store** — long-lived. Holds the established session: the user claims plus the access / + refresh / ID tokens and their absolute expiry. Read on every subsequent request via `getUser`, + `getSession`, `getAccessToken`. +3. **Cookies** — how the two stores key themselves to the browser. With a *stateless* store the + session data lives encrypted in the cookie itself; with a *stateful* store the cookie holds + only an identifier and the data lives in your backend (Redis, DB, etc.). + +node-auth0 exposed none of this; the customer built equivalents by hand. You are swapping their +implementation for the SDK's. + +--- + +### Store setup + +`@auth0/auth0-server-js` ships store base classes and cookie-backed implementations: + +- `CookieTransactionStore` — transaction store backed entirely by a cookie. Good default. +- `StatelessStateStore` — session lives encrypted in the cookie. No server-side storage; good + for serverless / horizontally-scaled deployments with small sessions. +- `StatefulStateStore` — session lives server-side; the cookie holds an id. Use for large + sessions or when you need server-side revocation. +- `AbstractStateStore` / `AbstractTransactionStore` — subclassable base classes. Extend + `AbstractStateStore` for a custom session/state store, or `AbstractTransactionStore` for a + custom transaction store. Alternatively, implement the `SessionStore` data-adapter interface + (`{ get(id), set(id,data), delete(id), deleteByLogoutToken(claims,opts?) }`) and pass it as + the backing `store` option to `StatefulStateStore`. Do NOT `extend SessionStore`. + +All stores accept a `CookieHandler` so they can integrate with any framework's cookie API. The +`storeOptions` generic (`TStoreOptions`) is how you thread per-request context (like the +framework `req`/`res`) into store reads/writes — every ServerClient method takes an optional +trailing `storeOptions` argument for exactly this. + +```ts +import { + ServerClient, + CookieTransactionStore, + StatelessStateStore, +} from '@auth0/auth0-server-js'; + +const serverClient = new ServerClient({ + domain: process.env.AUTH0_DOMAIN!, + clientId: process.env.AUTH0_CLIENT_ID!, + clientSecret: process.env.AUTH0_CLIENT_SECRET!, + authorizationParams: { + redirect_uri: 'https://app.example.com/callback', + scope: 'openid profile email offline_access', // offline_access => refresh token + audience: 'https://api.example.com', + }, + transactionStore: new CookieTransactionStore( + { secret: process.env.SESSION_SECRET! }, + cookieHandler // CookieHandler implementation + ), + stateStore: new StatelessStateStore( + { secret: process.env.SESSION_SECRET! }, + cookieHandler // CookieHandler implementation + ), +}); +``` + +--- + +### The redirect-login lifecycle + +#### 1. Start login — replace the hand-built `/authorize` redirect + +Whatever the customer did to send the user to Auth0 (a hand-constructed `/authorize` URL, or +`express-openid-connect`'s `/login`) becomes: + +```ts +// GET /login +app.get('/login', async (req, res) => { + const authorizationUrl = await serverClient.startInteractiveLogin( + { + authorizationParams: { /* optional per-login overrides */ }, + appState: { returnTo: req.query.returnTo || '/' }, // seed appState for round-trip + }, + { req, res }, // storeOptions — lets the transaction store write its cookie + ); + res.redirect(authorizationUrl.href); +}); +``` + +`startInteractiveLogin` generates `state` + PKCE, writes them to the transaction store, and +returns the fully-formed authorization URL. + +#### 2. Complete login — replace the manual code exchange + +The callback handler that used to call `oauth.authorizationCodeGrant` (or +`authorizationCodeGrantWithPKCE`) and then stuff tokens into the session becomes a single call: + +```ts +// GET /callback +app.get('/callback', async (req, res) => { + const callbackUrl = new URL(req.url, `https://${req.headers.host}`); + const { appState } = await serverClient.completeInteractiveLogin(callbackUrl, { req, res }); + // Session is now established in the state store. Tokens are NOT your concern anymore. + + // Validate returnTo before redirecting to prevent open-redirect attacks. + // Accept only safe relative paths: starts with "/" but not "//" (protocol-relative) + // and contains no URL scheme (http:, javascript:, data:, etc.). + // For absolute URLs, maintain an explicit allowlist of trusted origins instead. + const returnTo = appState?.returnTo; + const isSafeRelative = ( + typeof returnTo === 'string' && + returnTo.startsWith('/') && + !returnTo.includes('\\') && // reject backslash: browsers normalize \ → /, enabling bypass + !returnTo.startsWith('//') && + !/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(returnTo) // rejects any scheme prefix + ); + res.redirect(isSafeRelative ? returnTo : '/'); +}); +``` + +`completeInteractiveLogin` validates `state`, exchanges the code, validates the ID token, writes +the session (user + tokens + absolute expiry) to the state store, and clears the transaction. + +> **Open-redirect safety:** `appState.returnTo` originates from `req.query.returnTo` set in the +> login handler above. Although it round-trips through Auth0's opaque state parameter and cannot +> be injected mid-flight, the original query parameter comes from the browser and must be +> validated before passing to `res.redirect()`. The guard above covers the common case. For +> multi-origin apps, replace the `isSafeRelative` check with an explicit allowlist of permitted +> base URLs. + +#### 3. Read the user / session on later requests + +Replace `req.session.user` reads: + +```ts +const user = await serverClient.getUser({ req, res }); // user claims, or undefined +const session = await serverClient.getSession({ req, res }); // full session data, or undefined +``` + +`getUser` / `getSession` return `undefined` when there is no session or it has expired (the store +deletes expired sessions on read), so use that as your "not logged in" signal. + +#### 4. Get an access token to call an API — refresh is automatic + +Replace the customer's manual "is the token expired? if so refresh" block: + +```ts +const { accessToken } = await serverClient.getAccessToken({}, { req, res }); +// If the stored access token is expired and a refresh token exists, +// the SDK refreshes and persists the new tokens transparently. +``` + +The `expires_in` → `expiresAt` hazard from Section 4.3 disappears entirely here: the SDK owns +expiry math. Delete the customer's `Date.now() + expires_in * 1000` bookkeeping. + +For a downstream federated connection token (Token Vault), use +`serverClient.getAccessTokenForConnection({ connection }, { req, res })`. + +#### 5. Logout — replace manual revoke + session clear + `/v2/logout` redirect + +```ts +// GET /logout +app.get('/logout', async (req, res) => { + const logoutUrl = await serverClient.logout( + { returnTo: 'https://app.example.com' }, + { req, res } + ); + res.redirect(logoutUrl.href); +}); +``` + +`logout` clears the session from the state store AND automatically best-effort-revokes the +session refresh token before deletion. Do NOT call `serverClient.revokeRefreshToken({ req, res })` +on the logout path — the session is already gone by the time logout returns and the call will +throw `MissingSessionError`. Use `revokeRefreshToken` only for standalone revocation (revoking +a token without ending the session). + +--- + +### Non-redirect logins that also establish a session + +If the customer used node-auth0 for a non-redirect login (passwordless, CIBA, +custom token exchange) *and* wants a server-js session out of it, use the ServerClient methods +that both authenticate and write the session: + +| Flow | ServerClient method | +|---|---| +| Backchannel / CIBA | `loginBackchannel({ ... }, storeOptions)` | +| Passwordless (verify code → session) | `completePasswordless({ connection, email \| phoneNumber, verificationCode }, storeOptions)` | +| Passwordless magic link (callback → session) | `completePasswordlessMagicLink(url, storeOptions)` | +| Custom token exchange → session | `loginWithCustomTokenExchange({ ... }, storeOptions)` | +| MFA verify → session | `serverClient.mfa.verify({ ... }, storeOptions)` | + +Each of these performs the underlying grant *and* persists the resulting tokens to the state +store, so the user is logged in afterward. + +**Passwordless initiation** — `startPasswordless({ connection, email | phoneNumber, ... }, storeOptions)` sends the OTP or magic link. Only **email magic-link** mode (`send: 'link'`) stores a pending anti-forgery transaction with a generated `state`; SMS-OTP and email-OTP (`send: 'code'`) send the code and return immediately with no transaction stored. It does NOT exchange tokens or establish a session. Call `completePasswordless` or `completePasswordlessMagicLink` to finish the flow and create the session. + +**Password grant (ROPC)** — `ServerClient` has no password-grant session method. Migrations from `oauth.passwordGrant` stay on `@auth0/auth0-auth-js` (`authClient.getTokenByPassword`) and the app owns session handling. There is no server-js session bridge for ROPC. + +--- + +### Backchannel logout (OIDC back-channel logout) + +If the customer implemented an Auth0 back-channel logout endpoint by hand (validating the logout +token, then clearing their session store), replace it with: + +```ts +// POST /backchannel-logout +app.post('/backchannel-logout', async (req, res) => { + await serverClient.handleBackchannelLogout(req.body.logout_token, { req, res }); + res.sendStatus(204); +}); +``` + +It validates the logout token and clears the corresponding session. + +--- + +### What you did NOT change + +- Route definitions, controllers, templates, and business logic are untouched — you only swapped + the auth/session mechanism they call into. +- The Management API (`ManagementClient`) is not part of this and stays on the `auth0` package. + +--- + +## Section 8: Post-migration verification + +The migration is not complete until all verification steps pass. Run the following in sequence. +If **any** step fails, fix the reported issues and start the loop again from step 1. **Do not +declare the migration complete until the loop converges** — all steps pass in a single iteration. + +--- + +### Step 1: Residue check + +Run this residue scan against the customer's source root. It exits non-zero if any high-signal +residue is found, so it can gate CI. Replace `` with the source directory. + +```bash +set -uo pipefail +ROOT="${1:-}" + +if [ ! -d "$ROOT" ]; then + echo "error: '$ROOT' is not a directory" >&2 + echo "usage: bash " >&2 + exit 2 +fi + +FILE_GLOBS=(--include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \ + --include='*.mjs' --include='*.cjs' --include='*.cts' --include='*.mts') +EXCLUDES=(--exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build \ + --exclude-dir=.git --exclude-dir=coverage) +FAILED=0 + +check() { + local label="$1" regex="$2" hint="$3" + local hits + hits="$(grep -rEn "${FILE_GLOBS[@]}" "${EXCLUDES[@]}" "$regex" "$ROOT" 2>/dev/null || true)" + if [ -n "$hits" ]; then + printf '\n[FAIL] %s\n' "$label" + printf ' %s\n' "$hint" + printf '%s\n' "$hits" | sed 's/^/ /' + FAILED=1 + else + printf '[ ok ] %s\n' "$label" + fi +} + +echo "verify-migration — residue check" +echo "root: $ROOT" +echo + +check "No residual AuthenticationClient" \ + "new[[:space:]]+AuthenticationClient" \ + "Replace with AuthClient (@auth0/auth0-auth-js) or ServerClient (@auth0/auth0-server-js)." + +check "No residual UserInfoClient" \ + "UserInfoClient" \ + "Use TokenResponse.claims, serverClient.getUser(), or a direct /userinfo fetch." + +check "No residual node-auth0 auth sub-client calls" \ + "new[[:space:]]+AuthenticationClient|UserInfoClient|\.oauth\.(authorizationCodeGrant|authorizationCodeGrantWithPKCE|refreshTokenGrant|passwordGrant|clientCredentialsGrant|revokeRefreshToken|tokenForConnection|pushedAuthorization)|\.passwordless\.(loginWithEmail|loginWithSMS)|\.backchannel\.(authorize|backchannelGrant)|\.tokenExchange\.exchangeToken" \ + "Map each call using the API mapping table in the 'Method-by-method API mapping' section above." + +check "No '.data.' access on token/grant results" \ + "\.(data)\.(access_token|refresh_token|id_token|expires_in|token_type)" \ + "New SDKs return the domain object directly; read tokens.accessToken, not resp.data.access_token." + +check "No relative expires_in arithmetic" \ + "Date\.now\(\)[[:space:]]*\+[^;]*expires_in|expires_in[[:space:]]*\*[[:space:]]*1000" \ + "expiresAt is an ABSOLUTE Unix timestamp (seconds). Do not add Date.now(). See Section 4.3." +# NOTE: This grep catches the most common forms but cannot reliably detect all expiry math. +# For example, 'Date.now() + tokens.expiresAt * 1000' passes the grep but is wrong — +# expiresAt is already absolute Unix seconds, so no Date.now() addition is needed. +# Manually audit every expression that multiplies or adds to an expiry-related value. + +check "No AuthApiError catches" \ + "AuthApiError" \ + "Use the typed per-operation error (e.g. TokenByRefreshTokenError) and check e.cause.error." + +check "No 'mfa_required' string checks" \ + "['\"]mfa_required['\"]" \ + "Check e.cause?.error === 'mfa_required' on the typed per-operation error instead of string matching." + +echo +if [ "$FAILED" -ne 0 ]; then + echo "RESULT: residue found — resolve the [FAIL] items above, then re-run." + echo "After this passes, run the project's own type-check, lint, and tests." + exit 1 +fi +echo "RESULT: no residue detected. Now run the project's own type-check, lint, and tests." +``` + +--- + +### Step 2: Type-check (TypeScript projects only) + +Run the TypeScript compiler only when the project has TypeScript sources or a tsconfig. Do not +run `tsc` unconditionally on JavaScript-only projects — it will fail spuriously or pick up +unrelated errors. + +```bash +# Guard: only run tsc if the project is TypeScript +if find . -maxdepth 4 -name "tsconfig.json" -not -path "*/node_modules/*" | grep -q . \ + || find . -maxdepth 4 -name "*.ts" ! -name "*.d.ts" -not -path "*/node_modules/*" | grep -q .; then + echo "TypeScript project detected — running tsc --noEmit" + npx tsc --noEmit +else + echo "No TypeScript detected — skipping tsc check." +fi +``` + +--- + +### Step 3: Run the project's test suite + +```bash +npm test # or the project's test command (yarn test, pnpm test, jest, vitest, etc.) +``` + +--- + +### Step 4: Run the linter + +If the project has a linter configured: + +```bash +npm run lint # or the project's lint command +``` + +If all four steps pass in a single iteration, the migration is complete. diff --git a/main/.mintlify/skills/auth0/references/framework-react/index.md b/main/.mintlify/skills/auth0/references/framework-react/index.md index e3ce36de13..3f9fdb28a5 100644 --- a/main/.mintlify/skills/auth0/references/framework-react/index.md +++ b/main/.mintlify/skills/auth0/references/framework-react/index.md @@ -121,10 +121,6 @@ npm start # CRA | Missing Auth0Provider wrapper | Entire app must be wrapped in `` | | Provider not at root level | Auth0Provider must wrap all components that use auth hooks | | Wrong import path for env vars | Vite uses `import.meta.env.VITE_*`, CRA uses `process.env.REACT_APP_*` | -| Using `acr_values` redirect for in-app MFA | Use `useAuth0().mfa` API for in-app enrollment/challenge/verify flows | -| Not catching `MfaRequiredError` | Wrap `getAccessTokenSilently` in try/catch and check `instanceof MfaRequiredError` | -| Making direct HTTP calls to MFA endpoints | Use the `mfa` property from `useAuth0()` — it handles token management automatically | -| Forgetting refresh tokens for step-up MFA | Set `useRefreshTokens={true}` on Auth0Provider when using `interactiveErrorHandler="popup"` | ## Related Skills @@ -144,23 +140,13 @@ npm start # CRA - `loginWithRedirect()` - Initiate login - `logout()` - Log out user - `getAccessTokenSilently()` - Get access token for API calls -- `mfa` - MFA API client for enrollment, challenge, and verification - - `mfa.getAuthenticators(mfaToken)` - List enrolled authenticators - - `mfa.getEnrollmentFactors(mfaToken)` - Get available enrollment factors - - `mfa.enroll(params)` - Enroll new authenticator (OTP, SMS, Email, Voice, Push) - - `mfa.challenge(params)` - Initiate MFA challenge - - `mfa.verify(params)` - Verify MFA challenge and complete authentication - -**MFA Error Types (import from `@auth0/auth0-react`):** -- `MfaRequiredError` - Thrown by `getAccessTokenSilently` when MFA is needed (has `mfa_token` and `mfa_requirements`) -- `MfaEnrollmentError`, `MfaChallengeError`, `MfaVerifyError` - Thrown by respective `mfa.*` methods **Common Use Cases:** - Login/Logout buttons → See Step 4 above - Protected routes → see the Protected Routes section below - API calls with tokens → see the Calling APIs section below - Error handling → see the Error Handling section below -- MFA handling → see the MFA Handling section below +- MFA / step-up → ask for MFA (feature:mfa) ## References @@ -209,9 +195,6 @@ import { Auth0Provider } from '@auth0/auth0-react'; useRefreshTokensFallback={false} // Fall back to iframe if refresh token exchange fails (default: false) useMrrt={false} // Enable Multi-Refresh-Token for multi-tenant apps (default: false) - // MFA / Step-up - interactiveErrorHandler="popup" // Automatically handle MFA via popup (requires useRefreshTokens) - // Advanced options skipRedirectCallback={false} // Skip automatic callback handling context={Auth0Context} // Custom React context @@ -242,7 +225,6 @@ import { Auth0Provider } from '@auth0/auth0-react'; | `useMrrt` | boolean | `false` | Enable Multi-Refresh-Token support for multi-tenant apps. Requires `useRefreshTokens` and `useRefreshTokensFallback` to be `true` | | `workerUrl` | string | - | Custom worker script URL for token calls. Useful for CSP compliance when using `useRefreshTokens: true` with `cacheLocation: 'memory'` | | `context` | React.Context | - | Custom React context for nested Auth0Providers. Allows multiple Auth0Providers in same app | -| `interactiveErrorHandler` | `'popup'` | - | Automatically handle MFA via popup when `getAccessTokenSilently` encounters `mfa_required`. Requires `useRefreshTokens={true}` | | `skipRedirectCallback` | boolean | `false` | Skip automatic callback handling | | `onRedirectCallback` | function | - | Callback after successful login | @@ -283,9 +265,6 @@ const { getAccessTokenWithPopup, getIdTokenClaims, handleRedirectCallback, - - // MFA API - mfa, } = useAuth0(); ``` @@ -542,95 +521,6 @@ interface RedirectLoginResult { } ``` -#### mfa - -The `mfa` property provides access to the MFA API client for in-app Multi-Factor Authentication flows. - -**Methods:** - -| Method | Description | -|--------|-------------| -| `mfa.getAuthenticators(mfaToken)` | List enrolled authenticators for the user | -| `mfa.getEnrollmentFactors(mfaToken)` | Get available enrollment factors (when user needs to enroll) | -| `mfa.enroll(params)` | Enroll a new authenticator (OTP, SMS, Email, Voice, Push) | -| `mfa.challenge(params)` | Initiate an MFA challenge for an enrolled authenticator | -| `mfa.verify(params)` | Verify an MFA challenge and complete authentication | - -**Enroll params:** - -```typescript -// OTP enrollment -await mfa.enroll({ mfaToken, factorType: 'otp' }); -// Returns: { barcodeUri, recoveryCodes, ... } - -// SMS enrollment -await mfa.enroll({ mfaToken, factorType: 'sms', phoneNumber: '+12025551234' }); - -// Email enrollment -await mfa.enroll({ mfaToken, factorType: 'email', email: 'user@example.com' }); - -// Voice enrollment -await mfa.enroll({ mfaToken, factorType: 'voice', phoneNumber: '+12025551234' }); - -// Push enrollment -await mfa.enroll({ mfaToken, factorType: 'push' }); -``` - -**Challenge params:** - -```typescript -// OTP challenge (optional — code is already in authenticator app) -await mfa.challenge({ mfaToken, challengeType: 'otp', authenticatorId }); - -// SMS/Voice/Email/Push challenge (required — sends code to user) -await mfa.challenge({ mfaToken, challengeType: 'oob', authenticatorId }); -// Returns: { oobCode } -``` - -**Verify params:** - -```typescript -// Verify with OTP code -const tokens = await mfa.verify({ mfaToken, otp: '123456' }); - -// Verify with OOB code (SMS/Voice/Email) -const tokens = await mfa.verify({ mfaToken, oobCode, bindingCode: '123456' }); - -// Verify with recovery code -const tokens = await mfa.verify({ mfaToken, recoveryCode: 'recovery-code-here' }); -``` - ---- - -### MFA Error Types - -All MFA error types are importable from `@auth0/auth0-react`. - -| Error | When thrown | Key properties | -|-------|-----------|----------------| -| `MfaRequiredError` | `getAccessTokenSilently()` encounters an MFA requirement | `mfa_token`, `mfa_requirements` | -| `MfaEnrollmentError` | `mfa.enroll()` fails | `error_description` | -| `MfaChallengeError` | `mfa.challenge()` fails | `error_description` | -| `MfaVerifyError` | `mfa.verify()` fails (e.g., invalid OTP code) | `error_description` | -| `MfaListAuthenticatorsError` | `mfa.getAuthenticators()` fails | `error_description` | -| `MfaEnrollmentFactorsError` | `mfa.getEnrollmentFactors()` fails | `error_description` | - -**MfaRequiredError properties:** -- `mfa_token` — Token used for all subsequent MFA operations -- `mfa_requirements.enroll` — Array of factor types the user can enroll in (present when user needs to set up MFA) -- `mfa_requirements.challenge` — Array of factor types the user can challenge (present when user has enrolled authenticators) - -**Import:** - -```typescript -import { - MfaRequiredError, - MfaEnrollmentError, - MfaChallengeError, - MfaVerifyError, -} from '@auth0/auth0-react'; -``` - --- ## Custom Hooks @@ -748,20 +638,6 @@ import type { PopupLoginOptions, LogoutOptions, GetTokenSilentlyOptions, - MfaApiClient, - Authenticator, - EnrollParams, - ChallengeResponse, - VerifyParams, - EnrollmentFactor, -} from '@auth0/auth0-react'; - -// MFA error types (value imports, not type-only) -import { - MfaRequiredError, - MfaEnrollmentError, - MfaChallengeError, - MfaVerifyError, } from '@auth0/auth0-react'; ``` @@ -1002,193 +878,6 @@ export function App() { --- -## MFA Handling - -The `@auth0/auth0-react` SDK provides a built-in MFA API for handling Multi-Factor Authentication entirely within your app — no redirects to Universal Login required. Access it via the `mfa` property from `useAuth0()`. - -> **Note:** MFA support via SDKs is currently in Early Access. For a simpler approach that uses Universal Login to handle MFA automatically (no custom UI), see the [Step-Up via Popup](#step-up-via-popup-simpler-approach) section below. - -### Catching MfaRequiredError - -When `getAccessTokenSilently()` encounters an MFA requirement, it throws `MfaRequiredError`. Catch it and inspect `mfa_requirements` to determine the flow: - -```tsx -import { useAuth0, MfaRequiredError } from '@auth0/auth0-react'; -import { useState } from 'react'; - -export function ProtectedApiCall() { - const { getAccessTokenSilently, mfa } = useAuth0(); - const [mfaToken, setMfaToken] = useState(null); - const [error, setError] = useState(null); - - const callApi = async () => { - try { - const token = await getAccessTokenSilently(); - // Use token to call API... - } catch (err) { - if (err instanceof MfaRequiredError) { - setMfaToken(err.mfa_token); - - // Check if enrollment or challenge is needed - const factors = await mfa.getEnrollmentFactors(err.mfa_token); - if (factors.length > 0) { - // User needs to enroll — show enrollment UI - } else { - // User has authenticators — show challenge UI - const authenticators = await mfa.getAuthenticators(err.mfa_token); - // Let user pick authenticator and proceed with challenge - } - } else { - setError(err.message); - } - } - }; - - return ; -} -``` - -### OTP Enrollment - -When the user needs to set up MFA for the first time: - -```tsx -import { useAuth0, MfaEnrollmentError } from '@auth0/auth0-react'; -import { useState } from 'react'; - -export function OtpEnrollment({ mfaToken }: { mfaToken: string }) { - const { mfa } = useAuth0(); - const [barcodeUri, setBarcodeUri] = useState(null); - const [recoveryCodes, setRecoveryCodes] = useState(null); - const [error, setError] = useState(null); - - const startEnrollment = async () => { - try { - const enrollment = await mfa.enroll({ mfaToken, factorType: 'otp' }); - setBarcodeUri(enrollment.barcodeUri); - setRecoveryCodes(enrollment.recoveryCodes); - } catch (err) { - if (err instanceof MfaEnrollmentError) { - setError(err.error_description); - } - } - }; - - return ( -
- - {barcodeUri && ( -
-

Scan this QR code with your authenticator app:

- {/* Render barcodeUri as QR code using a library like qrcode.react */} - {barcodeUri} -
- )} - {recoveryCodes && ( -
-

Save these recovery codes:

-
    - {recoveryCodes.map((code, i) =>
  • {code}
  • )} -
-
- )} -
- ); -} -``` - -### Challenge and Verify - -When the user already has enrolled authenticators: - -```tsx -import { - useAuth0, - MfaChallengeError, - MfaVerifyError, -} from '@auth0/auth0-react'; -import { useState } from 'react'; - -export function MfaChallenge({ mfaToken }: { mfaToken: string }) { - const { mfa } = useAuth0(); - const [otp, setOtp] = useState(''); - const [error, setError] = useState(null); - - const handleVerify = async () => { - try { - // For OTP authenticators, you can skip challenge() and go straight to verify() - const tokens = await mfa.verify({ mfaToken, otp }); - // User is now authenticated — tokens are cached by the SDK - // Access token available at tokens.access_token - } catch (err) { - if (err instanceof MfaVerifyError) { - setError('Invalid code. Please try again.'); - } else if (err instanceof MfaChallengeError) { - setError('Challenge failed: ' + err.error_description); - } - } - }; - - return ( -
-

Enter your verification code

- setOtp(e.target.value)} - placeholder="6-digit code" - maxLength={6} - /> - - {error &&

{error}

} -
- ); -} -``` - -### SMS/Email Challenge (Out-of-Band) - -For SMS, Email, Voice, or Push authenticators, you must call `challenge()` first to send the code: - -```tsx -// Initiate challenge to send code via SMS/Email -const response = await mfa.challenge({ - mfaToken, - challengeType: 'oob', - authenticatorId: authenticator.id, -}); - -// Verify with the OOB code and the binding code the user received -const tokens = await mfa.verify({ - mfaToken, - oobCode: response.oobCode, - bindingCode: userEnteredCode, -}); -``` - -### Step-Up via Popup (Simpler Approach) - -If you don't need a custom MFA UI, configure `interactiveErrorHandler` to let the SDK handle MFA automatically via a Universal Login popup: - -```tsx - - - -``` - -With this setup, `getAccessTokenSilently()` automatically opens a popup when MFA is required. No error handling needed — the token is returned after the user completes MFA in the popup. - ---- - ## Security Considerations ### Client-Side Security diff --git a/main/.mintlify/skills/auth0/references/framework-server-python/index.md b/main/.mintlify/skills/auth0/references/framework-server-python/index.md new file mode 100644 index 0000000000..ae537c9efa --- /dev/null +++ b/main/.mintlify/skills/auth0/references/framework-server-python/index.md @@ -0,0 +1,700 @@ + +# Auth0 Server Python SDK (`auth0-server-python`) + +The framework-agnostic core SDK for adding OpenID Connect authentication to any +Python **server-side** application. It drives the Authorization Code flow (with +PKCE), token storage and refresh, sessions, logout, MFA, passkeys, account +linking, connected accounts, and RFC 8693 token exchange - independent of Flask, +FastAPI, Django, or any web framework. + +This reference documents the SDK's **public API surface** (the `ServerClient` +class and its option/return types) so it can be loaded alongside a feature +reference for the exact method, signature, and options a task needs. For a +turnkey Flask integration (with ready-made session stores), load +`framework-flask/index.md` instead - it wraps this same SDK. + +## When to use this reference + +- Integrating `auth0-server-python` into a web framework that has **no dedicated + reference** (FastAPI web app with login/logout UI, Django, Sanic, Starlette, + Quart, aiohttp, or a custom async server). +- Looking up an exact `ServerClient` method signature, option type, or return + shape referenced from a feature guide (MFA, Organizations, passkeys, token + exchange, connected accounts). +- Implementing a **custom state/transaction store** (Redis, database, cookie). + +## When NOT to use it + +| Instead of this SDK | Use | Why | +|---|---|---| +| Flask web app with login/logout | `framework-flask/index.md` | Ships concrete Flask session stores. | +| Protecting a FastAPI API (validating incoming JWT Bearer tokens) | `framework-fastapi-api/index.md` (`auth0-fastapi-api`) | That is a resource server. This SDK **acquires** tokens and manages sessions; it does not validate them on an API. | +| Single Page App / mobile | The relevant client-side framework reference (React, Vue, Angular, Swift, Android, ...) | Different auth model. | + +## Install + +```bash +pip install auth0-server-python +``` + +The SDK is fully **async** - every I/O method is a coroutine and must be +`await`ed. In frameworks that support async request handlers this is native; in +sync frameworks (e.g. Flask without the `[async]` extra) you must enable async +support or bridge with `asyncio.run(...)`. + +--- + +## Core model: the two stores + +`ServerClient` never touches your framework's request/response directly. It reads +and writes all persistent data through two pluggable stores you provide: + +| Store | Holds | Lifetime | +|-------|-------|----------| +| **transaction_store** | Short-lived per-login data: PKCE `code_verifier`, `state`, `nonce`, `redirect_uri`, `app_state`. Written by `start_interactive_login`, read/cleared by `complete_interactive_login`. | One login round-trip | +| **state_store** | The authenticated session: `UserClaims`, ID token, refresh token, and `TokenSet`s. | The user's session | + +Both extend `AbstractDataStore` and inherit `encrypt()`/`decrypt()` helpers that +JWE-encrypt data with the `secret` you pass. **If you do not pass stores, the SDK +falls back to in-memory stores** (`MemoryTransactionStore` / +`MemoryStateStore`) - fine for a single-process demo, but data is lost on restart +and not shared across workers. Production apps supply real stores (cookie, Redis, +database). + +### Store interface + +```python +from typing import Any, Optional +from auth0_server_python.store import StateStore, TransactionStore +from auth0_server_python.auth_types import StateData, TransactionData + +class MyStateStore(StateStore): + def __init__(self, secret: str): + super().__init__({"secret": secret}) # enables self.encrypt / self.decrypt + + async def set(self, identifier: str, state, remove_if_expires: bool = False, + options: Optional[dict[str, Any]] = None) -> None: + data = state.dict() if hasattr(state, "dict") else state + # persist self.encrypt(identifier, data) under `identifier`, using `options` + ... + + async def get(self, identifier: str, + options: Optional[dict[str, Any]] = None): + raw = ... # load the encrypted blob you stored, or return None + if raw is None: + return None + decrypted = self.decrypt(identifier, raw) + # The SDK expects a StateData instance, not a bare dict + return StateData(**decrypted) if isinstance(decrypted, dict) else decrypted + + async def delete(self, identifier: str, + options: Optional[dict[str, Any]] = None) -> None: + ... + + async def delete_by_logout_token(self, claims: dict[str, Any], + options: Optional[dict[str, Any]] = None) -> None: + # Backchannel logout: find & delete sessions matching claims["sub"]/["sid"]. + # No-op is acceptable for stateless cookie stores (nothing to query). + ... +``` + +`TransactionStore` has the same `set`/`get`/`delete` contract (return +`TransactionData` from `get`) but **no** `delete_by_logout_token`. + +### `store_options` - how per-request context reaches the store + +Many `ServerClient` methods accept a `store_options` dict as their **last** +argument. The SDK passes it straight through to your store's `set`/`get`/`delete` +methods. Use it to hand the current request/response to a store that reads or +writes cookies: + +```python +store_options = {"request": request, "response": response} +await auth0.complete_interactive_login(str(request.url), store_options=store_options) +user = await auth0.get_user(store_options=store_options) +``` + +If your store uses a globally-available session (e.g. Flask's context-local +`session`), it does not need anything from `store_options` and you can omit it. + +--- + +## Constructing the client + +Create **one** `ServerClient` and reuse it. Never hardcode credentials. + +```python +import os +from auth0_server_python.auth_server.server_client import ServerClient + +auth0 = ServerClient( + domain=os.environ["AUTH0_DOMAIN"], # "tenant.us.auth0.com" (no https://) + client_id=os.environ["AUTH0_CLIENT_ID"], + client_secret=os.environ["AUTH0_CLIENT_SECRET"], + secret=os.environ["AUTH0_SECRET"], # encryption key: openssl rand -hex 64 + redirect_uri=os.environ["AUTH0_REDIRECT_URI"],# "https://app.example.com/callback" + state_store=MyStateStore(secret=os.environ["AUTH0_SECRET"]), + transaction_store=MyTransactionStore(secret=os.environ["AUTH0_SECRET"]), + authorization_params={"scope": "openid profile email"}, +) +``` + +### Constructor parameters + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `domain` | Yes | Tenant domain **or** a `Callable[[DomainResolverContext], str]` for multi-custom-domain (MCD) mode. No `https://`. | +| `client_id` | Yes | Application Client ID. | +| `client_secret` | One of these two | Client secret for `client_secret_post` auth. | +| `client_assertion_signing_key` | One of these two | PKCS8 PEM private key for **private_key_jwt** auth (more secure). Pairs with `client_assertion_signing_alg` (default `RS256`). | +| `secret` | Yes | Encryption secret for JWE-protecting stored data. Generate with `openssl rand -hex 64`. | +| `redirect_uri` | Recommended | Default callback URL. May also be set inside `authorization_params`. | +| `state_store` | Recommended | Session persistence. Defaults to in-memory if omitted. | +| `transaction_store` | Recommended | Login-transaction persistence. Defaults to in-memory if omitted. | +| `authorization_params` | Recommended | Default OAuth params: `scope`, `audience`, `connection`, ... Merged into every authorize request. | +| `state_identifier` | No | Key used for the session record (default `_a0_session`). | +| `transaction_identifier` | No | Key used for the transaction record (default `_a0_tx`). | +| `pushed_authorization_requests` | No | `True` to use Pushed Authorization Requests (PAR). | +| `organization` | No | Default org ID (`org_...`) or name applied to all logins. Per-login options override it. | +| `mfa_token_ttl` | No | Seconds an encrypted MFA token stays valid (default 300). | + +To call an API you must set `audience` (and add `offline_access` to `scope` to +receive a refresh token): + +```python +authorization_params={ + "scope": "openid profile email offline_access", + "audience": "https://api.example.com", +} +``` + +--- + +## The core login flow + +Three endpoints implement browser login. Wire each to a route in your framework. + +### `start_interactive_login` + +```python +async def start_interactive_login( + self, + options: Optional[StartInteractiveLoginOptions] = None, + store_options: dict = None, +) -> str +``` + +Builds the Authorization Code + PKCE request, persists the transaction, and +returns the **Universal Login URL as a string**. You must redirect to it. + +```python +url = await auth0.start_interactive_login() +return redirect(url) # framework-specific redirect +``` + +With per-login options (connection, signup hint, organization, invitation, +extra params, or `app_state` to round-trip through the flow): + +```python +from auth0_server_python.auth_types import StartInteractiveLoginOptions + +url = await auth0.start_interactive_login( + options=StartInteractiveLoginOptions( + authorization_params={"connection": "google-oauth2", "screen_hint": "signup"}, + app_state={"return_to": "/dashboard"}, + ), +) +``` + +### `complete_interactive_login` + +```python +async def complete_interactive_login( + self, + url: str, + store_options: dict = None, +) -> dict[str, Any] +``` + +Call from your callback route with the **full callback URL** (including the +`?code=...&state=...` query string). It validates `state` (CSRF), exchanges the +code for tokens, and writes the session to the state store. Returns a dict that +includes `state_data` and any `app_state` you passed in. **Raises** on state +mismatch, missing params, or token-exchange failure - always wrap it. + +```python +try: + result = await auth0.complete_interactive_login(str(request.url)) + # result.get("app_state") -> {"return_to": "/dashboard"} + return redirect("/") +except Exception as e: + return f"Authentication error: {e}", 400 +``` + +--- + +## Reading the session + +### `get_user` + +```python +async def get_user(self, store_options=None) -> Optional[dict[str, Any]] +``` + +Returns the user profile dict from the session, or `None` if unauthenticated. +This is your route-protection primitive: + +```python +user = await auth0.get_user() +if user is None: + return redirect("/login") +# user["sub"], user["name"], user["email"], user["picture"], user["email_verified"] +``` + +### `get_session` + +```python +async def get_session(self, store_options=None) -> Optional[dict[str, Any]] +``` + +Returns the full session (`user`, `id_token`, `refresh_token`, `token_sets`, +`connection_token_sets`), or `None`. Use when you need more than the profile. + +### `get_access_token` + +```python +async def get_access_token( + self, + store_options=None, + audience: Optional[str] = None, + scope: Optional[str] = None, +) -> str +``` + +Returns a valid access token for calling your API, **transparently refreshing** +it via the refresh token when expired. Requires an `audience` (set on the client +or passed here) and, for silent refresh, `offline_access` in scope. **Raises** +`AccessTokenError` (e.g. code `session_expired`) when no valid token can be +produced. + +```python +token = await auth0.get_access_token() +async with httpx.AsyncClient() as client: + resp = await client.get( + "https://api.example.com/data", + headers={"Authorization": f"Bearer {token}"}, + ) +``` + +### `logout` + +```python +async def logout( + self, + options: Optional[LogoutOptions] = None, + store_options=None, +) -> str +``` + +Clears the session from the state store and returns the Auth0 `/v2/logout` URL. +Redirect to it. Pass `LogoutOptions(return_to=...)` for the post-logout URL +(which must be in the app's Allowed Logout URLs). Note `options` is the **first** +positional argument - pass store context by keyword (`store_options=...`). + +```python +from auth0_server_python.auth_types import LogoutOptions + +url = await auth0.logout(options=LogoutOptions(return_to="https://app.example.com")) +return redirect(url) +``` + +### `handle_backchannel_logout` + +```python +async def handle_backchannel_logout(self, logout_token: str, store_options=None) +``` + +Validates an OIDC back-channel `logout_token` sent by Auth0 and invokes your +state store's `delete_by_logout_token(claims, ...)` to revoke matching sessions. +Wire it to a POST endpoint (e.g. `/backchannel-logout`) that reads the +`logout_token` form field. Only effective with a **server-side** state store that +can query by `sub`/`sid`; a stateless cookie store has nothing to delete. + +--- + +## Calling third-party / connection APIs (Token Vault) + +### `get_access_token_for_connection` + +```python +async def get_access_token_for_connection( + self, + options: dict[str, Any], # {"connection": "google-oauth2", "login_hint": "..."} + store_options=None, +) -> str +``` + +Returns an access token **for an upstream IdP connection** (e.g. Google, GitHub) +via Token Vault, so your server can call that provider's API on the user's +behalf. Requires the connection to be configured for Token Vault in Auth0. + +--- + +## Advanced flows (referenced from feature guides) + +These are the SDK's specialized public methods. Load the matching feature +reference for the end-to-end workflow; the signatures below are the SDK contract. + +### RFC 8693 custom token exchange + +```python +from auth0_server_python.auth_types import ( + CustomTokenExchangeOptions, LoginWithCustomTokenExchangeOptions, +) + +# Exchange an external token for Auth0 tokens (no session) +resp = await auth0.custom_token_exchange( + CustomTokenExchangeOptions( + subject_token="external-token", + subject_token_type="urn:acme:legacy-token", # your registered type + audience="https://api.example.com", # optional + scope="read:data write:data", # optional + ) +) +# resp.access_token / resp.expires_in are now available - use the token to call +# your API. Never log or print access tokens. + +# Exchange AND establish a user session in one call +result = await auth0.login_with_custom_token_exchange( + LoginWithCustomTokenExchangeOptions( + subject_token="external-token", + subject_token_type="urn:acme:legacy-token", + audience="https://api.example.com", + ), + store_options={"request": request, "response": response}, +) +user = result.state_data.user # state_data is a StateData model - attribute access +``` + +Signatures: + +```python +async def custom_token_exchange(self, options: CustomTokenExchangeOptions, + store_options=None) -> TokenExchangeResponse +async def login_with_custom_token_exchange(self, options: LoginWithCustomTokenExchangeOptions, + store_options=None) -> LoginWithCustomTokenExchangeResult +``` + +### Other SDK flows + +Every method below takes a trailing `store_options=None`; `start_*` methods +return a redirect URL (`str`) you complete in a return route with the callback +URL, mirroring the interactive-login pattern. + +| Flow | Methods (async unless noted) | Returns / notes | +|---|---|---| +| Account linking | `start_link_user(options)` · `complete_link_user(url)` · `start_unlink_user(options)` · `complete_unlink_user(url)` | `start_*` -> redirect URL. | +| Connected accounts (My Account API) | `start_connect_account(options: ConnectAccountOptions)` · `complete_connect_account(...)` · `list_connected_accounts()` · `delete_connected_account(...)` · `list_connected_account_connections(...)` | `start_*` -> `str`; others -> `CompleteConnectAccountResponse` / `ListConnectedAccountsResponse` / `None` / `ListConnectedAccountConnectionsResponse`. | +| CIBA (client-initiated backchannel auth) | `login_backchannel(options: dict) -> dict` | Approves login out-of-band (e.g. push to a phone), no browser redirect. | +| Session transfer (web-to-native handoff) | `request_session_transfer_token() -> SessionTransferTokenResult` · `build_session_transfer_redirect(...) -> str` **(sync)** | Hands a web session to a native app. | +| Passkeys | `passkey_signup_challenge(...)` · `passkey_login_challenge(...)` · `signin_with_passkey(...)` | -> `PasskeySignupChallengeResponse` / `PasskeyLoginChallengeResponse` / `PasskeyLoginResult`. | + +### Multi-factor authentication (`auth0.mfa`) + +MFA is exposed as an `MfaClient` on the `auth0.mfa` property. It is driven by an +**`mfa_token`**, which the SDK hands you when a login or token request needs a +second factor: any call (e.g. `get_access_token`, `complete_interactive_login`, +`login_with_custom_token_exchange`) that hits an `mfa_required` response raises +`MfaRequiredError`. Catch it, read `err.mfa_token`, and pass that token into the +`auth0.mfa` methods. The token is encrypted; the SDK decrypts it internally on +each call, so pass it through unchanged. + +```python +from auth0_server_python.error import MfaRequiredError + +try: + token = await auth0.get_access_token(audience="https://api.example.com") +except MfaRequiredError as err: + mfa_token = err.mfa_token # opaque, encrypted; feed into auth0.mfa.* + # err.mfa_requirements describes which factors satisfy the challenge + ... # run the enroll/challenge/verify flow below +``` + +Every `MfaClient` method takes an `options` **dict** whose keys are listed below, +plus the same optional `store_options` passthrough as the rest of the SDK. All +are async except `decrypt_mfa_token`. + +| Method | Returns | Purpose | +|---|---|---| +| `list_authenticators(options)` | `list[AuthenticatorResponse]` | List enrolled factors. | +| `enroll_authenticator(options)` | `EnrollmentResponse` | Register a new factor. | +| `challenge_authenticator(options)` | `ChallengeResponse` | Send/prepare a challenge. | +| `verify(options, dpop_key=None)` | `MfaVerifyResponse` | Complete the challenge, get tokens. | +| `decrypt_mfa_token(encrypted_token)` **(sync)** | `MfaTokenContext` | Inspect the token. | + +The typical flow is list -> enroll (if none) -> challenge -> verify: + +```python +# 1. What's enrolled? each -> AuthenticatorResponse: id, authenticator_type, +# active, name, oob_channel, type, phone_number, created_at, last_auth +authenticators = await auth0.mfa.list_authenticators({"mfa_token": mfa_token}) + +# 2. Enroll if needed. factor_type: "otp" | "sms" | "voice" | "email" | "auth0" +# (Guardian push). Add "phone_number" for sms/voice, "email" for email. +enroll = await auth0.mfa.enroll_authenticator({"mfa_token": mfa_token, "factor_type": "otp"}) +# otp -> OtpEnrollmentResponse: secret, barcode_uri (render as a QR code), +# recovery_codes, id +# oob -> OobEnrollmentResponse: oob_channel, oob_code, binding_method, +# recovery_codes, id + +# 3. Challenge. For OOB factors this delivers the code/push; for otp it prepares it. +challenge = await auth0.mfa.challenge_authenticator({ + "mfa_token": mfa_token, "factor_type": "sms", + "authenticator_id": authenticators[0].id, # optional, targets one enrolled factor +}) +# -> ChallengeResponse: challenge_type, oob_code, binding_method, expires_in + +# 4. Verify with EXACTLY ONE credential: "otp", or "oob_code" + "binding_code", +# or "recovery_code". persist=True (needs "audience") stores the token set as +# a session. +result = await auth0.mfa.verify({ + "mfa_token": mfa_token, + "otp": "123456", + "persist": True, + "audience": "https://api.example.com", # required when persist=True + "scope": "openid profile email", # optional +}) +# -> MfaVerifyResponse: access_token, token_type, expires_in, id_token, +# refresh_token, scope, audience, recovery_code +# Recovery fallback: auth0.mfa.verify({"mfa_token": mfa_token, "recovery_code": "ABCD-1234"}) +``` + +`decrypt_mfa_token` returns `MfaTokenContext(mfa_token, audience, scope, +mfa_requirements, created_at)`; raises `MfaTokenExpiredError` past `mfa_token_ttl` +(default 300s) or `MfaTokenInvalidError` if tampered. You rarely call it directly +- the client methods decrypt internally. + +**MFA errors** (from `auth0_server_python.error`): `MfaRequiredError` (the +trigger, subclass of `AccessTokenError`), `MfaEnrollmentError`, +`MfaChallengeError`, `MfaVerifyError`, `MfaListAuthenticatorsError` (all subclass +`MfaApiError`), and `MfaTokenExpiredError` / `MfaTokenInvalidError`. + +For enabling MFA policies on the tenant (which factors are required, when to +prompt), load `feature-mfa/index.md`; the SDK-side surface is fully above. + +### Passwordless (`auth0.passwordless`) + +Exposed as a `PasswordlessClient` for email/SMS one-time-code and magic-link +login. Use `StartPasswordlessEmailOptions` / `StartPasswordlessSmsOptions` to +start and `VerifyPasswordlessOtpOptions` to verify, following the same +options-dict + `store_options` pattern as `auth0.mfa`. + +--- + +## Option and return types (`auth0_server_python.auth_types`) + +### Input options + +```python +class StartInteractiveLoginOptions(BaseModel): + pushed_authorization_requests: Optional[bool] = False + app_state: Optional[Any] = None # round-tripped to complete_interactive_login + authorization_params: Optional[dict[str, Any]] = None + organization: Optional[str] = None # overrides client-level organization + invitation: Optional[str] = None # Organizations invitation token + +class LogoutOptions(BaseModel): + return_to: Optional[str] = None # post-logout redirect (must be allow-listed) + +class CustomTokenExchangeOptions(BaseModel): # login_with_* has the same fields + subject_token: str + subject_token_type: str + audience: Optional[str] = None + scope: Optional[str] = None + actor_token: Optional[str] = None + actor_token_type: Optional[str] = None # required if actor_token is set + organization: Optional[str] = None + authorization_params: Optional[dict[str, Any]] = None +``` + +### Session / return types + +```python +class UserClaims(BaseModel): # extra claims allowed (Config.extra = "allow") + sub: str + name / nickname / given_name / family_name / picture / email: Optional[str] + email_verified: Optional[bool] + org_id / org_name: Optional[str] + session_expiry: Optional[int] + +class TokenSet(BaseModel): + audience: str + access_token: str + scope: Optional[str] + expires_at: int + +class SessionData(BaseModel): + user: Optional[UserClaims] + id_token: Optional[str] + refresh_token: Optional[str] + token_sets: list[TokenSet] + connection_token_sets: list[ConnectionTokenSet] + +class StateData(SessionData): # what your StateStore persists / returns + internal: InternalStateData # SDK-managed (sid, timestamps) + +class TransactionData(BaseModel): # what your TransactionStore persists / returns + code_verifier / state / nonce / redirect_uri: Optional[...] + audience / organization: Optional[str] + app_state: Optional[Any] +``` + +Your store's `get` must return a `StateData` / `TransactionData` instance (not a +plain dict) - construct it with `StateData(**decrypted)` after decrypting. + +--- + +## Errors (`auth0_server_python.error`) + +All inherit `Auth0Error`. Catch the base to handle any SDK failure; catch a +subclass for specific handling. + +| Error | When | +|-------|------| +| `AccessTokenError` | `get_access_token` cannot produce a token (e.g. code `session_expired`). | +| `MfaRequiredError` | A second factor is required (code `mfa_required`). Subclass of `AccessTokenError`; carries `.mfa_token` and `.mfa_requirements` for the `auth0.mfa` flow. | +| `MissingTransactionError` | Callback arrived with no matching stored transaction (expired, cookie lost, or replay). | +| `MissingRequiredArgumentError` | A required constructor/method argument is absent (e.g. `secret`). | +| `ConfigurationError` | Invalid config (missing/invalid `domain`, bad `mfa_token_ttl`). | +| `ApiError` / `MyAccountApiError` | Auth0 endpoint returned an error (carries `error`, `error_description`). | +| `BackchannelLogoutError` | Invalid/failed back-channel `logout_token`. | +| `DomainResolverError` | MCD `domain` callable raised or returned nothing. | + +```python +from auth0_server_python.error import AccessTokenError, MissingTransactionError + +try: + token = await auth0.get_access_token() +except AccessTokenError as e: + return redirect("/login") # session expired, re-authenticate +``` + +--- + +## Integrating with any framework (generic recipe) + +1. **Implement the two stores** for your framework's session mechanism (cookie, + Redis, DB). Return `StateData` / `TransactionData` from `get`. +2. **Construct one `ServerClient`** at startup with those stores and your + credentials from environment variables. +3. **Add four async routes** and pass `store_options={"request": ..., "response": + ...}` when your stores need request context: + - `GET /login` -> redirect to `await start_interactive_login()` + - `GET /callback` -> `await complete_interactive_login(str(full_url))`, then redirect + - `GET /profile` (or any protected route) -> gate on `await get_user()` + - `GET /logout` -> redirect to `await logout(...)` +4. **Configure the Auth0 Application** (Regular Web App): add the callback URL to + Allowed Callback URLs and the post-logout URL to Allowed Logout URLs. Use the + Auth0 CLI or MCP - load the tooling reference for exact commands. + +FastAPI example (login + callback): + +```python +from fastapi import FastAPI, Request +from starlette.responses import RedirectResponse + +app = FastAPI() + +@app.get("/login") +async def login(): + return RedirectResponse(await auth0.start_interactive_login()) + +@app.get("/callback") +async def callback(request: Request): + await auth0.complete_interactive_login(str(request.url)) + return RedirectResponse("/") + +@app.get("/profile") +async def profile(): + user = await auth0.get_user() + if user is None: + return RedirectResponse("/login") + return user +``` + +For a complete, copy-paste Flask app with working cookie and Redis session +stores, load `framework-flask/index.md`. + +--- + +## Advanced configuration + +### Private Key JWT (no client secret) + +```python +# Load the PEM from a path given by an env var (keep the key file out of the +# repo - add *.pem to .gitignore); or pass the key contents directly via +# os.environ["AUTH0_CLIENT_ASSERTION_KEY"]. +with open(os.environ["AUTH0_CLIENT_ASSERTION_KEY_PATH"]) as f: + private_key = f.read() + +auth0 = ServerClient( + domain=os.environ["AUTH0_DOMAIN"], + client_id=os.environ["AUTH0_CLIENT_ID"], + client_assertion_signing_key=private_key, # instead of client_secret + client_assertion_signing_alg="RS256", # default + secret=os.environ["AUTH0_SECRET"], + authorization_params={"redirect_uri": os.environ["AUTH0_REDIRECT_URI"]}, +) +``` + +### Organizations + +Set a default org on the client (`organization="org_abc123"`) or per-login via +`StartInteractiveLoginOptions(organization=..., invitation=...)`. The logged-in +user's `org_id` / `org_name` appear on `UserClaims`. Load +`feature-organizations/index.md` for the full B2B workflow. + +### Pushed Authorization Requests (PAR) + +Pass `pushed_authorization_requests=True` to the constructor (or per login in +`StartInteractiveLoginOptions`) to send auth params over the back channel. + +### Multiple Custom Domains (MCD) + +Pass a callable as `domain` to resolve the tenant per request: + +```python +from auth0_server_python.auth_types import DomainResolverContext + +async def resolve_domain(ctx: DomainResolverContext) -> str: + host = (ctx.request_headers or {}).get("host", "").split(":")[0] + return DOMAIN_MAP.get(host, "default.auth0.com") + +auth0 = ServerClient(domain=resolve_domain, client_id=..., client_secret=..., secret=...) +``` + +--- + +## Common mistakes + +| Mistake | Fix | +|---------|-----| +| Calling a method without `await` | Every I/O method is async - `await` it or you get a coroutine object. | +| Returning `start_interactive_login()` / `logout()` directly | They return **URL strings** - wrap in your framework's redirect. | +| Passing options positionally to `logout()` | First positional arg is `LogoutOptions`; pass store context as `store_options=...`. | +| `get` returning a dict from a custom store | Return `StateData(**data)` / `TransactionData(**data)`, not a raw dict. | +| Omitting stores in production | In-memory defaults lose data on restart and aren't shared across workers - supply real stores. | +| `get_access_token` fails with no token | Set `audience` and add `offline_access` to `scope` so a refresh token is issued. | +| Not wrapping `complete_interactive_login` | It raises on CSRF/expiry/exchange failure - always try/except. | +| Passing `domain` with `https://` | Use the bare host, e.g. `tenant.us.auth0.com`. | +| Reusing this SDK to validate API JWTs | Wrong tool - use `auth0-fastapi-api` (resource server) for that. | +| Expecting back-channel logout with a cookie store | `delete_by_logout_token` has nothing to query - use a server-side store. | + +--- + +## References + +- [auth0-server-python on PyPI](https://pypi.org/project/auth0-server-python/) +- [auth0-server-python on GitHub](https://github.com/auth0/auth0-server-python) +- [SDK examples (InteractiveLogin, CustomTokenExchange, Passkeys, ConfigureStore, ...)](https://github.com/auth0/auth0-server-python/tree/main/examples) diff --git a/main/.mintlify/skills/auth0/references/framework-spa-js/index.md b/main/.mintlify/skills/auth0/references/framework-spa-js/index.md index 73172289a2..9750248a5c 100644 --- a/main/.mintlify/skills/auth0/references/framework-spa-js/index.md +++ b/main/.mintlify/skills/auth0/references/framework-spa-js/index.md @@ -184,7 +184,7 @@ const response = await fetch('https://your-api.example.com/data', { - API calls with tokens → see the Calling Protected APIs section below - Refresh tokens → see the Refresh Token Rotation section below - Organizations → see the Organizations section below -- MFA handling → see the MFA Handling section below +- MFA / step-up → ask for MFA (feature:mfa) - Error handling → see the Error Handling section below ## References @@ -269,13 +269,8 @@ process.env.REACT_APP_AUTH0_DOMAIN | `PopupTimeoutError` | `@auth0/auth0-spa-js` | `loginWithPopup` — user didn't complete in time | | `PopupCancelledError` | `@auth0/auth0-spa-js` | `loginWithPopup` — popup was closed by the user | | `PopupOpenError` | `@auth0/auth0-spa-js` | `loginWithPopup` — `window.open` returned null (popups blocked) | -| `MfaRequiredError` | `@auth0/auth0-spa-js` | `getTokenSilently` — MFA step required; access `error.mfa_token` | | `MissingRefreshTokenError` | `@auth0/auth0-spa-js` | `getTokenSilently` — refresh token not available | | `ConnectError` | `@auth0/auth0-spa-js` | `handleRedirectCallback` — error in connected accounts flow | -| `MfaListAuthenticatorsError` | `@auth0/auth0-spa-js` | `auth0.mfa.getAuthenticators()` failed | -| `MfaEnrollmentError` | `@auth0/auth0-spa-js` | `auth0.mfa.enroll()` failed | -| `MfaChallengeError` | `@auth0/auth0-spa-js` | `auth0.mfa.challenge()` failed | -| `MfaVerifyError` | `@auth0/auth0-spa-js` | `auth0.mfa.verify()` failed | --- @@ -698,29 +693,6 @@ if (organization && invitation) { --- -## MFA Handling - -Handle MFA when `getTokenSilently()` requires a second factor: - -```js -import { MfaRequiredError } from '@auth0/auth0-spa-js'; - -try { - const token = await auth0.getTokenSilently(); -} catch (error) { - if (error instanceof MfaRequiredError) { - // Trigger MFA challenge via popup or redirect - await auth0.loginWithPopup({ - authorizationParams: { - mfa_token: error.mfa_token - } - }); - } -} -``` - ---- - ## DPoP (Device-Bound Tokens) Enable DPoP to bind access tokens to the client's cryptographic key pair: @@ -755,7 +727,6 @@ import { PopupTimeoutError, PopupCancelledError, PopupOpenError, - MfaRequiredError, MissingRefreshTokenError } from '@auth0/auth0-spa-js'; diff --git a/main/.mintlify/skills/auth0/references/framework-swift/index.md b/main/.mintlify/skills/auth0/references/framework-swift/index.md index 007cee3797..bcc9b2f367 100644 --- a/main/.mintlify/skills/auth0/references/framework-swift/index.md +++ b/main/.mintlify/skills/auth0/references/framework-swift/index.md @@ -771,10 +771,8 @@ Auth0 // Access token available at credentials.accessToken credentialsManager.store(credentials: credentials) case .failure(let error) where error.isMultifactorRequired: - // Extract MFA token for MFA challenge flow - if let mfaPayload = error.mfaRequiredErrorPayload { - startMFAChallenge(mfaToken: mfaPayload.mfaToken) - } + // MFA required — see the feature:mfa reference + break case .failure(let error) where error.isNetworkError: showNetworkError() case .failure(let error): @@ -785,22 +783,6 @@ Auth0 --- -## MFA (Multi-Factor Authentication) - -### Handling MFA Required Error - -```swift -// When login returns isMultifactorRequired = true, challenge with OTP -func verifyMFA(mfaToken: String, otp: String) async throws -> Credentials { - return try await Auth0 - .authentication() - .multifactorChallenge(mfaToken: mfaToken, types: ["otp"]) - .start() -} -``` - ---- - ## Organizations ### Login to a Specific Organization diff --git a/main/.mintlify/skills/auth0/scripts/validate-skill.sh b/main/.mintlify/skills/auth0/scripts/validate-skill.sh index e9d5439b0c..8a54d0ed54 100755 --- a/main/.mintlify/skills/auth0/scripts/validate-skill.sh +++ b/main/.mintlify/skills/auth0/scripts/validate-skill.sh @@ -62,7 +62,7 @@ fi # "every routed framework has a file and every file is routed" guarantee is # enforced by scripts/check_router_reachability.py (run below), which derives # slugs from the router itself. Keep this list in sync when adding frameworks. -EXPECTED_FRAMEWORKS="react nextjs vue angular spa-js nuxt express flask fastify fastify-api java-mvc aspnetcore-auth aspnetcore-api php php-api express-jwt fastapi-api springboot-api go react-native expo ionic-angular ionic-react ionic-vue android swift kmp flutter-native flutter-web laravel laravel-api maui net-android net-ios winforms wpf node-auth0" +EXPECTED_FRAMEWORKS="react nextjs vue angular spa-js nuxt express flask fastify fastify-api java-mvc aspnetcore-auth aspnetcore-api php php-api express-jwt fastapi-api springboot-api go react-native expo ionic-angular ionic-react ionic-vue android swift kmp flutter-native flutter-web flutter-windows laravel laravel-api maui net-android net-ios winforms wpf node-auth0 auth0-auth-js auth0-server-js" for fw in $EXPECTED_FRAMEWORKS; do if [ ! -f "$REFS_DIR/framework-$fw/index.md" ]; then echo "FAIL: missing references/framework-$fw/index.md"