Skills/Auth0 Agent Skills

Auth0 Agent Skills

byAuth0 by Okta·backend

AI agent skills that help coding assistants implement Auth0 authentication correctly. The skill automatically inspects project dependencies, detects your target framework (Next.js, React, Node/Express, FastAPI, Nuxt, Angular, Flutter, Go, .NET, etc.), and loads exact SDK instructions, protected route patterns, and environment variable configurations.

CLAUDE CODECURSORCODEXGEMINI CLIANTIGRAVITYWINDSURF
Loading stats...

Installation

Install directly into your agent CLI.

$npx skills add auth0/agent-skills
Usage examplePrompt your agent

“Add Auth0 authentication to my app: set up protected routes, configure environment variables, and wire up login/logout.”

Auth0 Agent Skills Subgroups

Core SDK & Framework Integrationsjump ↓

Auto-detects framework (Next.js, React, Node/Express, FastAPI, Nuxt, Angular, Flutter, Android, iOS, Go, .NET) and scaffolds official SDK routes, login/logout, and session handlers.

Universal Login & Brandingjump ↓

Customizes hosted login and signup screens, brand colors, fonts, logos, and Advanced Customization for Universal Login (ACUL).

MFA & Passwordless Passkeys (WebAuthn)jump ↓

Configures multi-factor authentication (SMS, OTP, security keys), step-up auth for sensitive actions, and biometric passwordless passkey login.

B2B SaaS Organizations & Self-Service Portalsjump ↓

Implements multi-tenant business customer login with Auth0 Organizations, member role-based access control (RBAC), and hosted Universal Portals.

Security, DPoP & Token Exchangejump ↓

Enforces Demonstrating Proof-of-Possession (DPoP) sender-constrained tokens, Custom Token Exchange (RFC 8693), and API token verification.

Tenant Health, CheckMate Audit & Migrationjump ↓

Audits tenant configuration for security leaks, runs CheckMate health checks, and migrates users and auth from Firebase, Supabase, Cognito, or Clerk.

SKILL.md

--- name: auth0 description: Official Auth0 AI agent skills to implement authentication, MFA, Passkeys, Organizations, Universal Login, and tenant audits across 40+ frameworks. license: Apache-2.0 metadata: author: Auth0 <support@auth0.com> repo: auth0/agent-skills version: "2.6.0" ---

Auth0 Agent Skills

AI agent skills that help coding assistants implement Auth0 authentication correctly. Works with Claude Code, Cursor, Codex, Antigravity, Gemini CLI, GitHub Copilot, and 40+ other agents that support the Agent Skills format.

Overview & Architecture

  1. When you ask your AI assistant something like *"add Auth0 login to my app"*, the assistant:
  2. Loads the single auth0 skill, whose router inspects your project configuration files (package.json, requirements.txt, build.gradle, composer.json, go.mod, *.csproj, etc.)
  3. Automatically detects your framework (Next.js, React, Node/Express, FastAPI, Nuxt, Angular, Flutter, Go, .NET, etc.) and loads the matching reference files
  4. Follows step-by-step instructions to install the right SDK, configure environment variables, create authentication routes, and wire up login/logout

Subgroups Included

1. Core SDK & Framework Integrations — Auto-detects framework (Next.js, React, Node/Express, FastAPI, Nuxt, Angular, Flutter, Android, iOS, Go, .NET) and scaffolds official SDK routes, login/logout, and session handlers. 2. Universal Login & Branding — Customizes hosted login and signup screens, brand colors, fonts, logos, and Advanced Customization for Universal Login (ACUL). 3. MFA & Passwordless Passkeys (WebAuthn) — Configures multi-factor authentication (SMS, OTP, security keys), step-up auth for sensitive actions, and biometric passwordless passkey login. 4. B2B SaaS Organizations & Self-Service Portals — Implements multi-tenant business customer login with Auth0 Organizations, member role-based access control (RBAC), and hosted Universal Portals. 5. Security, DPoP & Token Exchange — Enforces Demonstrating Proof-of-Possession (DPoP) sender-constrained tokens, Custom Token Exchange (RFC 8693), and API token verification. 6. Tenant Health, CheckMate Audit & Migration — Audits tenant configuration for security leaks, runs CheckMate health checks, and migrates users and auth from Firebase, Supabase, Cognito, or Clerk.

Installation Quick Start

Claude Code

bash /plugin install auth0@claude-plugins-official # Or from terminal: claude plugin install auth0@claude-plugins-official

Cursor

Install via the Cursor marketplace or run: ``bash npx skills add auth0/agent-skills --agent github-copilot

Any Agent (Skills CLI)

bash npx skills add auth0/agent-skills

Antigravity

bash git clone https://github.com/auth0/agent-skills.git cd agent-skills agy plugin install plugins/auth0

ClawHub

bash npx clawhub install auth0

Common Example Prompts

- "Add Auth0 authentication to my app" - "Set up Auth0 in my Next.js project with protected routes" - "Add multi-factor authentication with TOTP" - "Migrate from Firebase Auth to Auth0" - "Secure my Express API with Auth0 JWT validation"


Subgroup 1: Core SDK & Framework Integrations

Auth0

Detect intent → detect framework → detect tooling → load 2–3 reference files.


Step 1: Detect intent

Match the request against the What the developer wants column — it describes the goal in plain language, not just the Auth0 term. The Intent you pick is a lookup key: in Step 4 it appears verbatim as a section heading (### feature:mfa) listing which reference files to load.

What the developer wants (plain language + Auth0 term)Intent
Add login, signup, sign-in, "let users log in / create accounts" to an app, or otherwise add and use an Auth0 SDK in an app or scriptintegrate
Require a second step after the password — a one-time code, SMS or email code, authenticator app, or a passkey/fingerprint/face/security key used as an *additional* factor; or re-confirm identity before a sensitive action. *Auth0: multi-factor authentication (MFA), two-factor (2FA), two-step verification, step-up authentication.*feature:mfa
Let users sign up or sign in with a passkey, fingerprint, or Face ID instead of a password — passwordless WebAuthn as the primary credential — or let a signed-in user add a passkey to their account. *Auth0: passkeys, WebAuthn, passwordless login.*feature:passkeys
Let separate companies, teams, workspaces, or tenants each have their own users, members, roles, and login — typically a product sold to businesses. *Auth0: Organizations, multi-org, B2B SaaS.*feature:organizations
Deploy a hosted self-service portal for profile, passkeys, MFA, or organization details instead of building a “My Account” or “My Organization” UI. *Auth0: Universal Portals, My Account portal, My Organization portal.*feature:universal-portals
Serve the login page from your own web address (e.g. login.example.com, auth.company.com) instead of the default Auth0 URL. *Auth0: custom domain.*feature:custom-domains
Build fully custom login/signup screens with your own code or framework, beyond what theme settings allow. *Auth0: Advanced Customization for Universal Login (ACUL).*feature:acul
Change how the login page looks — logo, colors, fonts, background, overall theme. *Auth0: branding, Universal Login customization.*feature:branding
Bind tokens to the client so a stolen or leaked token can't be reused/replayed from another machine. *Auth0: DPoP (Demonstrating Proof-of-Possession), sender-constrained tokens.*feature:dpop
Trade an external or partner token (legacy system, MCP/agent, or another IdP) for Auth0 tokens with no interactive login. *Auth0: Custom Token Exchange (CTE), RFC 8693, subject_token.*feature:custom-token-exchange
Audit a tenant for security/config issues, report, then optionally fix findings. *Auth0: tenant audit, CheckMate.*audit
Check if a tenant is healthy and on the right plan — two scores + a recommendation. *Auth0: health check.*healthcheck
Ask for best practices, "is this secure?", how to handle tokens safely, "how should I do X". *Auth0: guidance / security.*guidance
Hit an error: 401 Unauthorized, 403 Forbidden, CORS, callback URL mismatch, redirect loop. *Auth0: debugging.*debug
Hit rate limiting: 429 Too Many Requests, quota exceeded. *Auth0: rate limits.*debug:rate-limit
Move an existing app off Clerk, NextAuth.js, Firebase, Cognito, Okta, Supabase, Passport.js, or another auth provider. *Auth0: provider migration.*migrate
Upgrade the Auth0 SDK itself to a new major version (e.g. Auth0.swift v2→v3, Auth0.Android v3→v4) — breaking changes, deprecated APIs, "update to the latest SDK". *Auth0: SDK major-version upgrade.*upgrade-sdk
Install Auth0's Vercel Marketplace integration, connect Auth0 to a Vercel project, or sync Auth0 configuration into a Vercel-hosted Next.js app. *Auth0: Vercel native integration.*integrate
Use the Auth0 CLI directly — "create an app/API with the auth0 CLI", script tenant setup, or automate Auth0 config in CI — with no application framework in play. *Auth0: CLI / tooling-only.*tooling

If nothing clearly matches

Pick the closest goal. If the goal is genuinely unclear, ask the developer what they're trying to accomplish - don't guess an intent.


Step 2: Detect framework

Skip this step for the `tooling` intent — a CLI-first request has no > framework. Go to Step 3, load the tooling reference; only ask about a > framework if the developer later pivots to integrating auth into an app.

Work top-down. Stop at the first tier that yields a framework.

Tier 1 — Auth0 SDK already installed (strongest signal)

Node.js / JavaScript / TypeScript — check `package.json` → `dependencies`

Rows are most-specific first: an Ionic/Capacitor project also carries the base SDK, so check the @capacitor/browser rows before it.

PackageFramework
@capacitor/browser + @auth0/auth0-angularionic-angular
@capacitor/browser + @auth0/auth0-reactionic-react
@capacitor/browser + @auth0/auth0-vueionic-vue
@auth0/nextjs-auth0nextjs
@auth0/auth0-nuxtnuxt
@auth0/auth0-tanstack-start-reacttanstack-start
@auth0/auth0-reactreact
@auth0/auth0-vuevue
@auth0/auth0-angularangular
@auth0/auth0-spa-jsspa-js
express-openid-connectexpress
@auth0/auth0-fastifyfastify
@auth0/auth0-fastify-apifastify-api
express-oauth2-jwt-bearerexpress-jwt
react-native-auth0 + app.json or app.config.js presentexpo
react-native-auth0 (no Expo files)react-native
@auth0/auth0-api-jsauth0-api-js
@auth0/auth0-server-jsauth0-server-js
@auth0/auth0-auth-jsauth0-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.

PackageFramework
auth0-server-python + flaskflask
auth0-server-python (no Flask web framework)server-python
auth0-fastapi-apifastapi-api
auth0-api-pythonapi-python

Java / Kotlin — check `build.gradle` or `pom.xml`

DependencyFramework
mvc-auth-commons (com.auth0:mvc-auth-commons)java-mvc
spring-security-oauth2-resource-serverspringboot-api

.NET — check `*.csproj` or `NuGet.Config`

PackageFramework
Auth0.AspNetCore.Authentication (no .Api suffix)aspnetcore-auth
Auth0.AspNetCore.Authentication.Apiaspnetcore-api
Auth0.OidcClient.MAUImaui
Auth0.OidcClient.AndroidXnet-android
Auth0.OidcClient.iOSnet-ios
Auth0.OidcClient.WinFormswinforms
Auth0.OidcClient.WPFwpf

PHP — check `composer.json`

auth0/auth0-php powers both PHP web apps and APIs via SdkConfiguration's strategy. The STRATEGY_API row is more specific — check it first.

PackageFramework
auth0/auth0-php + SdkConfiguration::STRATEGY_API (or strategy: 'api')php-api
auth0/auth0-php (no STRATEGY_API / STRATEGY_REGULAR or strategy: 'webapp')php
auth0/login (laravel, no AuthorizationGuard)laravel
auth0/login + AuthorizationGuardlaravel-api
If auth0/auth0-php is installed but no SdkConfiguration strategy is set > yet (fresh project), fall through to variant disambiguation below.

Go — check `go.mod`

ModuleFramework
github.com/auth0/go-jwt-middlewarego

Mobile (native)

SignalFramework
build.gradle(.kts) + com.auth0.kmp:auth0 (Kotlin Multiplatform module)kmp
Package.swift or .xcodeproj + Auth0.swiftswift
build.gradle + com.auth0.android:auth0android
pubspec.yaml + auth0_flutter + developer names/targets Windows desktopflutter-windows
pubspec.yaml + auth0_flutter + flutter.web: falseflutter-native
pubspec.yaml + auth0_flutter + web enabledflutter-web

Tier 2 — Framework from non-Auth0 workspace dependencies

If no Auth0 SDK matched, detect the framework from ordinary (non-Auth0) dependencies. Stop at the first match. This picks the base; any web-vs-API variant is resolved in "Variant disambiguation" below. As in Tier 1, check the @ionic/* rows before their base framework.

SignalBase framework
next in package.jsonnextjs
nuxt in package.jsonnuxt
@tanstack/react-start in package.jsontanstack-start
@ionic/* + @angular/coreionic-angular
@ionic/* + reactionic-react
@ionic/* + vueionic-vue
@angular/core in package.jsonangular
vue in package.json (no nuxt)vue
expo in package.jsonexpo
react-native (no expo)react-native
react (no meta-framework above)react (SPA) — see note
express in package.jsonexpress (variant below)
fastify in package.jsonfastify (variant below)
flask in requirements.txt/pyproject.tomlflask
fastapi in requirements.txt/pyproject.tomlfastapi-api (variant below)
spring-boot in pom.xml/build.gradlespringboot-api
laravel/framework in composer.jsonlaravel (variant below)
composer.json present (no Laravel)php (variant below)
go.mod present + HTTP server/routergo
org.jetbrains.kotlin.multiplatform plugin + commonMain source set (shared Android+iOS module)kmp
Package.swift or .xcodeprojswift
pubspec.yaml (Flutter) + developer names/targets Windows desktopflutter-windows
pubspec.yaml (Flutter, web disabled)flutter-native
pubspec.yaml (Flutter, web enabled)flutter-web
*.csproj referencing MAUImaui
*.csproj (WinForms)winforms
*.csproj (WPF)wpf
*.csproj ASP.NET (web app or API)aspnetcore (variant below)
`react` note: a plain React project maps to react for an SPA using the > React SDK, or spa-js if the app is framework-agnostic vanilla JS. If unclear, > ask before loading.

Tier 3 — Framework from the prompt

If no workspace signal matched, map the framework or language named in the request. Stop at the first match.

Developer mentions...Framework
Next.js / nextnextjs
Nuxtnuxt
TanStack Start (React)tanstack-start
Angular (not Ionic)angular
Vue (not Nuxt/Ionic)vue
React SPA (not Next.js)react
vanilla JS / plain JS / no framework SPAspa-js
node-auth0 / the auth0 npm packagenode-auth0
@auth0/auth0-api-js / auth0-api-js / low-level resource-server SDKauth0-api-js
@auth0/auth0-server-js / auth0-server-js / server-side Auth0 session SDKauth0-server-js
@auth0/auth0-auth-js / auth0-auth-js / AuthClient / low-level OAuth OIDCauth0-auth-js
Express (web app / server-rendered)express
Express API / protect API routesexpress-jwt
Fastify (web) / Fastify APIfastify / fastify-api
Flaskflask
FastAPI (web app) / FastAPI APIserver-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
auth0-api-python / Python API JWT validator (resource server / verify_access_token)api-python
Spring Bootspringboot-api
Java MVC / servletjava-mvc
ASP.NET Core web app / APIaspnetcore-auth / aspnetcore-api
MAUI / WinForms / WPFmaui / winforms / wpf
PHP web app / PHP APIphp / php-api
Laravel web app / Laravel APIlaravel / laravel-api
Go / Golang APIgo
Kotlin Multiplatform / KMP / shared Android+iOS auth code / com.auth0.kmpkmp
Swift / iOSswift
Android / Kotlinandroid
Flutter (native / web / Windows)flutter-native / flutter-web / flutter-windows
React Native / Exporeact-native / expo
Ionic (Angular/React/Vue)ionic-angular / ionic-react / ionic-vue

Variant disambiguation (web app vs API)

Some frameworks have separate web-app and API references. When Tier 1 did not pin the variant, choose intent-first:

BaseWeb-app variantAPI variantChoose API when…
expressexpressexpress-jwtprotecting API routes / validating JWTs, no server-rendered UI
fastifyfastifyfastify-apiresource server / JWT validation only
fastapiserver-pythonfastapi-apiresource server / JWT validation only; a web app with login/logout UI uses server-python
phpphpphp-apibuilding/protecting a PHP API, no web UI
laravellaravellaravel-apiAPI-only (token guard), no Blade UI
aspnetcoreaspnetcore-authaspnetcore-apiWeb API / JWT bearer, no cookie login UI

If intent is still ambiguous (both a UI and protected endpoints, or unclear), state what you detected and ask the developer web app vs API before loading.

If nothing matched

Ask the developer what framework/language they are using. Do not guess.

Conflicts

If Tier 2 (workspace) and Tier 3 (prompt) disagree materially (e.g. the prompt says "Next.js" but package.json has no next), state the conflict and ask rather than silently picking. Workspace signals outrank the prompt when both are present and consistent.


Step 3: Detect tooling

Read the project file tree and the request — a project-context decision.

Project has...Load
terraform/ directory OR any *.tf filestooling-terraform/index.md
Auth0 MCP server active in this agent sessiontooling-mcp/index.md
A request for the Auth0 Vercel Marketplace/native integration, or to connect Auth0 to a Vercel projecttooling-vercel/index.md
Anything else (default)tooling-cli/index.md

Step 4: Load reference files

Find the section below whose heading matches the Intent you picked in Step 1, then read the reference files it lists.

integrate

Read: references/framework-{framework}/index.md Read: references/tooling-{tooling}/index.md Follow the integration workflow in references/framework-{framework}/index.md. Use references/tooling-{tooling}/index.md for all Auth0 tenant configuration steps.

feature:mfa

Read: references/feature-mfa/index.md Read: references/tooling-{tooling}/index.md

feature:passkeys

Read: references/feature-passkeys/index.md Then Read the detected SDK's passkey leaf from the hub's "Example code snippets" table (e.g. nextjs → nextjs-auth0.md; react-native/expo → ONLY react-native-auth0.md, which covers react-native-web — never auth0-react.md) — it has the exact API and ceremony; don't implement from framework-{framework}/index.md alone. Read: references/tooling-{tooling}/index.md If framework detected: Read references/framework-{framework}/index.md

feature:organizations

Read: references/feature-organizations/index.md Read: references/tooling-{tooling}/index.md If framework detected: Read references/framework-{framework}/index.md If multi-tenant architecture / B2B SaaS design question: also Read references/pattern-multi-tenant/index.md

feature:universal-portals

Read: references/feature-universal-portals/index.md Read: references/tooling-{tooling}/index.md

feature:custom-domains

Read: references/feature-custom-domains/index.md Read: references/tooling-{tooling}/index.md

feature:acul

Read: references/feature-acul/index.md Read: references/tooling-{tooling}/index.md

feature:branding

Read: references/feature-branding/index.md Read: references/tooling-{tooling}/index.md

feature:dpop

Read: references/feature-dpop/index.md Read: references/tooling-{tooling}/index.md If a SPA framework is detected (vue/react/angular/spa-js): Read references/framework-{framework}/index.md DPoP is SPA-only (no SSR: Next.js/Nuxt) — feature-dpop/index.md states the exclusion.

feature:custom-token-exchange

Read: references/feature-custom-token-exchange/index.md Read: references/tooling-{tooling}/index.md If framework detected: Read references/framework-{framework}/index.md

guidance

Read: references/pattern-security/index.md If framework detected: Read references/framework-{framework}/index.md (for SDK-specific guidance — token storage, session handling, route protection) If token handling / JWT vs opaque / storage: Read references/pattern-token-handling/index.md If multi-tenant / B2B architecture: Read references/pattern-multi-tenant/index.md + references/feature-organizations/index.md

debug

Read: references/pattern-common-errors/index.md If framework detected: Read references/framework-{framework}/index.md

debug:rate-limit

Read: references/pattern-rate-limiting/index.md

migrate

Read: references/feature-migration/index.md Read: references/tooling-{tooling}/index.md If framework detected: Read references/framework-{framework}/index.md

audit

Read: references/feature-audit/index.md Read: references/feature-audit-pricing/index.md Read: references/feature-audit-remediation/index.md Read: references/tooling-{tooling}/index.md Apply findings only with per-command confirmation; verify each change by re-fetch.

healthcheck

Read: references/feature-healthcheck/index.md Read: references/feature-audit/index.md Read: references/feature-audit-pricing/index.md Read: references/feature-audit-remediation/index.md Read: references/tooling-{tooling}/index.md If a scan can run, do the audit workflow first, then score and recommend a plan. If not, score capability fit and recommend anyway. Never quote Enterprise pricing.

upgrade-sdk

Read: references/framework-{framework}/index.md Follow its "Major Version Migration" section (e.g. Auth0.swift v3, Auth0.Android v4). This is an Auth0 SDK version bump — NOT a provider migration. Do not load feature-migration/index.md. If no framework is detected: ask which Auth0 SDK the developer is upgrading.

tooling

Read: references/tooling-{tooling}/index.md No framework file — this is a CLI/tooling-only task (create apps/APIs, script tenant setup, automate config in CI). If the developer then wants to integrate auth into an app, return to Step 1 with the integrate intent.


Subgroup 2: Universal Login & Branding

Universal Login Customization

Universal Login provides hosted authentication pages maintained and secured by Auth0.

Available Customization Surfaces:

- No-Code Theme Editor: Brand colors, widget corner radius, page background, typography, and company logo. - Custom Domains: Serve login pages directly from auth.yourdomain.com instead of *.us.auth0.com. - Advanced Customization for Universal Login (ACUL): Build custom React or vanilla JS screens using @auth0/auth0-acul-react or @auth0/auth0-acul-js.

bash
# Initialize ACUL project locally with Auth0 CLI
auth0 acul init
auth0 acul screen add

Subgroup 3: MFA & Passwordless Passkeys (WebAuthn)

Auth0 MFA

Require a second authentication factor - during login, or step-up before a sensitive action - and enforce it where it cannot be bypassed: on the tenant and on your API. For adding a first login to an app, use the framework-* reference instead; MFA layers on top of an existing login.

When to use / when NOT to use

Use when the app must:

  • Require a second factor for all logins (baseline enforcement).
  • Step up to MFA before a specific sensitive action (payment, settings change, admin operation) without forcing it on every login.
  • Require MFA conditionally based on risk, role, client, or requested scopes (adaptive MFA).
  • Meet a compliance obligation that mandates multi-factor (PCI-DSS, SOC 2, HIPAA).

Do NOT use this reference when:

  • The task is adding the initial login/logout to an app - that is the framework-* reference. MFA presupposes a working login.
  • The request is passkeys/WebAuthn as the *primary* passwordless factor rather than a second factor - the mechanic overlaps but the framing differs; still route here for the enrollment surface, but treat passwordless login as a login concern.
  • The task is only tenant provisioning with no application behavior - defer to tooling-*.

Concepts

  • Factor - a verification method (TOTP/OTP, SMS, email, push, WebAuthn, voice, recovery code).
  • Step-up authentication - requiring MFA for a specific action after an initial login that did not use MFA.
  • Adaptive MFA - requiring MFA conditionally from risk or context signals rather than always.

SDK integration

MFA must be enforced in two independent places; getting either wrong ships a bypass:

  1. Tenant / login flow - the tenant decides MFA is required (Guardian policy or Action). This is what actually challenges the user. See "Tenant configuration".
  2. Application - the app triggers the step-up and *verifies* the result. Triggering alone enforces nothing; verification closes the bypass.

Pick one mechanic:

  • App redirects to Universal Login (regular web app or SPA) -> browser step-up below.
  • App collects credentials itself (a direct-grant or passwordless backend, e.g. @auth0/auth0-auth-js / @auth0/auth0-server-js) -> API-driven MFA below; skip the browser-step-up section.

Before writing code, read the detected SDK's example (see "Example code snippets").

Scope — do this, then stop

The deliverable is the application code, written from the detected SDK's example file. Write it early; do not spend the task investigating. Specifically:

  • Trust the per-SDK file's method/option names — they are verified against the installed SDK. Do NOT grep node_modules, read .d.ts/.d.cts/site-packages/SDK source, run the SDK's own test suite, or write throwaway python -c/node -e probes to confirm a signature. Write the code; inspect the installed package only if a specific line you wrote fails to compile, and then only that line.
  • The minimum version in each SDK file is informational. The scaffold already pins a compatible release, so don't read node_modules/package.json to confirm the installed version meets it — only verify — X+ rows call for a check.
  • Don't add dependencies you weren't asked for. A barcode_uri/barcodeUri is a string you can render or return as-is; don't npm install a QR library unless the task requires rendering one.
  • You're done when the app code is in place (and, for a JS/TS app, npm run build passes if quick). Stop there.

The mechanic: browser step-up

Recipe (do in order):

  1. Trigger. Send the authorization request with the PAPE multi-factor acr_values and
  2. max_age=0 (forces a fresh challenge so a live session cannot satisfy it silently).
  3. Every SDK wraps this one shape:
   GET /authorize?...&acr_values=http://schemas.openid.net/pape/policies/2007/06/multi-factor&max_age=0
   ```
  1. Verify completion with the signal that matches your audience (table below).
  2. Enforce that same signal server-side on every protected endpoint, rejecting when it
  3. is absent. A frontend check is UX, never security.
  4. Conditional MFA: require it from a post-login Action calling api.multifactor.enable(...).
Your contextVerify 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 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". For express-oauth2-jwt-bearer, read the express-jwt skill reference and add the step-up scope to the existing requiredScopes() as a space-separated string or array (requiredScopes('write:transfers transfer:funds')); passing multiple string args (requiredScopes('a', 'b')) silently drops all but the first and leaves the gate open.
FrontendNothing - 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"). 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 by tooling-*); if the all-applications policy is set, an Action can only add conditions, not relax it.

The mechanic: API-driven MFA (no-redirect flows)

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 SDK's own example ("Example code snippets" below); never hand-roll the token grant or the MFA API URLs.

Recipe (do in order):

  1. Read the token. Catch mfa_required; read mfa_token off it.
  2. Branch on enrollment.
  3. - *No factor yet* -> associate (enroll). OTP returns a barcode_uri to render as a QR
  4. code; out-of-band (SMS/voice/email/push) returns an oob_code you confirm by
  5. submitting it directly to the token endpoint (MFA OOB grant, plus a binding_code
  6. when the channel requires one) - a just-associated authenticator needs no separate
  7. /mfa/challenge. Recovery codes are returned only on first enrollment.
  8. - *Already enrolled* -> challenge the existing authenticator so the user is prompted for
  9. its code.

(An explicit enrolled/not-enrolled check or branching on the error's requirements are both fine.) 3. Verify to finish. Always carries the mfa_token: submit an OTP code as otp; for an already-enrolled out-of-band factor, /mfa/challenge first for a fresh oob_code, then submit it (plus a binding_code when needed); submit a recovery code as recovery_code (its own recovery-code grant, not treated as an OTP). Success returns tokens like an ordinary sign-in. 4. Self-service management. List and remove factors through the MFA client, not the Management API. List and challenge use the mfa_token; removal needs a post-MFA access token (https://{yourDomain}/mfa/ audience + remove:authenticators scope) - the mfa_token alone does not authorize the DELETE. See "MFA API surface".

Feature-level symbols

Protocol-level names that are identical across every SDK - what a grader would assert and what the app must get right:

SymbolMeaning
acr_valuesAuthorization-request parameter used to request MFA
http://schemas.openid.net/pape/policies/2007/06/multi-factorThe PAPE value that requests multi-factor
max_age=0Forces fresh authentication so a live session cannot satisfy the step-up silently
amrID-token claim listing the methods used; contains mfa when MFA completed. Not present on access tokens by default
acrClaim echoing the satisfied authentication context
high-value scopeAn API scope (e.g. transfer:funds) whose request an Action gates behind MFA; its presence on the access token is the API-side proof of step-up
api.multifactor.enable(...)Post-login Action call that requires MFA for the current login

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; get them from the SDK's own example (see "Example code snippets").

`amr` claim values

The amr array reports how the user authenticated (protocol-level; KEEP INLINE):

ValueMeaning
pwdPassword
mfaMulti-factor authentication completed
otpOne-time password (TOTP authenticator app)
smsSMS code
emailEmail code
hwkHardware key (WebAuthn security key)
swkSoftware key
popProof of possession
fedFederated (social / enterprise)

Error responses

Returned by the token/authorization endpoints during an MFA flow (KEEP INLINE):

ErrorCauseHandling
mfa_requiredMFA has not been completedBrowser flow: re-authenticate interactively with the step-up parameters. No-redirect flow: read the mfa_token off the error and drive the MFA API (enroll/challenge/verify)
association_requiredThe user has no authenticator enrolledSend the user through enrollment (self-service or enrollment ticket), then challenge
unsupported_challenge_typeThe app supports none of the challenge types the user is enrolled with, or the user is not enrolledAlign the app's supported challenge types with the user's enrolled authenticators (or enroll the user) - do NOT change tenant config first
mfa_invalid_codeWrong OTP enteredPrompt to retry
too_many_attemptsRepeated failuresBack off; the account may be temporarily blocked

Example code snippets

Before writing MFA code: find the detected SDK's row below and `Read:` the file in its Reference column (the path is relative to the skill root, e.g. Read: references/feature-mfa/auth0-react.md). That file is required reading — it has the SDK's exact method/option names for the step-up and/or MFA API flow, plus the minimum version the feature needs. These files are the trusted source; implement directly from them. The method and option names in them are verified against the installed SDK — do NOT re-verify signatures by grepping node_modules, reading .d.ts files or SDK source, fetching from GitHub, web-searching "how to do MFA", or querying the auth0-docs MCP. Write the code from the file. Only inspect node_modules if a specific call you wrote fails to compile — and then only that call. No matching row (a backend SDK not listed)? Fall back to the language-neutral mechanic above.

Min version is the earliest SDK release where the feature shipped; verify — X+ marks a version to confirm against the installed package. If the app pins an older version, upgrade it first or fall back to the language-neutral mechanic. Several MFA API flows are Early Access (noted in the file) and need tenant enablement.

SDKMin versionFlow(s)Reference (Read this file)
@auth0/auth0-react2.14.0 (MFA API) · 2.15.0 (popup step-up)popup step-up, MFA APIreferences/feature-mfa/auth0-react.md
@auth0/auth0-vue2.6.0popup step-up, MFA APIreferences/feature-mfa/auth0-vue.md
@auth0/auth0-angular2.9.0popup step-up, MFA APIreferences/feature-mfa/auth0-angular.md
@auth0/auth0-spa-js2.16.0popup step-upreferences/feature-mfa/auth0-spa-js.md
@auth0/nextjs-auth04.15.0 (MFA + APIs) · verify — 4.19+ (popup step-up)step-up, MFA API, popupreferences/feature-mfa/nextjs-auth0.md
@auth0/auth0-auth-js1.8.0MFA APIreferences/feature-mfa/auth0-auth-js.md
@auth0/auth0-server-js1.5.0MFA APIreferences/feature-mfa/auth0-server-js.md
express-openid-connect2.17.0redirect step-upreferences/feature-mfa/express-oidc.md
Auth0.swift (iOS/macOS)verify — 3.0+MFA APIreferences/feature-mfa/auth0-swift.md
Auth0.Androidverify — 3.13+MFA APIreferences/feature-mfa/auth0-android.md
react-native-auth05.10.0MFA APIreferences/feature-mfa/react-native-auth0.md
auth0-server-python1.0.0b10 (MFA API) · 1.0.0b15 (step-up)step-up, MFA APIreferences/feature-mfa/auth0-server-python.md
Auth0.OidcClient.* (.NET native/desktop)4.4.0client-initiated step-upreferences/feature-mfa/oidc-client-net.md
go-jwt-middleware (Go, resource server)v2.2.1API-side scope gatereferences/feature-mfa/go-jwt-middleware.md
Auth0.AspNetCore.Authentication.Api (resource server)1.0.1API-side scope gatereferences/feature-mfa/aspnetcore-api.md
spring-boot-starter-oauth2-resource-server (Java)Spring Boot 3.3 / Security 6API-side scope gatereferences/feature-mfa/spring-security-api.md
auth0-api-python (Python resource server)1.0.0b10API-side scope gatereferences/feature-mfa/auth0-api-python.md

Note: the resource-server rows (go-jwt-middleware, Auth0.AspNetCore.Authentication.Api, Spring Security, auth0-api-python) do not run MFA — the API enforces the step-up scope the tenant issues post-MFA, gating a sensitive route on it. Auth0.OidcClient.* is a native client that triggers step-up via acr_values.

Note: the JS SPA/framework SDKs (auth0-react, auth0-vue, auth0-angular, auth0-spa-js) drive step-up through the SDK's interactiveErrorHandler: 'popup' option, not the acr_values/max_age=0 parameters of the language-neutral mechanic above — see the file.

Tenant configuration

A factor must be enabled before anything can challenge with it; enabling a factor alone never prompts anyone until an enforcement path requires it, and setting enforcement before any factor is enabled leaves users unable to complete MFA. So enable the factor first, then choose an enforcement path - the two are independent:

  • Tenant-wide (Guardian policy) - set guardian/policies to ["all-applications"] to
  • require MFA for *every* application on every login. This is the only path the ordering
  • rule above is about, and the CLI anchor below shows it.
  • Conditional (post-login Action) - enable the factor and configure an Action that
  • calls api.multifactor.enable(...); do not set the all-applications policy, or MFA
  • becomes mandatory for every application instead of the conditions the Action defines.

The CLI anchor for the tenant-wide path (enable factors, then enforce the policy). SMS requires three separate endpoints — guardian/factors/sms does not accept message_type or provider in its body (returns 400); use the phone sub-endpoints below:

bash
# TOTP / Authenticator app
auth0 guardian factors set otp --enabled

SMS — three separate commands required

auth0 guardian factors set sms --enabled auth0 guardian factors phone set-message-types --message-type sms auth0 guardian factors phone set-provider --provider auth0 # legacy phone config only; unified-phone tenants set the provider in Dashboard > Branding > Phone Provider

Email

auth0 guardian factors set email --enabled

Enforce MFA for all applications

auth0 guardian policies set --policy all-applications

Verify

auth0 guardian factors list ```

The full factor set, the confidence-score (adaptive) policy, the Terraform auth0_guardian resource, and MCP coverage are owned by the loaded tooling-* reference (DEFER ACROSS): the Auth0 MCP server exposes no Guardian/MFA tool, so tenant MFA config is CLI or Terraform only.

MFA API surface (in-flow self-service)

The mechanic above wraps these MFA API endpoints - the language-neutral floor that every SDK wraps; do not call them by hand. None need a Management API admin scope, but they do not all take the same credential: enrolling, challenging, and listing run on the mfa_token from the mfa_required error, while removing an authenticator requires a post-MFA access token with the https://{yourDomain}/mfa/ audience and the remove:authenticators scope:

OperationEndpointAuthorized by
Enroll (associate) a new authenticatorPOST /mfa/associatemfa_token when the user has no active factor yet; otherwise an enroll-scoped access token for the https://{yourDomain}/mfa/ audience
List the user's enrolled authenticatorsGET /mfa/authenticatorsmfa_token
Challenge an enrolled authenticatorPOST /mfa/challengemfa_token
Remove an enrolled authenticatorDELETE /mfa/authenticators/{id}post-MFA access token, remove:authenticators scope, mfa audience
Complete sign-in with the verified factorPOST /oauth/token (MFA grant)mfa_token

Management API surface (admin / out-of-band enrollment)

Only for acting on a user *by id* with a Management API token (an admin dashboard or a provisioning back-end) - not for a user managing their own factors in the flow above, which uses the mfa_token and the MFA API surface instead:

OperationEndpoint
List a user's authentication methodsGET users/{id}/authentication-methods
Delete one authentication methodDELETE users/{id}/authentication-methods/{authentication_method_id}
Send an enrollment ticketPOST guardian/enrollments/ticket

Common mistakes

MistakeWhy it breaksCorrect approach
Setting a Guardian policy before enabling any factorUsers are required to do MFA but have no factor to complete it withEnable the factor first, then set the policy
Treating an enabled factor as enforcementA factor with no policy never challenges anyoneEnforce with a policy or a post-login Action
Reading guardian/policies as [] and assuming MFA is on[] means available but not requiredConfirm a non-empty policy (or an Action that enables MFA)
Trusting a frontend MFA checkThe client can be bypassed entirelyEnforce server-side: amr on a web/session backend, the high-value scope on a resource API
Checking amr on a resource API's access tokenAccess tokens carry no amr by default, so valid stepped-up callers are rejectedGate 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 processRead amr from the SDK's session/current-user accessor; its claims are already verified
Assuming the session accessor always carries amrSome SDKs persist only a default claim subset, so amr is silently absent and the check never passesOpt 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-upA still-valid session satisfies the request with no fresh challengeSend max_age=0 (or the SDK's fresh-auth option) for step-up
Ignoring mfa_required from a silent token callThe step-up silently fails and the action proceeds unverifiedCatch it and re-authenticate interactively
Preferring SMS by defaultSMS is vulnerable to SIM-swapPrefer TOTP or WebAuthn; treat SMS as a fallback
No recovery codes enabledUsers get locked out when they lose a deviceEnable recovery codes during enrollment
Wrong HTTP verb on guardian/policiesReturns a misleading 404Use auth0 guardian policies set --policy all-applications
Sending message_type or provider to guardian/factors/sms directlyReturns a 400 — those fields are not accepted on that endpointUse auth0 guardian factors phone set-message-types --message-type sms for the message type and auth0 guardian factors phone set-provider --provider auth0 for the provider (legacy phone config only — on the unified phone experience, set the provider in Dashboard > Branding > Phone Provider)
Using the Management API to list or remove a user's own factors during the sign-in flowForces the app to hold Management API admin scopes and ignores the mfa_token the flow already issuedList and challenge through the SDK's MFA client on the mfa_token; remove with a post-MFA remove:authenticators access token (mfa audience); reserve the Management API for admin / out-of-band
Assuming an already-enrolled factor needs no challenge and jumping straight to verifyDiverges from the SDK's documented enrolled-factor flow and breaks for out-of-band factors (SMS/push), whose challenge is what delivers the codeChallenge the enrolled authenticator, then verify

Related capabilities

  • 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 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
  • check the amr claim.
  • First login - if the app has no login yet, add it with the framework-* reference
  • before layering MFA.

References

Run auth0 docs search "multi-factor authentication" for the latest Auth0 docs on this topic.

Auth0 MFA docs Step-Up Authentication.


Passwordless WebAuthn & Passkeys Guide

Auth0 Passkeys

Let users sign up and log in with a passkey (WebAuthn) instead of a password — the passkey is the *primary* credential, not a second factor. Also covers letting an already-authenticated user add a passkey to their account (My Account API). For adding a first password login to an app, use the framework-* reference; passkeys layer their own grant on top of an existing connection.

When to use / when NOT to use

Use when the app must:

  • Let users sign up with a passkey (no password created).
  • Let users log in with an existing passkey.
  • Let a signed-in user enroll an additional passkey as a self-service account action.

Do NOT use this reference when:

  • The request is a passkey/WebAuthn/security key as a second factor on top of a password login — that is the feature-mfa reference (the hwk factor and the MFA API enroll/challenge/verify flow). Passkeys here replace the password; MFA layers on top of it.
  • The task is deploying a hosted self-service portal for passkey management rather than building the flow in-app — that is feature-universal-portals (My Account portal).
  • The task is only tenant provisioning with no application behavior — defer to tooling-*.

Concepts

  • Passkey / WebAuthn — a FIDO2 credential (platform biometric or roaming security key) bound to a relying party. Replaces the password entirely.
  • Relying Party ID (`rpId`) — the domain the passkey is bound to. This is why a custom domain is mandatory (below).
  • Attestation vs assertion — *signup/registration* produces an attestation (navigator.credentials.create()); *login* produces an assertion (navigator.credentials.get()).
  • Passkey grant — the OAuth grant urn:okta:params:oauth:grant-type:webauthn the app exchanges a verified credential for tokens on. Must be enabled on the application.

SDK integration

Passkeys need setup in two independent places:

  1. Tenant / connection — the passkey authentication method is enabled on the database connection, the passkey grant is enabled on the application, and a custom domain is configured. See "Tenant configuration".
  2. Application — the app runs the passkey flow with its SDK. See "Example code snippets".

The mechanic — a 3-step flow

Every SDK wraps the same shape:

  1. Challenge — ask Auth0 for a WebAuthn challenge (signup or login). Returns an authSession plus the public-key options the browser/authenticator needs.
  2. Ceremony — the platform runs WebAuthn: navigator.credentials.create() (signup) or .get() (login) on web; ASAuthorizationController on Apple platforms; CredentialManager on Android. This produces a signed credential.
  3. Token exchange — send the credential + authSession back to Auth0 on the passkey grant; on success you get tokens (or an established session).

Two SDK shapes — pick by what the detected SDK exposes:

  • High-level (auth0-react, auth0-vue, auth0-angular, native SDKs) — one passkey.signup() / passkey.login() (or platform equivalent) runs all three steps for you and returns tokens.
  • Low-level (auth0-spa-js, auth0-server-js, auth0-auth-js, auth0-server-python, nextjs-auth0) — you call the challenge method, run the browser ceremony yourself, then call the token-exchange method.

Before writing code, read the detected SDK's file (see "Example code snippets").

Scope — do this, then stop

The deliverable is the application code, written from the detected SDK's leaf file. Write it early; do not spend the task investigating.

  • Trust the per-SDK file's method/option names — they are verified against the installed SDK. Do NOT grep node_modules, read .d.ts/.d.cts/site-packages/SDK source, run the SDK's test suite, write throwaway probes, fetch the SDK's GitHub/source or any web page, run a filesystem-wide search (`find /`), or cross-check against the Auth0-docs MCP to confirm a signature. The leaf is authoritative; when it names a symbol, use it. Write the code; inspect the installed package only if a specific line fails to compile, and then only that line.
  • The minimum version in each SDK file is informational. The scaffold already pins a compatible release; don't read package.json/node_modules to confirm it unless a verify — X+ row tells you to.
  • You're done when the app code is in place (and, for a JS/TS app, npm run build passes if quick). Stop there.

Tenant configuration

Passkeys require three things set before any flow works; all three are independent of the app code:

bash
# 1. A custom domain MUST exist and be verified — passkeys are bound to the rpId
#    (the domain). They will NOT work on the default *.auth0.com domain.
auth0 domains list
# create + verify one if none: see the tooling-* reference for the full flow.

2. Enable the passkey authentication method on the database connection

# (Dashboard: Authentication > Database > {connection} > Authentication Methods > Passkey). # The signup identifier (email / username / phone) must match the connection's # Attributes configuration.

3. Enable the passkey (WebAuthn) grant on the application

auth0 apps update <CLIENT_ID> --grants "authorization_code,refresh_token,urn:okta:params:oauth:grant-type:webauthn" ```

The full connection-attribute config, the passkey policy, and the Terraform/MCP coverage are owned by the loaded tooling-* reference (DEFER ACROSS).

Feature-level symbols

Protocol-level concepts common to every SDK — what a grader asserts and what the app must get right. The concept is shared, but a few names vary by SDK (called out in the row); match the spelling the SDK's own leaf file uses:

SymbolMeaning
urn:okta:params:oauth:grant-type:webauthnThe passkey grant the credential is exchanged for tokens on
authSessionOpaque session string returned by the challenge; passed back to the token exchange
authnParamsPublicKey / authParamsPublicKey / publicKeyThe WebAuthn public-key options (challenge, rpId, user) the authenticator consumes — web/server SDKs spell it authnParamsPublicKey, the native SDKs (Auth0.Android, react-native-auth0) spell it authParamsPublicKey (no "n"), raw REST uses publicKey
clientDataJSONPart of the browser credential response (both create and get)
attestationObjectRegistration (signup) credential response field
authenticatorData + signature + userHandleAssertion (login) credential response fields

The resulting refresh token records the authentication method (AMR). Passkey logins surface WebAuthn AMR values defined by RFC 8176 — swk (proof-of-possession of a software-secured key, e.g. a platform passkey), hwk (hardware-secured key / security key), pop (proof of possession). These come from the IANA AMR registry, not an Auth0-specific list.

SDK-specific symbols (the exact method/option names) are not listed here — get them from the SDK's own leaf file.

Passkey API endpoints

The language-neutral REST floor every SDK wraps (the authoritative reference is the native passkeys API):

OperationEndpointKey paramsAuthorized by
Signup / registration challengePOST /passkey/registerclient_id, realm (optional; connection), user_profile { email, name? }Public client (client_id)
Login / authentication challengePOST /passkey/challengeclient_id, realm (optional)Public client (client_id)
Token exchangePOST /oauth/tokengrant_type=urn:okta:params:oauth:grant-type:webauthn, client_id, auth_session, authn_response, plus optional realm/scope/audienceClient credentials in the request body
Enroll — begin (My Account)POST /me/v1/authentication-methods body { "type": "passkey" }returns auth_session + authn_params_public_keyBearer token, scope create:me:authentication_methods, audience https://{yourDomain}/me/
Enroll — verify (My Account)POST /me/v1/authentication-methods/{id}/verifyauth_session, authn_responseSame Bearer token

Both challenge endpoints return authn_params_public_key (the WebAuthn PublicKeyCredential*Options) plus an auth_session. The connection is passed as realm, not a separate connection field. An organization parameter on /passkey/challenge is not documented — treat multi-org passkey login as SDK-specific where a leaf exposes it.

Enrollment (My Account API)

Letting an *already-authenticated* user add a passkey uses the My Account API (the two enroll rows above), not the passkey grant:

  • Requires an access token with the create:me:authentication_methods scope, minted for the https://{yourDomain}/me/ audience.
  • Requires a Multi-Resource Refresh Token (MRRT) policy so the app can obtain that token alongside its API token.
  • Two steps: POST /me/v1/authentication-methods {type:"passkey"} for the challenge → run the WebAuthn *registration* ceremony → POST /me/v1/authentication-methods/{id}/verify.

Only some SDKs expose enrollment directly (nextjs-auth0, react-native-auth0, Auth0.swift); their leaf files document it. For the others, enrollment is a hosted-portal concern — see feature-universal-portals.

MFA interplay and errors

A passkey login can still hit MFA if the tenant/connection requires a second factor. The token exchange surfaces this as an mfa_required error (or an MfaRequiredError in the SDK) carrying an mfa_token — continue with the MFA flow documented in feature-mfa (do not re-implement it here).

ErrorCauseHandling
mfa_required / MfaRequiredErrorThe tenant requires a second factor after the passkeyContinue with the MFA API flow (feature-mfa) using the mfa_token
passkey challenge / registration errorUser cancelled, no authenticator, or rpId/domain mismatchSurface a retry; confirm the custom domain matches the rpId
login_required on a later silent token callThe passkey flow created no session cookie and refresh tokens were not enabledConfigure useRefreshTokens (SPA SDKs) — see the leaf

Example code snippets

Before writing passkey code: find the detected SDK's row below and `Read:` the file in its Reference column (path relative to the skill root). That file has the SDK's exact method/option names, return-type shapes, and the minimum version. It is the trusted source — implement directly from it and do NOT re-verify signatures by grepping node_modules, reading .d.ts/source, fetching GitHub, or web-searching. No matching row (an SDK not listed)? Passkeys are not supported there via a dedicated SDK API — fall back to hosted Universal Login passkeys.

Min version is the earliest release where the passkey API shipped; verify — X+ marks a version to confirm against the installed package.

SDKMin versionFlow(s)Reference (Read this file)
@auth0/auth0-react2.18.0signup, loginreferences/feature-passkeys/auth0-react.md
@auth0/auth0-vue2.8.0signup, loginreferences/feature-passkeys/auth0-vue.md
@auth0/auth0-angular2.10.0signup, loginreferences/feature-passkeys/auth0-angular.md
@auth0/auth0-spa-js2.21.0signup, loginreferences/feature-passkeys/auth0-spa-js.md
@auth0/nextjs-auth04.22.0signup, login, enrollmentreferences/feature-passkeys/nextjs-auth0.md
@auth0/auth0-server-js1.7.0signup, loginreferences/feature-passkeys/auth0-server-js.md
@auth0/auth0-auth-js1.7.0signup, loginreferences/feature-passkeys/auth0-auth-js.md
auth0-server-python1.0.0b13signup, loginreferences/feature-passkeys/auth0-server-python.md
Auth0.swift (iOS/macOS/visionOS)2.12.0 · 2.13.0 (enrollment)signup, login, enrollmentreferences/feature-passkeys/auth0-swift.md
Auth0.Android3.2.0signup, loginreferences/feature-passkeys/auth0-android.md
react-native-auth05.7.0 (native) · 5.11.0 (web)signup, login, enrollmentreferences/feature-passkeys/react-native-auth0.md

Common mistakes

MistakeWhy it breaksCorrect approach
No custom domain (using *.auth0.com)Passkeys are bound to the rpId; the default domain cannot be a relying partyConfigure and verify a custom domain first
Passkey grant not enabled on the appThe token exchange is rejectedEnable urn:okta:params:oauth:grant-type:webauthn on the application
SPA SDK without useRefreshTokensPasskey flow creates no session cookie, so getAccessTokenSilently() later fails login_requiredConfigure useRefreshTokens: true (+ Refresh Token Rotation) on SPA SDKs
Using a public client for the server token exchangeServer SDK passkey token exchange requires client authenticationUse a confidential client (clientSecret / private-key JWT / mTLS) per the leaf
Treating a passkey as a second factorPasskeys here are the primary credential; the MFA reference covers the second-factor caseFor step-up, use feature-mfa; for passwordless primary login, use this reference
Reusing a signup challenge for login (or vice-versa)Registration and assertion are different ceremoniesUse the signup challenge with create() and the login challenge with get()
Returning the token-exchange result (raw tokens / server-side session state) in the HTTP response, or logging itLeaks Auth0 access / ID / refresh tokens to the clientLet the SDK persist the session in its state store; respond with success/redirect only and read claims server-side
Swapping in @auth0/auth0-react / @auth0/auth0-spa-js (or hand-rolled /me/v1 fetches) for the web build of a React Native appA different session/token model from react-native-auth0; splits the flow across two SDKsKeep react-native-auth0 (5.11.0+ for web) on every platform; branch only the WebAuthn ceremony (navigator.credentials on web). See react-native-auth0.md → "react-native-web — one SDK, branch only the ceremony"

References

Run auth0 docs search "passkeys" for the latest Auth0 docs on this topic.

Auth0 passkeys documentation WebAuthn / passkey grant


Subgroup 4: B2B SaaS Organizations & Self-Service Portals

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.


When to use Organizations

  • Use Organizations when you need:
  • Multiple business customers (tenants), each with their own users and SSO
  • Per-org user roles and permissions
  • 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.


Concepts

ConceptDescription
OrganizationAn isolated tenant within your Auth0 tenant. Has an id (org_xxx) and name (slug).
MemberA user belonging to an organization. A user can belong to multiple orgs.
Org-level roleA role granted to a user within a specific org (not globally).
ConnectionA login method enabled for an org (database, enterprise SSO, social).
InvitationA time-limited invite to join an org, sent by email.

SDK Integration

Pass organization at login

The org-login shape is protocol-level: send the organization identifier on the /authorize request, then read org_id back off the returned token. Only how the SDK takes it differs - get the exact call from the loaded `framework-{framework}/index.md` reference, not from memory, preferring the most specific form:

  • A dedicated `organization` option (e.g. startInteractiveLogin({ organization: 'org_xxx' }),
  • .organization("org_xxx"), a client-level default). Prefer this whenever the SDK has one -
  • it is the canonical form; the authorization-params bag below is a backwards-compatible fallback.
  • Inside the authorization parameters (e.g. loginWithRedirect({ authorizationParams: { organization: 'org_xxx' } })).
  • For SDKs with no dedicated option (common for SPA/mobile) this is itself canonical.
  • As a URL/query param the handler forwards (e.g. /login?organization=org_xxx).

Match the detected SDK to the form its own reference documents; do not infer it from the platform.

Get the exact call from the per-SDK reference below — `Read:` the file for the detected SDK. Each carries that SDK's exact org-login option, invitation forwarding, and org_id accessor, verified against the installed SDK. Implement directly from it: do NOT fetch from GitHub, grep node_modules/.d.ts/site-packages, or web-search to re-verify a signature. Min version is the release the feature landed in — check the installed version (`package.json`/lockfile, `Package.swift`, Gradle, `pyproject.toml`) meets it before implementing, and upgrade if it falls short. It matters most for the newer server SDKs (auth0-server-js, auth0-auth-js, auth0-api-js, auth0-server-python), where organizations lands mid-1.x, so an older install won't have it. No matching row? Fall back to the protocol shape above plus the loaded framework-{framework}/index.md. Never hand-roll the authorize URL or decode the token by hand.

Wire the change into the project's existing files. Edit the app's real login/callback/route and config files in place; do not scaffold extra README, SETUP, NOTES, CHECKLIST, or *-summary documents to "explain" the integration - they are not part of the task and dilute the diff. Keep the change minimal and focused on what makes org login, invitation acceptance, and org_id enforcement work. Verify your edits by re-reading the files you changed - the task workspace is not guaranteed to be a git repository, so git diff/git status may just error.

SDKMin versionRead this file
@auth0/auth0-react2.xRead: references/feature-organizations/auth0-react.md
@auth0/auth0-spa-js2.xRead: references/feature-organizations/auth0-spa-js.md
@auth0/auth0-vue2.xRead: references/feature-organizations/auth0-vue.md
@auth0/auth0-angular2.xRead: references/feature-organizations/auth0-angular.md
@auth0/nextjs-auth04.xRead: references/feature-organizations/nextjs-auth0.md
express-openid-connect2.x/3.xRead: references/feature-organizations/express-oidc.md
react-native-auth05.xRead: references/feature-organizations/react-native-auth0.md
Auth0.swift2.x/3.xRead: references/feature-organizations/auth0-swift.md
Auth0.Android2.x–4.xRead: references/feature-organizations/auth0-android.md
Auth0.OidcClient.* (.NET WPF/WinForms/UWP/MAUI/AndroidX/iOS)3.2.0+ (Core)Read: references/feature-organizations/auth0-oidc-client-net.md
@auth0/auth0-server-js1.9.0Read: references/feature-organizations/auth0-server-js.md
@auth0/auth0-auth-js1.10.0Read: references/feature-organizations/auth0-auth-js.md
auth0-server-python1.0.0b11Read: references/feature-organizations/auth0-server-python.md
@auth0/auth0-api-js (API)1.3.0Read: references/feature-organizations/auth0-api-js.md
express-oauth2-jwt-bearer (API)1.0.0Read: references/feature-organizations/express-oauth2-jwt-bearer.md
go-jwt-middleware (API, Go)v3.3.0Read: references/feature-organizations/go-jwt-middleware.md
auth0-api-python (API, Python)1.0.0b4Read: references/feature-organizations/auth0-api-python.md
Auth0.AspNetCore.Authentication.Api (API, .NET)1.0.0Read: references/feature-organizations/aspnetcore-api.md

Reading the organization back

org_id (and org_name, when your tenant uses organization names) is present in both the ID token and the access token after an organization login. Which one you read depends on *why* you need it:

  • To display which org the user is in (client / web app): read org_id /
  • org_name from the ID token. Auth0's guidance is that web applications
  • validate org_id from the ID token. Use the SDK's own claim accessor rather
  • than hand-decoding a token.
  • To authorize an API request (server side): validate org_id from the
  • access token the API receives (see below).

Validate org on the backend

What you do here depends on the app type - applying the wrong one is a real defect:

  • A resource API validating access tokens: enforce org_id on the *verified access token*. Per
  • Auth0's guidance,
  • check it against a known list of the org IDs the API serves (or the org implied by request
  • context) *and* segment data by org_id. A valid token proves membership in *some* org, not that
  • it is one your API serves. Read the detected SDK's file from the per-SDK table above for the exact
  • enforcement call.
  • A login / session app: read org_id from the session / ID token to display the org and to
  • segment data by it. Do not add a post-login allow-list that rejects a signed-in user whose
  • org_id is not the default - the user already authenticated into that org, so gating the session
  • or its routes 403s anyone who accepted an invitation to another org.

Wherever a known list is used, source it from your org records and keep it in sync with every org served - a list seeded with only the default org 403s the first invited member.


Tenant Configuration (via chosen tooling)

The Auth0 MCP server exposes no organizations tool, so use the CLI or Terraform (full command syntax lives in your tooling reference).

When the task is to configure the tenant itself (not wire an SDK into an app), the deliverable is the mutated tenant, not a guide to it. Run each auth0 command directly as its own step, and read state back after each mutation to confirm it landed. Do not bundle the whole setup into a single script you run once (bash setup.sh), and do not emit setup scripts, README, or summary files describing commands for a human to run later - a wrapped or unrun script leaves the tenant unchanged and hides which step failed.

OperationCLITerraform
Create an organizationauth0 orgs create --name <slug> --display "<Name>"auth0_organization
List / show / update / deleteauth0 orgs list / show / update / deleteauth0_organization
Add a memberauth0 api post "organizations/<org-id>/members" --data '{"members":["<user-id>"]}'auth0_organization_member
Enable a connectionauth0 api post "organizations/<org-id>/enabled_connections" --data '{"connection_id":"<con-id>","assign_membership_on_login":true}'auth0_organization_connections
Assign an org-scoped roleauth0 api post "organizations/<org-id>/members/<user-id>/roles" --data '{"roles":["<role-id>"]}'auth0_organization_member_roles
Create an invitationauth0 orgs invitations create (see below)not covered

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[]'.

Application (client) organization settings

Two settings control whether and how an app uses organization login. They live on the application (client), not on the organization, and are set with auth0 apps update <client-id> --organization-usage ... --organization-require-behavior ...:

FieldValuesMeaning
organization_usagedeny / allow / requireWhether a login may (allow) or must (require) carry an organization. require forbids any login without one
organization_require_behaviorno_prompt / pre_login_prompt / post_login_promptHow the org is resolved when one is required. pre_login_prompt makes the user pick the org before entering credentials

Map the intent to the pair - do not assume the allow/no_prompt invitation default (below) is the only option:

  • "No login outside an organization" -> organization_usage: require
  • "Choose the organization up front, before credentials" -> organization_require_behavior: pre_login_prompt
  • Accept invitations, but org is otherwise optional -> organization_usage: allow with organization_require_behavior: no_prompt
bash
# Org-only login with an up-front org selector - set both fields in one call.
auth0 apps update <client-id> \
  --organization-usage require --organization-require-behavior pre_login_prompt

auth0 apps update and auth0 apps show <client-id> leave client_secret out of their output unless you pass --reveal-secrets, so the secret stays out of your context.

Finding or creating a login connection

Reuse an existing database connection when the tenant has one; create one only if it does not:

bash
# Look up a database connection by name. Filtering on `name` returns the exact match -
# the API defines no ordering, so `.[0]` on an unfiltered list grabs an arbitrary connection.
auth0 connections list --query '{"strategy":["auth0"],"name":"<connection-name>"}' --json | jq -r '.[].id'

Create one only if there is none matching. `name` must match

# ^a-zA-Z0-9*$, max 128 chars. auth0 connections create --data '{"name":"<connection-name>","strategy":"auth0"}'

Enable it for the organization - without this, org members have no way to log in.

auth0 api post "organizations/<org-id>/enabled_connections" \ --data '{"connection_id":"<con-id>","assign_membership_on_login":true}'

Check which apps already have this connection enabled.

auth0 connections enabled-clients show <con-id> --json | jq -r '.[].client_id'

ONLY if the app is not already listed above: enable the connection for it.

# This is a separate setting from org login (see note below), so skip it on a # connection the tenant already had enabled for the app. status false disables; max 50 per call. # This is a PATCH: it changes only the clients in the payload and keeps every other # client's status, so a single entry does not replace the existing list. auth0 connections enabled-clients update <con-id> \ --data '[{"client_id":"<client-id>","status":true}]' ```

enabled-clients show returns every enabled client, across pages. Fail unless exactly one connection matches the name rather than guessing.

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/<con-id>/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.


Invitation flow

An invitation lets you add a user who has no Auth0 account yet. The invitee gets a link, authenticates, and becomes a member.

Two prerequisites, each a hard 400. Do both before the first invitations create call:

bash
# 1. Without this: "The specified client_id (...) does not allow organizations."
#    allow/no_prompt is the minimum for invitations; for org-only login use
#    require/pre_login_prompt instead (see "Application (client) organization settings").
auth0 apps update <client-id> \
  --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 # to restore it. An empty response means it's currently unset. auth0 tenant-settings show --json | jq -r '.default_redirection_uri // ""'

auth0 api patch "tenants/settings" \ --data '{"default_redirection_uri":"https://app.example.com/callback"}' ```

The tenant setting is default_redirection_uri, validated as absolute-https-uri-or-empty: it must be https, and localhost is rejected on 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 literal text "unset", if it was empty before.

If you keep the new value, say so in your summary. Silently repointing a shared tenant setting is the kind of change someone else has to debug.

bash
auth0 orgs invitations create --org-id "<org-id>" \
  --invitee-email "user@company.com" --inviter-name "Admin" \
  --client-id "<client-id>" --roles "<role-id>" --send-email=false

--send-email defaults to `true`, and it needs the = form, since --send-email false reads false as a positional argument. Verify with auth0 orgs invitations list --org-id <org-id>.

Accepting an invitation (app side)

The invite link lands on your app carrying both an invitation and an organization parameter:

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:

  • 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.

  • Read invitation + organization off the login request and forward both; only fall back to the default org when no organization is present.
  • If you accept cross-org invitations, pass organization per login call - a client-wide default org is validated against the returned org_id at login completion and rejects invites to other orgs.

Common mistakes

MistakeFix
Not passing organization at loginPass the org identifier via the SDK's dedicated organization option when it has one (preferred); only fall back to the authorization-parameters bag for SDKs that expose no dedicated option
Not forwarding the invitation param when accepting an inviteRead invitation + organization from the callback URL and forward both to /authorize
Using your default org for an invitation linkForward the invite's own organization param - it may differ from your configured default
Login route overwriting an incoming organization with the defaultForward an organization present on the request; only fall back to the default when none is supplied
Pinning a client-wide default org while accepting cross-org invitationsA client-level organization is validated against the returned org_id at login completion, rejecting invites to other orgs. Pass organization per login call instead
Reading org_id from the wrong tokenWeb/client apps read it from the ID token (display); APIs validate it from the access token (authorization)
Validating org_id against a single hardcoded org on the backendValidate against the set of orgs the request may serve - a known list, or the org derived from request context. A fixed !== defaultOrg check rejects valid members of other orgs
Gating a login/session app on an org allow-listThe allow-list is an access-token check for APIs, not a session gate. A signed-in user already authenticated into their org; rejecting a non-default org_id blocks invited members. Read org_id from the session and segment data by it
On an API, an allow-list seeded with only the default orgKeep the served-org list sourced from your org records and in sync with every org served; a list holding just the default 403s the first invited member
Comparing org_id against an env var that can be undefinedGuarantee the required org id: give it a literal fallback (process.env.ACME_ORG_ID ?? 'org_xxx') or validate env vars at startup. An unset var makes the check compare against undefined and silently breaks enforcement
Scaffolding extra README/SETUP/summary files for the integrationWire the change into the project's existing login/callback/route/config files; keep the diff minimal
Hand-decoding a token to read org_idUse 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 rolesUse the org member roles endpoint, not the user roles endpoint
Not enabling a connection for the orgauth0 api post "organizations/<org-id>/enabled_connections", or Dashboard → Organization → Connections
A space or underscore in a new connection's nameAlphanumerics and hyphens only, starting and ending alphanumeric. Anything else is a 400
Creating a connection and enabling it for no appNothing can use it. auth0 connections enabled-clients update <con-id> --data '[{"client_id":"<client-id>","status":true}]'
Reading or writing enabled_clients on the connection object"NOT RECOMMENDED" on write, deprecated on read. Read with auth0 connections enabled-clients show <con-id> and write with auth0 connections enabled-clients update <con-id>, which sends a PATCH to connections/<con-id>/clients
Reading or setting a client field with auth0 apiUse auth0 apps show <client-id> or auth0 apps update <client-id>, which leave client_secret out unless you pass --reveal-secrets. For a raw call, pipe through jq to keep only the fields you need
Overwriting default_redirection_uri without reading it firstIt is tenant-wide. Capture the old value, and restore or disclose it
Guessing a auth0 orgs subcommand for membership, roles, or connectionsVerify with auth0 commands orgs --detailed, and use auth0 api post organizations/... for whatever has no dedicated subcommand
Prefixing auth0 api paths with /api/v2/Paths are relative to the API root. /api/v2/organizations/... returns 404
Inviting before setting organization_usage on the app and default_redirection_uri on the tenantBoth are hard 400s. Configure them first (see Invitation flow)
Leaving organization_usage at allow when the app must reject non-org loginsallow permits both; use require to forbid any login without an organization, with pre_login_prompt to choose the org before credentials
Letting auth0 orgs invitations create send a live email--send-email defaults to true. Pass --send-email=false

Multi-tenant architecture

For broader B2B SaaS architecture guidance (tenant isolation models, when to use one Auth0 organization per customer vs. shared connections), the router loads the multi-tenant pattern guidance alongside this file for architecture questions.

References

Run auth0 docs search "organizations" for the latest Auth0 docs on this topic.


Subgroup 5: Security, DPoP & Token Exchange

Auth0 DPoP (Device-Bound Tokens)

Bind access tokens to the client's cryptographic key so stolen tokens cannot be replayed.


Overview

What is DPoP?

DPoP (Demonstrating Proof-of-Possession) is an OAuth 2.0 mechanism defined in RFC 9449 that cryptographically binds access tokens to a client-held key pair. Each API request includes a short-lived signed JWT (the DPoP proof) that proves the sender holds the private key — a stolen token alone cannot be replayed by an attacker.

When to Use DPoP

  • Protecting high-value API calls against token theft and replay attacks
  • Meeting security or compliance requirements that mandate sender-constrained tokens
  • Any SPA or Vanilla JS app calling a protected Auth0 API with elevated security needs

When NOT to Use DPoP

  • SSR / server-side environments — DPoP relies on a private key held in the browser; it cannot be safely used server-side (Next.js, Nuxt, etc.)
  • APIs that don't support DPoP — the resource server must be configured to accept DPoP token dialect; Bearer-only APIs will reject DPoP proofs
  • Flows requiring token sharing — DPoP tokens are bound to a single key pair and cannot be forwarded to or reused by another client

Requirements

  • Auth0 tenant with DPoP-capable authorization server
  • API resource server with DPoP token dialect enabled
  • A browser SPA using one of: @auth0/auth0-vue, @auth0/auth0-react,
  • @auth0/auth0-angular, or @auth0/auth0-spa-js
  • HTTPS in production (required by Auth0 for DPoP)

Key Concepts

ConceptDescription
DPoP ProofA short-lived signed JWT attached to each request proving key possession
DPoP NonceA server-issued value that must be included in the proof to prevent replay
useDpop: trueSDK option that enables automatic DPoP proof generation
createFetcher()SDK helper that returns a fetch-compatible function handling proofs automatically
UseDpopNonceErrorError thrown when the server rotates its nonce mid-flight; retry with the new nonce

Step 1: Enable DPoP on Your API

Via Auth0 Dashboard

  1. Go to Applications → APIs
  2. Select the API your SPA calls
  3. Under the Settings tab, confirm the API identifier matches your audience
  4. No additional toggle is needed in the dashboard — DPoP is enabled per-request
  5. by the client when the API resource server is configured to accept DPoP tokens

Via Auth0 CLI

bash
# Inspect current resource server settings
auth0 apis list | jq '.[] | select(.identifier == "https://your-api-identifier")'

Enable DPoP token dialect on the API

auth0 apis update {API_ID} \ --data '{"token_dialect": "access_token_authz"}' ```

Replace {API_ID} with the ID returned from the GET call above.

Same configuration in Terraform / MCP

  • Sender-constraining is set on the resource server (via a proof_of_possession
  • setting with mechanism: "dpop") and, for a mandatory client, on the client.
  • There is no tenant-wide DPoP toggle. The loaded tooling reference has the exact
  • syntax:
  • CLI: auth0 apis update {API_ID} with the proof_of_possession payload
  • Terraform: auth0_resource_server proof_of_possession block + auth0_client require_proof_of_possession
  • MCP: auth0_update_resource_server accepts proof_of_possession; the client-side toggle is not exposed by MCP — set it via CLI or Terraform.

Step 2: Configure Your Application

Common pattern across all frameworks

  1. Add useDpop: true to your Auth0 client/provider configuration alongside your audience
  2. Use createFetcher() instead of attaching tokens manually — the SDK handles
  3. proof generation, nonce management, and header injection for you
  4. Handle UseDpopNonceError in cases where the server rotates its nonce

Environment variables

Ensure your .env includes the API audience:

bash
# Vite
VITE_AUTH0_DOMAIN=your-tenant.auth0.com
VITE_AUTH0_CLIENT_ID=your-client-id
VITE_AUTH0_AUDIENCE=https://your-api-identifier

Framework Examples

Vue.js

Uses @auth0/auth0-vue.

1. Enable DPoP in plugin config

typescript
// src/main.ts
import { createApp } from 'vue';
import { createAuth0 } from '@auth0/auth0-vue';
import App from './App.vue';

const app = createApp(App);

app.use( createAuth0({ domain: import.meta.env.VITE_AUTH0_DOMAIN, clientId: import.meta.env.VITE_AUTH0_CLIENT_ID, authorizationParams: { redirect_uri: window.location.origin, audience: import.meta.env.VITE_AUTH0_AUDIENCE }, useDpop: true }) );

app.mount('#app'); ```

2. Make DPoP-protected API calls

vue
<script setup lang="ts">
import { ref } from 'vue';
import { useAuth0, UseDpopNonceError } from '@auth0/auth0-vue';

const { createFetcher } = useAuth0(); const data = ref(null); const error = ref<string | null>(null);

// Create a DPoP-aware fetcher bound to your API base URL const apiFetch = createFetcher({ baseUrl: 'https://your-api.example.com' });

const fetchData = async () => { error.value = null; try { const response = await apiFetch('/data'); data.value = await response.json(); } catch (err) { if (err instanceof UseDpopNonceError) { // Server rotated its nonce — retry once const response = await apiFetch('/data'); data.value = await response.json(); } else { error.value = (err as Error).message; } } }; </script>

<template> <div> <button @click="fetchData">Fetch Data</button> <div v-if="error" class="error">{{ error }}</div> <pre v-if="data">{{ JSON.stringify(data, null, 2) }}</pre> </div> </template> ```

3. POST / PUT / DELETE requests

typescript
// Pass standard fetch options as the second argument
const response = await apiFetch('/items', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'example' })
});

4. Multiple APIs with separate fetchers

typescript
const { createFetcher } = useAuth0();

const ordersApi = createFetcher({ baseUrl: 'https://orders.example.com' }); const inventoryApi = createFetcher({ baseUrl: 'https://inventory.example.com' }); ```

5. Manual DPoP management (advanced)

typescript
import { useAuth0 } from '@auth0/auth0-vue';

const { generateDpopProof, getDpopNonce, setDpopNonce } = useAuth0();

// Generate a proof manually (e.g. for a custom fetch wrapper) const proof = await generateDpopProof({ url: 'https://your-api.example.com/data', method: 'GET', nonce: getDpopNonce() });

const response = await fetch('https://your-api.example.com/data', { headers: { Authorization: DPoP ${accessToken}, DPoP: proof } });

// Store a server-issued nonce for subsequent requests const newNonce = response.headers.get('DPoP-Nonce'); if (newNonce) setDpopNonce(newNonce); ```


React

Uses @auth0/auth0-react.

1. Enable DPoP in provider config

tsx
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Auth0Provider } from '@auth0/auth0-react';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <Auth0Provider domain={import.meta.env.VITE_AUTH0_DOMAIN} clientId={import.meta.env.VITE_AUTH0_CLIENT_ID} authorizationParams={{ redirect_uri: window.location.origin, audience: import.meta.env.VITE_AUTH0_AUDIENCE }} useDpop={true} > <App /> </Auth0Provider> </React.StrictMode> ); ```

2. Make DPoP-protected API calls

tsx
import { useAuth0, UseDpopNonceError } from '@auth0/auth0-react';
import { useState, useMemo } from 'react';

export function DataFetcher() { const { createFetcher } = useAuth0(); const [data, setData] = useState(null); const [error, setError] = useState<string | null>(null);

const apiFetch = useMemo( () => createFetcher({ baseUrl: 'https://your-api.example.com' }), [createFetcher] );

const fetchData = async () => { setError(null); try { const response = await apiFetch('/data'); setData(await response.json()); } catch (err) { if (err instanceof UseDpopNonceError) { // Server rotated its nonce — retry once const response = await apiFetch('/data'); setData(await response.json()); } else { setError((err as Error).message); } } };

return ( <div> <button onClick={fetchData}>Fetch Data</button> {error && <div className="error">{error}</div>} {data && <pre>{JSON.stringify(data, null, 2)}</pre>} </div> ); } ```

3. Memoize the fetcher to avoid re-creation

tsx
import { useAuth0 } from '@auth0/auth0-react';
import { useMemo } from 'react';

export function useApiClient() { const { createFetcher } = useAuth0();

return useMemo( () => createFetcher({ baseUrl: 'https://your-api.example.com' }), [createFetcher] ); } ```

4. POST / PUT / DELETE requests

typescript
const response = await apiFetch('/items', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'example' })
});

Angular

Uses @auth0/auth0-angular.

1. Enable DPoP in provider config

Standalone components (Angular 14+):

typescript
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideAuth0 } from '@auth0/auth0-angular';
import { environment } from '../environments/environment';

export const appConfig: ApplicationConfig = { providers: [ provideAuth0({ domain: environment.auth0.domain, clientId: environment.auth0.clientId, authorizationParams: { redirect_uri: window.location.origin, audience: environment.auth0.audience }, useDpop: true }) ] }; ```

NgModule-based apps:

typescript
// src/app/app.module.ts
import { AuthModule } from '@auth0/auth0-angular';

@NgModule({ imports: [ AuthModule.forRoot({ domain: environment.auth0.domain, clientId: environment.auth0.clientId, authorizationParams: { redirect_uri: window.location.origin, audience: environment.auth0.audience }, useDpop: true }) ] }) export class AppModule {} ```

2. Make DPoP-protected API calls

typescript
import { Component, inject, signal } from '@angular/core';
import { AuthService, UseDpopNonceError } from '@auth0/auth0-angular';

@Component({ selector: 'app-data', template: <button (click)="fetchData()">Fetch Data</button> <div *ngIf="error()">{{ error() }}</div> <pre *ngIf="data()">{{ data() | json }}</pre> }) export class DataComponent { private auth = inject(AuthService); data = signal<unknown>(null); error = signal<string | null>(null);

// Created once — reused across all fetchData() calls so nonce state is preserved private apiFetch = this.auth.createFetcher({ baseUrl: 'https://your-api.example.com' });

async fetchData() { this.error.set(null); try { const response = await this.apiFetch('/data'); this.data.set(await response.json()); } catch (err) { if (err instanceof UseDpopNonceError) { // Server rotated its nonce — retry once const response = await this.apiFetch('/data'); this.data.set(await response.json()); } else { this.error.set((err as Error).message); } } } } ```

3. POST / PUT / DELETE requests

typescript
// Uses the same class-field fetcher — pass standard fetch options as the second argument
const response = await this.apiFetch('/items', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'example' })
});

auth0-spa-js (Vanilla JS)

Uses @auth0/auth0-spa-js directly — suitable for Vanilla JS, Svelte, SolidJS, or any SPA not using a framework wrapper.

1. Initialize client with DPoP enabled

typescript
import { createAuth0Client, UseDpopNonceError } from '@auth0/auth0-spa-js';

const auth0 = await createAuth0Client({ domain: import.meta.env.VITE_AUTH0_DOMAIN, clientId: import.meta.env.VITE_AUTH0_CLIENT_ID, authorizationParams: { redirect_uri: window.location.origin, audience: import.meta.env.VITE_AUTH0_AUDIENCE }, useDpop: true });

// Handle redirect callback after login const query = new URLSearchParams(window.location.search); if ((query.has('code') || query.has('error')) && query.has('state')) { await auth0.handleRedirectCallback(); window.history.replaceState({}, document.title, window.location.pathname); } ```

2. Make DPoP-protected API calls

typescript
// Create a DPoP-aware fetcher
const apiFetch = auth0.createFetcher({
  baseUrl: 'https://your-api.example.com'
});

async function fetchData() { try { const response = await apiFetch('/data'); return await response.json(); } catch (err) { if (err instanceof UseDpopNonceError) { // Server rotated its nonce — retry once const response = await apiFetch('/data'); return await response.json(); } throw err; } } ```

3. POST / PUT / DELETE requests

typescript
const response = await apiFetch('/items', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'example' })
});

4. Multiple APIs with separate fetchers

typescript
const ordersApi = auth0.createFetcher({ baseUrl: 'https://orders.example.com' });
const inventoryApi = auth0.createFetcher({ baseUrl: 'https://inventory.example.com' });

const [orders, inventory] = await Promise.all([ ordersApi('/list').then(r => r.json()), inventoryApi('/stock').then(r => r.json()) ]); ```


Error Handling

`UseDpopNonceError`

Servers may rotate their DPoP nonce. When this happens the SDK throws UseDpopNonceError. Retry the request once — the SDK will have updated the stored nonce automatically:

typescript
// Import from your framework SDK:
// @auth0/auth0-vue | @auth0/auth0-react | @auth0/auth0-angular | @auth0/auth0-spa-js
import { UseDpopNonceError } from '@auth0/auth0-vue';

try { const response = await apiFetch('/data'); const data = await response.json(); } catch (err) { if (err instanceof UseDpopNonceError) { // Nonce was stale — retry once; SDK has already stored the new nonce const response = await apiFetch('/data'); const data = await response.json(); } else { throw err; } } ```


Common Issues

IssueCauseFix
API returns 401 with error: use_dpop_nonceServer issued a new nonceCatch UseDpopNonceError and retry
API returns 401 with invalid_dpop_proofClock skew or wrong htm/htu valuesEnsure system clock is accurate; verify baseUrl matches API URL exactly
Token still issued as Bearer instead of DPoPuseDpop: true missing or audience not setConfirm both options are present in client config
createFetcher is undefinedSDK version too oldUpgrade to @auth0/auth0-spa-js ≥ 2.1 (or framework SDK wrapping it)

Subgroup 6: Tenant Health, CheckMate Audit & Migration

Auth0 tenant audit (CheckMate) — full workflow

Use this reference when the user wants to run an Auth0 tenant configuration / security audit and produce a tenant configuration report (Auth0 Platform Development Review format — markdown + styled PDF) tailored to their product context, or wants to apply the audit's recommendations. It bootstraps the Auth0 CLI and a dedicated audit M2M application if needed, gathers lightweight public company context so findings read in the customer's own terms, and implements approved fixes via the Auth0 CLI with per-command confirmation.

You produce a personalized tenant audit as a markdown brief plus styled PDF, walk the user through the recommendations as two ordered loops (free-now vs after-upgrading), and apply approved fixes via the Auth0 CLI with per-command confirmation.

The audit is run with CheckMate (@auth0/auth0-checkmate), a Node CLI that calls the Auth0 Management API and emits a JSON (and optionally PDF) report. The Auth0 CLI (auth0) is what you use both to bootstrap CheckMate's M2M app and to apply the recommendations.

For the feature→plan matrix (Free/Essentials/Professional/Enterprise, B2C + B2B) that is the source of truth for triaging findings into Immediate vs After-Upgrading, use the co-loaded pricing reference. For the step-by-step remediation flows (command-shape mapping, guided/batch flows, the never-without-confirmation list), use the co-loaded remediation reference — it is what the two apply loops (Phase 6) hand off to. The markdown report skeleton this audit's brief MUST follow lives at assets/audit/report-template.md; the matching HTML skeleton (CSS-styled, used to render the PDF) lives at assets/audit/report-template.html. The PDF is rendered from the HTML by the script at the skill's scripts/render_pdf.sh (invoke it as ${CLAUDE_SKILL_DIR}/scripts/render_pdf.sh). Company context is gathered inline in Phase 3 — no external skill required. Tenant-configuration CLI usage beyond what's inlined below (full command reference, flags, output formatting) is covered by the tooling reference loaded alongside this one.

Chat output rules (terse mode)

  • This workflow runs many phases. Keep chat tight to make the run feel fast:
  • One status line per phase max — e.g. Phase 1 ✓ CLI ready · tenant: acme.us.auth0.com. No paragraph narration of what you're about to do.
  • Use ✓ for success, ⚠ for warnings (will fix automatically), ✗ for errors that need user input.
  • Show full detail only when (a) something fails, (b) the user must answer a question, or (c) Phase 5 chat summary at end.
  • Don't restate what's in the report files — point to the file path and stop.
  • Final-run summary may be 3-5 lines (top findings, applied count, queued count, file paths).

Tool-aware execution (agent portability)

This workflow targets Claude Code first but works on any agent that can run shell commands and read files. It references several tools by their Claude Code names; map each to your agent's equivalent:

Reference in this workflowClaude CodeOther agents (Codex, Cursor, Aider, Continue, Cline, etc.)
AskUserQuestion (mode selector, per-finding triage, confirmations)Native multi-choice button UIAsk the user as plain text with explicit options — e.g. "Reply: express / guided / audit" or "Apply this fix? yes / queue / skip". Same content, plain prompt.
TodoWrite (progress tracking)Native todo list toolUse your agent's progress mechanism, or skip — progress tracking is optional.
${CLAUDE_SKILL_DIR} env var (path to skill folder)Auto-set by Claude CodeSubstitute the absolute path to wherever the customer extracted the skill.

State files for this workflow live at ~/.auth0-checkmate/state/ regardless of agent (vendor-neutral).

Phase 0 — Pre-flight + mode

State cache lives at ~/.auth0-checkmate/state/setup.json (create dir on first write): ``json { "schema_version": 1, "tenant_domain": "tenant.region.auth0.com", "checkmate_client_id": "...", "company_domain": "example.com", "mode": "guided", "last_run_at": "2026-06-01T12:34:56Z" } ` mode is "express", "guided", or "audit" (set in Phase 0.3, gates Phases 6-7). **Treat the state file as a cache, not source of truth.** Re-validate each field (Phase 1.1, Phase 2.1) before trusting it. Secrets (the M2M client_secret) live in ~/.auth0-checkmate.env` (mode 600) — never write secrets to the state file.

0.1 Pre-flight diagnostic (silent → one-line status table)

Run all checks without prompting, then render a single compact status block. Keep it under 8 lines.

Checks to run in parallel (no chat output during checks): | Check | Command | Result | |---|---|---| | CLI installed | auth0 --version | version or missing | | Tenant session | auth0 tenants list --json | first tenant or none | | CheckMate app | auth0 apps list --json \| jq '.[]\|select(.name\|test("CheckMate";"i"))' | client_id or none | | M2M scopes | auth0 client-grants list --client-id <id> | scope count or none | | CheckMate npm | command -v a0checkmate | path or missing | | Node version | node -v | version or missing | | Reviewer | read state/operator.json | name or missing | | Company domain | read state/setup.json.company_domain | domain or missing | | Enrichment cache | newest state/enrichment_*.json | age or missing |

Render to chat: `` auth0-checkmate pre-flight: ✓ Auth0 CLI v1.8.2 ✓ Logged in to acme.us.auth0.com ✓ CheckMate M2M app present (cm_xxxxxx · 26/26 scopes) ⚠ a0checkmate npm package not installed (will install) ⚠ Company domain not cached (will ask) → Estimated setup: ~30s

Use ✓ (ready), ⚠ (will fix automatically), ✗ (blocking — needs user input). If any ✗, surface the action needed and pause for input.

0.2 Capture missing inputs

If reviewer or company domain is missing, ask in one combined AskUserQuestion (don't split into separate prompts). Save reviewer to state/operator.json (reviewer_name, optional reviewer_org for the report subtitle), company domain to state/setup.json.company_domain.

Skip this step entirely when both are cached.

0.3 Mode selector

Single AskUserQuestion:

How do you want to handle the recommendations? > - Express — produce the report, then apply Immediate Actions in one batch confirmation; per-command confirm only for After-Upgrading items. Fastest. > - Guided — produce the report, then walk through each recommendation with per-command confirmation. Safest. (default) > - Audit only — produce the report and stop. No fixes applied.

Save the chosen mode to state/setup.json.mode. The choice gates Phases 6 + 7 below.

Phase 1 — Auth0 CLI bootstrap

1.1 Verify

bash auth0 --version # CLI installed? auth0 tenants list --json # session active? returns tenants array

  • If --version fails, install per platform:
  • macOS — brew install auth0
  • Linux / macOS (curl) — download the binary with curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh | sh -s -- -b . (it lands in ./auth0; move it onto $PATH with sudo mv ./auth0 /usr/local/bin if you want to run it from any directory)
  • Windows — scoop bucket add auth0 https://github.com/auth0/scoop-auth0-cli.git && scoop install auth0, or via PowerShell: fetch the latest release tag from the GitHub releases API, download auth0-cli_<version>_Windows_x86_64.zip, expand it, and add it to $PATH
  • Any platform with Go ≥ 1.21 — go install github.com/auth0/auth0-cli/cmd/auth0@latest (ensure $GOPATH/bin is on $PATH)
  • Manual — download the release archive for your OS/architecture from the auth0-cli GitHub releases page and extract it; make sure PATH and HOME include the extraction folder

Autocompletion instructions for supported shells are available by running auth0 completion -h.

  • If tenants list returns empty / 401, authenticate. There are two ways:
  • As a user (recommended for personal/interactive machines) — device authorization flow, run auth0 login. Cannot be used for private cloud tenants.
  • As a machine (recommended for servers, CI, or private cloud tenants) — client credentials flow:
  • ```bash
  • auth0 login --domain <domain> --client-id <client-id> --client-secret <client-secret>
  • # or, with a private key instead of a secret:
  • auth0 login --domain <domain> --client-id <client-id> --client-assertion-private-key <path-to-private-key> --client-assertion-signing-alg <signing-algorithm>
  • ```

Critical: the session must carry create:client_grants and read:client_grants so Phase 2's grant call doesn't 403:

bash
auth0 login --scopes "create:client_grants,read:client_grants"

For non-interactive shells (CI, agent without TTY), or private cloud tenants, use the client-credentials login above. Detect TTY before invoking the device flow: ``bash [ -t 0 ] && auth0 login --scopes "create:client_grants,read:client_grants" || echo "Non-interactive: use client-credentials login"

When an access token expires, the CLI prompts to confirm the default tenant (Continue login with default tenant 'your-tenant.auth0.com'? [y/N]) or lets you pick a different one — select y to proceed with the default, N to choose another. Note that CLI usage consumes Management API rate limits according to the subscription plan.

1.2 Pin tenant

Save the tenant domain returned by auth0 tenants list --json (.[] | select(.active) | .name for the active tenant; if several tenants, ask the user to pick) into state/setup.json.tenant_domain. Don't parse region from the domain suffix — use the value the CLI returns. Custom domains (private cloud) are valid here.

This pinned `tenant_domain` is the canonical Auth0-hosted URL. Every report reference to the tenant URL — header right rail, "authentication is served from <tenant_domain>" narrative, CLI commands — reads from state/setup.json.tenant_domain. Never derive it from the company context, never parse it from the company domain. The company context provides company-level fields (name, products, business model, integrations) — never the tenant URL.

Phase 2 — Dedicated CheckMate M2M app

2.1 Detect existing

bash auth0 apps list --json | jq -r '.[] | select(.name|test("CheckMate";"i")) | {client_id, name}'

If a match exists, fetch its current client grant for the Management API audience and diff scopes against the required list (Phase 2.3). Missing scopes → PATCH the existing grant; never delete + recreate (rotates secret silently).

2.2 Create if missing

bash auth0 apps create --name "Auth0 CheckMate" --type m2m \ --description "CheckMate tenant audit (managed by this workflow)" \ --json

The JSON response carries client_id and client_secret. Never echo the secret outside the user's chat or write it to disk other than ~/.auth0-checkmate.env (Phase 2.4).

Free-plan tenants are limited to 1 free M2M app for the Management API. If create fails with a quota error, list existing M2M apps with Management API grants and ask the user which to reuse.

2.3 Authorize Management API scopes (26 read scopes)

read:tenant_settings read:custom_domains read:prompts read:clients
read:connections read:connections_options read:resource_servers
read:client_grants read:roles read:branding read:email_provider
read:email_templates read:phone_providers read:phone_templates
read:shields read:attack_protection read:self_service_profiles
read:guardian_factors read:mfa_policies read:actions read:log_streams
read:logs read:network_acls read:event_streams read:hooks read:rules

Grant the Management API scopes with a client grant: ``bash auth0 client-grants create --client-id "<id>" \ --audience "https://<tenant_domain>/api/v2/" \ --scopes "read:tenant_settings,read:custom_domains,read:prompts,read:clients,read:connections,read:connections_options,read:resource_servers,read:client_grants,read:roles,read:branding,read:email_provider,read:email_templates,read:phone_providers,read:phone_templates,read:shields,read:attack_protection,read:self_service_profiles,read:guardian_factors,read:mfa_policies,read:actions,read:log_streams,read:logs,read:network_acls,read:event_streams,read:hooks,read:rules"

Some tenants reject read:hooks / read:rules (deprecated). On 400, retry without those two and proceed.

If the grant call returns 403, the login session is missing create:client_grants — go back to Phase 1.1 and re-login with that scope.

2.4 Persist creds to a portable env file

Write a dedicated env file at ~/.auth0-checkmate.env and lock its permissions. This works the same way on macOS, Linux, WSL, and Git Bash — no shell-rc surgery required. Fish users: see the Portability section below.

bash
ENV_FILE="$HOME/.auth0-checkmate.env"
cat > "$ENV_FILE" <<'EOF'
# Auth0 CheckMate credentials — managed by this workflow.
# Treat as secret. Restrict permissions and exclude from dotfile repos / backups.
export AUTH0CHECKMATE_DOMAIN="<tenant_domain>"
export AUTH0CHECKMATE_CLIENT_ID="<client_id>"
export AUTH0CHECKMATE_CLIENT_SECRET="<client_secret>"
EOF
chmod 600 "$ENV_FILE"

Replace `<tenant_domain>`, `<client_id>`, and `<client_secret>` with the values from Phase 2 before writing this file. The heredoc is single-quoted to prevent shell expansion — substitute inline, e.g. sed -i "s/<tenant_domain>/$DOMAIN/" ~/.auth0-checkmate.env, or open the file in an editor. The workflow reloads this file in Phase 4 via source. The user does NOT need to add anything to their shell rc — but they can, for convenience: echo 'source ~/.auth0-checkmate.env' >> ~/.zshrc (or ~/.bashrc).

Save client_id (NOT the secret) to the state file.

About the CheckMate CLI itself

CheckMate for Auth0 (@auth0/auth0-checkmate) is Auth0's official first-party tenant-audit CLI, published by Auth0 Inc. under the @auth0 npm organisation scope. It performs configuration checks on your Auth0 tenant, validating key settings and generating a detailed audit report. It requires Node.js v20.18.3 or higher and a valid Auth0 tenant.

It makes use of the Auth0 Management API, which consumes the tenant's rate limits — use it thoughtfully to avoid throttling. Its usage is visible in the tenant's log events in several ways: the User-Agent HTTP header (${packageName}/${packageVersion}, e.g. @auth0/auth0-checkmate/1.4.0, unless the client modifies it), the client_name and scopes assigned when configuring initial access, and seccft events (successful exchange of an access token for a client-credentials grant) in Auth0 logs.

Install it globally with npm install -g @auth0/auth0-checkmate, then run it with a0checkmate and follow the interactive prompts. Update with npm update -g @auth0/auth0-checkmate. Alternatively, run from source: clone auth0/auth0-checkmate, cd into it, npm install, then npm start.

The dedicated M2M application this phase creates authorizes calls to the Management API and needs exactly the 26 read scopes listed in 2.3 — no more. CheckMate also accepts these environment variables for a CI-friendly, non-interactive configuration (used directly in Phase 4.2): ``text AUTH0CHECKMATE_DISABLE_PDF_REPORTING=true|false AUTH0CHECKMATE_DOMAIN=your_domain AUTH0CHECKMATE_CLIENT_ID=your_client_id AUTH0CHECKMATE_CLIENT_SECRET=your_client_secret AUTH0CHECKMATE_FILE_PATH="./reports" AUTH0CHECKMATE_SHOW_VALIDATORS=false

Phase 3 — Company context (lightweight, parallel)

Kick this off before running CheckMate — it's network-bound and independent. The goal is narrow: make the audit *relevant to the customer's product* — what they build, who logs in, and what they integrate with — so each finding reads in their own terms. This is not a sales-qualification step: do not produce fit scores, a "plan fit" verdict, or firmographics (funding, valuation, investors, headcount). The plan suggestion in Phase 5 is derived from the customer's use case + this tenant's actual findings + the pricing reference — never from a company fit score.

Given the company domain, gather public context with whatever web fetch / search tools the agent has. Treat live data as the primary source of truth; use training knowledge only to supplement.

Pages to read (best-effort, cap ~3000 chars each): / (value prop, products, target market), /about (what they do), /login /signin (auth surfaces + current-provider signals), /pricing (B2B vs B2C, tiers), /security /trust (posture, compliance), /docs /developers (auth standards, API patterns), /integrations /partners (third-party APIs — relevant to Token Vault / A4AA), /careers (auth tech named in job posts).

Current auth-provider detection (optional, best-effort): scan the homepage + login-page HTML and job posts. A login redirect to a provider domain is the strongest signal. Patterns: Auth0 (.auth0.com, auth0-spa-js), Clerk (clerk.com, @clerk/, pk_live_), WorkOS (workos.com, authkit), AWS Cognito (cognito-idp, amazoncognito.com), Firebase Auth (firebaseapp.com, identitytoolkit.googleapis.com), Okta / Keycloak / FusionAuth / Frontegg / Supabase (provider domain or SDK name). Record provider + confidence (high = redirect match, medium = single SDK signal, low = inference) + the exact evidence.

  • *Classify (best-effort; use null when unsure — never invent):**
  • company_name, product_summary — name the product(s) and what they do, who the end users are.
  • business_model — B2C / B2B / B2B SaaS / Marketplace / Platform / Fintech / Other.
  • ai_use_case — AI-Native / AI-Differentiated / AI-Enhanced / None / Unknown, with a one-line description.
  • login_portal_assessment — the distinct login surfaces and who uses each (consumers / business customers / developers / partners / internal staff).
  • user_types, auth_standards_detected (OIDC / OAuth2 / SAML / JWT / WebAuthn), notable_auth_features (MFA, SSO, passwordless, social, magic links, API keys, bot protection).
  • integrations — third-party APIs the product or its agents touch (Gmail, Slack, Salesforce, Stripe, GitHub, Jira, HubSpot, …). Used only to judge whether Token Vault / CIBA recommendations are relevant.

Save the resulting JSON to ~/.auth0-checkmate/state/enrichment_<domain>_<timestamp>.json (company_context_*.json is also fine).

If context gathering fails (no internet, web tool errors), don't block — continue with CheckMate. The Phase 5 report degrades gracefully: findings stay accurate, they just won't be framed in product-specific language.

Phase 4 — Run CheckMate + gather tenant facts

4.1 Prereqs

- Node ≥ 20.18.3 (node -v). CheckMate ships Puppeteer which downloads Chromium (~150 MB) on install — flag this to the user before installing. - Install if missing: npm install -g @auth0/auth0-checkmate

4.2 Run with env-var config

Use an absolute report path (relative paths resolve to cwd which may be wrong) and disable PDF generation — the JSON is what Phase 5 consumes, and skipping PDF avoids the Chromium dependency:

bash
REPORT_DIR="$HOME/Documents/auth0_checkmate_reports"
mkdir -p "$REPORT_DIR"
source ~/.auth0-checkmate.env 2>/dev/null  # ensure env vars are available
AUTH0CHECKMATE_DOMAIN="$AUTH0CHECKMATE_DOMAIN" \
AUTH0CHECKMATE_CLIENT_ID="$AUTH0CHECKMATE_CLIENT_ID" \
AUTH0CHECKMATE_CLIENT_SECRET="$AUTH0CHECKMATE_CLIENT_SECRET" \
AUTH0CHECKMATE_FILE_PATH="$REPORT_DIR" \
AUTH0CHECKMATE_DISABLE_PDF_REPORTING=true \
AUTH0CHECKMATE_SHOW_VALIDATORS=false \
a0checkmate

4.3 Locate output

CheckMate writes <tenant>_<locale>_<YYYYMMDD_HHMMSS>_report.json (and a .pdf unless disabled) into FILE_PATH. Pick the newest: ``bash ls -t "$REPORT_DIR"/*_report.json | head -1

The JSON's main array lives at data.report.summary[] with fields like severity (info/warning/critical), title, severity_message, detailsLength, plus per-finding details. Confirm the exact shape before parsing — versions may evolve.

4.4 Gather extra tenant facts (for the report template)

The Phase 5 report needs more than CheckMate alone provides. Run these in parallel and capture each output:

bash
auth0 apps list --json                   # apps_list — count + names for Section 1, 3, 4
auth0 logs list --number 50 --json       # logs_sample — to flag "Active" login activity
auth0 tenant-settings show --json             # tenant_settings — for "Current Auth0 Tier" detection (look for plan/subscription hints; fallback "Free Plan"). Map detected features against the pricing reference to infer the tier when no explicit plan field is present.
auth0 domains list                       # custom_domains — empty array → triggers "Branding Gap"
auth0 connections list                   # connections — for enterprise-connection check
auth0 logs streams list                  # log_streams — empty array → triggers "Observability Gap"
auth0 protection breached-password-detection show  # for "Account Security" section

These outputs feed directly into the Phase 5 fusion.

Phase 5 — Produce the personalized report (markdown + PDF)

  • Read all inputs:
  • CheckMate JSON (*_report.json) — severities, finding titles, details
  • Enrichment JSON (state/enrichment_<domain>_<ts>.json, may be missing if Phase 3 failed)
  • Tenant facts from Phase 4.4
  • state/operator.json — reviewer name
  • state/setup.json.tenant_domain — canonical tenant URL
  1. Produce three files following the locked report skeleton:
  2. A markdown brief following the markdown report template in assets/audit/
  3. A styled HTML brief following assets/audit/report-template.html — same data, render-ready
  4. A PDF rendered from the HTML by ${CLAUDE_SKILL_DIR}/scripts/render_pdf.sh

Key rules below.

Source-of-truth rule

Every <tenant_domain> reference (header right rail, "authentication is served from..." narrative, CLI commands) reads from state/setup.json.tenant_domain. Never from the company context. The company context provides company-level fields (name, products, business model, integrations).

Header

- Title: Auth0 Platform Development Review - Subtitle: Prepared by <reviewer_org> — omit the line entirely if `reviewer_org` is blank (most self-audit cases) - Right rail: <plan_tag> · <Customer Name> · <tenant_domain> · <Month DD, YYYY> - Metadata table: - Prepared by: state/operator.json.reviewer_name - Date of Review: today - Customer Account: <Customer Name> (<tenant_domain>) - Current Auth0 Tier: plan from tenant_settings, fallback "Free Plan" - Use Case Diagnosis: <business_model> — <product_summary_short> from the company context - Recommended Plan: <recommended_plan> — the plan that fits the customer's use case and unlocks this tenant's *After Upgrading* findings (derived from the use case + findings + the pricing reference; plan name only, no price) · Auth for AI Agents: <Yes|No> (Yes only when Phase 3 found concrete agent / third-party integrations Token Vault or CIBA would serve). Example: B2B Essentials · Auth for AI Agents: Yes

Section 1 — Account Health

- Metrics table: - Applications Registered: count from apps_list - Login Activity: "Active (detected on canonical domain)" / "Inactive" — derived from logs_sample - Security Findings: <N> High, <N> Moderate, <N> Low / Info, <N> Passing from CheckMate severity breakdown - 2-4 personalized Gap callouts — bold name + 1-2 sentence narrative naming customer products / portals / users from enrichment. Standard candidates (include only when CheckMate evidence + enrichment context support it): - The Branding Gap — empty custom_domains → name canonical tenant_domain, name 2-3 affected products - The Security Gap — no MFA configured → reference customer's user types / data sensitivity - The Production Readiness Gap — dev URLs in prod, implicit grant types → name offending apps from apps_list - The Compliance Gap — empty log_streams + regulated industry / EU tenant - The Observability Gap — empty log_streams (generic version)

Section 2 — Executive Summary

- Opening paragraph: name customer products from enrichment, apps count from apps_list, "graduating from working setup to production-grade enterprise-ready" framing, then state CheckMate finding counts (<N> High and <N> Moderate). - Configuration Posture at a Glance table: Severity / Count / Key Examples (top 3-5 finding titles per row, dot-separated).

Section 3 — Strategic Opportunities & Action Plan

Four-column table: Feature Area · The Enterprise Challenge · The Solution · Strategic Impact. Standard rows when applicable:

  • Brand Trust & UX → Custom Domains (no custom_domains)
  • Per-Organization Management → Auth0 Organizations (B2B + multi-customer apps)
  • Enterprise SSO → Enterprise Connections (SAML / OIDC) (B2B + no enterprise connections)
  • Account Security → MFA (WebAuthn / Authenticator App) (no MFA)
  • Observability → Log Streaming (no log_streams)
  • AI Agent Security → Token Vault (A4AA) (ai_use_case ∈ AI-Native/AI-Differentiated + integrations)
  • Human-in-the-Loop AI → CIBA (A4AA) (autonomous-action workflows in enrichment)

Every row weaves enrichment data — name products, customer types, business model, integrations, region.

Section 4 — Recommended Roadmap & Next Steps

- Recommended Plan banner: <recommended_plan> — the plan that fits the customer's use case and unlocks the *After Upgrading* fixes (from the use case + the triage table + the pricing reference). Append " + Auth for AI Agents (A4AA)" only when Phase 3 found concrete agent / third-party integrations Token Vault or CIBA would serve. Never quote or estimate an Enterprise price; if Enterprise-only findings exist, note they require Auth0 sales. - 1-paragraph rationale tying plan to enrichment (name customer products, business model) - Immediate Actions — Available Today (Free): numbered list of CheckMate findings actionable on the current plan. Name affected apps/connections explicitly. Feeds the remediation reference's Loop A. - After Upgrading: numbered list of fixes requiring the recommended plan. Feeds the remediation reference's Loop B. - Key Documentation: 2-column grid of doc links matched to Section 3 rows. - Footer: Confidential · Auth0 tenant configuration review for <Customer Name> · <Month YYYY>

Triage rule (Immediate vs After Upgrading)

The source of truth for which plan a feature requires is the co-loaded pricing reference — the full Auth0 feature→plan matrix (B2C + B2B). Read it before triaging; do NOT guess from feature names or memory. A finding goes into Immediate Actions only if its fix is achievable on the tenant's current plan (detected in Phase 4.4, fallback Free). Everything else goes into After Upgrading, labeled with the minimum plan that unlocks it.

Common misconception to avoid: Custom Domains is NOT an upgrade feature — the Free plan includes 1 custom domain (credit-card verification required). Never place it under "After Upgrading."

Quick reference for the findings CheckMate emits (verify against the pricing reference — it wins on any conflict):

CheckMate findingMinimum planLoop
Application Grant Types (remove implicit, Auth Code + PKCE)Free (app config)Immediate
Cross Origin Authentication (disable)FreeImmediate
Refresh Tokens (enable rotating)FreeImmediate
PKCE Enforcement / Token Sender-ConstrainingFreeImmediate
Databases – Password Complexity / Email Attribute VerificationFree (connection config)Immediate
Brute Force Protection / Suspicious IP ThrottlingFree (all plans)Immediate
Session Management / Tenant Login URI / Allowed Logout URLsFree (tenant settings)Immediate
Management API Access Control (M2M scopes)FreeImmediate
Custom DomainsFree (1, CC verification)Immediate
Email Providers / Email Templates (Email Workflow)EssentialsAfter Upgrading
Log StreamsEssentials (1 stream)After Upgrading
MFA factors / Multifactor Policy / MFA for Password Reset†Essentials (Pro MFA Factors)After Upgrading
Breached Password Detection (blocking) / Enhanced Password ProtectionProfessionalAfter Upgrading
Security CenterProfessional (B2C)After Upgrading
Tenant Access Control List (ACL)Enterprise (add-on)After Upgrading
OIDC Back-Channel LogoutEnterpriseAfter Upgrading
Private Key JWT / JAR / PAREnterpriseAfter Upgrading
CIBA (A4AA)Essentials + A4AA add-onAfter Upgrading
Token Vault (A4AA)Free gets 2; more via A4AA add-onAfter Upgrading (scale)

† The Password Reset *Action* itself deploys on Free (5 Actions included), but it can only challenge MFA once MFA factors are enabled, which requires Essentials+. Treat the end-to-end "MFA on password reset" outcome as Essentials.

Enterprise-only findings (Tenant ACL, Back-Channel Logout, Private Key JWT) are out of self-service scope — list them under After Upgrading but note they require an Enterprise plan / Auth0 sales, not a self-service upgrade.

Fusion checklist (verify before saving — every item is mandatory)

The point of this workflow is to fuse enrichment intelligence into the audit so the customer sees why each recommendation matters for their specific business, not generic best-practice. Before saving the files, walk this checklist. If any item fails, regenerate that section.

  1. Header metadata uses the customer's actual company_name, business_model, product_summary_short, and the derived <recommended_plan> — no template placeholders.
  2. Section 1 metrics show real apps_count from auth0 apps list, real CheckMate severity counts, real login activity derived from auth0 logs list.
  3. Section 1 Gap callouts each name at least one specific product, app, or user segment from enrichment / apps_list. If a callout reads generically ("enterprise clients...", "the affected apps..."), rewrite it with concrete names or omit it.
  4. Section 2 opening paragraph lists the customer's products by name (e.g. "Customer Portal, Admin Console, Mobile App") — never "the customer's apps".
  5. Section 3 — every row's `Enterprise Challenge` cell references concrete enrichment data: product names, business model, user types, integrations, region, AI workflows, or industry. If you cannot fill any cell with company-specific content, OMIT the row entirely. A generic row is worse than no row.
  6. Section 4 plan rationale names the customer's products and ties them to the recommended plan's specific features.
  7. Section 4 action lists name affected apps/connections explicitly (e.g. "Disable implicit grant on Default App, Admin Console Web" — never "Disable implicit grant on the affected apps").
  8. No `{{placeholder}}` strings remain in either rendered file.

Final lint: ``bash grep -nE '\{\{|enterprise clients[^A-Za-z]|the customer[^A-Za-z]|the affected apps' "$MD_PATH" "$HTML_PATH" `` Output must be empty. If non-empty, rewrite the flagged passages with company-specific content.

Output paths

Write all three to ~/Documents/, with the same timestamped basename: `` auth0_checkmate_<sanitized_tenant>_<YYYYMMDD_HHMMSS>.md auth0_checkmate_<sanitized_tenant>_<YYYYMMDD_HHMMSS>.html auth0_checkmate_<sanitized_tenant>_<YYYYMMDD_HHMMSS>.pdf ` (Date-time, not just date — same-day re-runs must not collide. Sanitize tenant_domain by replacing . with _`.)

Render the PDF

bash "${CLAUDE_SKILL_DIR}/scripts/render_pdf.sh" "$HTML_PATH" "$PDF_PATH" # ${CLAUDE_SKILL_DIR} is the absolute path to this skill folder (auto-set by Claude Code). # Other agents: substitute the absolute path to wherever this skill folder was extracted.

If the script exits non-zero, surface the renderer's error message — the markdown and HTML are already saved, so the user has fallback artifacts. Common cause: no Chrome / Chromium / Edge / Brave / Arc app installed and no wkhtmltopdf / weasyprint. The script's stderr message tells the user how to fix.

Chat summary

Render a short chat summary: top 3-5 findings by severity, each with a one-line personalized "why this matters for <Company>" framed in the customer's product/business terms. End with the three file paths.

Phase 6 — Walk the user through the report

Branch on state/setup.json.mode (set in Phase 0.3):

  • Audit only → render the chat summary, print the three file paths (md / html / pdf), END the run. Do NOT proceed to remediation.
  • Express or Guided → render the chat summary, print file paths, then continue to remediation using the co-loaded remediation reference. The report's "Immediate Actions" and "After Upgrading" lists are the canonical work queues.

Remediation itself — the command-shape mapping, the per-item guided flow, the express-mode batch flow, the plan-upgrade gate, and the never-without-confirmation list — is covered in full by the remediation reference loaded alongside this one; every applied fix still requires per-command confirmation, every change is verified by re-fetching the resource, and a failed command is never retried destructively.

State files

Located under ~/.auth0-checkmate/state/ (created lazily):

FilePurpose
setup.jsoncache: tenant_domain, checkmate_client_id, company_domain, last_run_at — secrets excluded
operator.jsonreviewer name + optional org/role for the report header
enrichment_<domain>_<ts>.jsonone per enrichment run
queue.jsonqueued / skipped / pending_upgrade items across runs
history.jsonlappend-only log of applied/queued/skipped findings per run

Pitfalls to remember

  • read:hooks / read:rules are deprecated; some tenants 400 on grant. Retry without them.
  • Free-plan tenants: 1 M2M app quota for Management API. Reuse if name match.
  • AUTH0CHECKMATE_FILE_PATH is relative-to-cwd by default — always pass an absolute path.
  • Tenant region: trust auth0 tenants list --json, don't parse the domain.
  • State file is a cache — re-validate each field every run; never act on it without verification.
  • ~/.auth0-checkmate.env is not auto-sourced. The workflow does it in Phase 4; outside the workflow, the user has to either source it manually each session or chain it into their shell rc.

Portability notes (cross-OS / cross-shell)

This workflow is designed to work for any Auth0 customer, not just the author's machine.

  • *Operating systems**
  • macOS, Linux, and Windows-WSL are first-class. Native Windows (PowerShell / cmd) is not supported by this workflow's bash steps; users on native Windows should run inside WSL or Git Bash.
  • ~/Documents exists by default on macOS and Windows; on Linux it depends on user setup. If ~/Documents doesn't exist, fall back to ~/auth0-checkmate-reports (create it on first write).
  • *Shells**
  • ~/.auth0-checkmate.env uses POSIX export syntax — works for bash, zsh, dash, ksh, and Git Bash.
  • Fish shell users: the env file syntax differs (set -gx instead of export). The workflow writes the POSIX form by default; a fish-specific file at ~/.config/fish/conf.d/auth0-checkmate.fish can be added by the user. Phase 4's source ~/.auth0-checkmate.env won't work in fish — pass env vars inline instead.
  • *PDF renderer**
  • scripts/render_pdf.sh searches Chrome / Chromium / Edge / Brave / Arc on macOS, Linux, and WSL paths automatically. Falls back to wkhtmltopdf and then weasyprint if no browser is found.
  • If none are available, the markdown and HTML files still get written — the PDF step is skipped with a clear error message telling the user how to install one renderer. The workflow should NOT fail the whole run on PDF render failure.
  • *Auth0 CLI install**
  • See Phase 1.1 for per-platform install commands. The CLI itself is a single Go binary, so any OS with Go ≥ 1.21 can use go install as the universal fallback.
  • *Self-audit vs CS-audit modes**
  • reviewer_org blank → the report subtitle is omitted (clean self-audit look).
  • reviewer_org populated → the report subtitle reads "Prepared by <reviewer_org>" (CS / consulting / partner mode).
  • Either way, the report content is identical — the only difference is the header subtitle.
  • Enrichment provides company-level fields only — never the tenant URL. Tenant URL always comes from state/setup.json.tenant_domain.