--- 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
- When you ask your AI assistant something like *"add Auth0 login to my app"*, the assistant:
- Loads the single auth0 skill, whose router inspects your project configuration files (
package.json,requirements.txt,build.gradle,composer.json,go.mod,*.csproj, etc.) - Automatically detects your framework (Next.js, React, Node/Express, FastAPI, Nuxt, Angular, Flutter, Go, .NET, etc.) and loads the matching reference files
- 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 script | integrate |
| 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.
| Package | Framework |
|---|---|
@capacitor/browser + @auth0/auth0-angular | ionic-angular |
@capacitor/browser + @auth0/auth0-react | ionic-react |
@capacitor/browser + @auth0/auth0-vue | ionic-vue |
@auth0/nextjs-auth0 | nextjs |
@auth0/auth0-nuxt | nuxt |
@auth0/auth0-tanstack-start-react | tanstack-start |
@auth0/auth0-react | react |
@auth0/auth0-vue | vue |
@auth0/auth0-angular | angular |
@auth0/auth0-spa-js | spa-js |
express-openid-connect | express |
@auth0/auth0-fastify | fastify |
@auth0/auth0-fastify-api | fastify-api |
express-oauth2-jwt-bearer | express-jwt |
react-native-auth0 + app.json or app.config.js present | expo |
react-native-auth0 (no Expo files) | react-native |
@auth0/auth0-api-js | auth0-api-js |
@auth0/auth0-server-js | auth0-server-js |
@auth0/auth0-auth-js | auth0-auth-js |
auth0 (the bare package, not @auth0/*) | node-auth0 |
Python — check `requirements.txt` or `pyproject.toml`
Rows are most-specific first: auth0-server-python is the framework-agnostic
server core, so check for a co-installed web framework before falling back to the
bare-SDK row.
| Package | Framework |
|---|---|
auth0-server-python + flask | flask |
auth0-server-python (no Flask web framework) | server-python |
auth0-fastapi-api | fastapi-api |
auth0-api-python | api-python |
Java / Kotlin — check `build.gradle` or `pom.xml`
| Dependency | Framework |
|---|---|
mvc-auth-commons (com.auth0:mvc-auth-commons) | java-mvc |
spring-security-oauth2-resource-server | springboot-api |
.NET — check `*.csproj` or `NuGet.Config`
| Package | Framework |
|---|---|
Auth0.AspNetCore.Authentication (no .Api suffix) | aspnetcore-auth |
Auth0.AspNetCore.Authentication.Api | aspnetcore-api |
Auth0.OidcClient.MAUI | maui |
Auth0.OidcClient.AndroidX | net-android |
Auth0.OidcClient.iOS | net-ios |
Auth0.OidcClient.WinForms | winforms |
Auth0.OidcClient.WPF | wpf |
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.
| Package | Framework |
|---|---|
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 + AuthorizationGuard | laravel-api |
Ifauth0/auth0-phpis installed but noSdkConfigurationstrategy is set > yet (fresh project), fall through to variant disambiguation below.
Go — check `go.mod`
| Module | Framework |
|---|---|
github.com/auth0/go-jwt-middleware | go |
Mobile (native)
| Signal | Framework |
|---|---|
build.gradle(.kts) + com.auth0.kmp:auth0 (Kotlin Multiplatform module) | kmp |
Package.swift or .xcodeproj + Auth0.swift | swift |
build.gradle + com.auth0.android:auth0 | android |
pubspec.yaml + auth0_flutter + developer names/targets Windows desktop | flutter-windows |
pubspec.yaml + auth0_flutter + flutter.web: false | flutter-native |
pubspec.yaml + auth0_flutter + web enabled | flutter-web |
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.
| Signal | Base framework |
|---|---|
next in package.json | nextjs |
nuxt in package.json | nuxt |
@tanstack/react-start in package.json | tanstack-start |
@ionic/* + @angular/core | ionic-angular |
@ionic/* + react | ionic-react |
@ionic/* + vue | ionic-vue |
@angular/core in package.json | angular |
vue in package.json (no nuxt) | vue |
expo in package.json | expo |
react-native (no expo) | react-native |
react (no meta-framework above) | react (SPA) — see note |
express in package.json | express (variant below) |
fastify in package.json | fastify (variant below) |
flask in requirements.txt/pyproject.toml | flask |
fastapi in requirements.txt/pyproject.toml | fastapi-api (variant below) |
spring-boot in pom.xml/build.gradle | springboot-api |
laravel/framework in composer.json | laravel (variant below) |
composer.json present (no Laravel) | php (variant below) |
go.mod present + HTTP server/router | go |
org.jetbrains.kotlin.multiplatform plugin + commonMain source set (shared Android+iOS module) | kmp |
Package.swift or .xcodeproj | swift |
pubspec.yaml (Flutter) + developer names/targets Windows desktop | flutter-windows |
pubspec.yaml (Flutter, web disabled) | flutter-native |
pubspec.yaml (Flutter, web enabled) | flutter-web |
*.csproj referencing MAUI | maui |
*.csproj (WinForms) | winforms |
*.csproj (WPF) | wpf |
*.csproj ASP.NET (web app or API) | aspnetcore (variant below) |
`react` note: a plain React project maps toreactfor an SPA using the > React SDK, orspa-jsif 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 / next | nextjs |
| Nuxt | nuxt |
| 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 SPA | spa-js |
node-auth0 / the auth0 npm package | node-auth0 |
@auth0/auth0-api-js / auth0-api-js / low-level resource-server SDK | auth0-api-js |
@auth0/auth0-server-js / auth0-server-js / server-side Auth0 session SDK | auth0-server-js |
@auth0/auth0-auth-js / auth0-auth-js / AuthClient / low-level OAuth OIDC | auth0-auth-js |
| Express (web app / server-rendered) | express |
| Express API / protect API routes | express-jwt |
| Fastify (web) / Fastify API | fastify / fastify-api |
| Flask | flask |
| FastAPI (web app) / FastAPI API | server-python / fastapi-api |
auth0-server-python / framework-agnostic Python server SDK / Python OIDC web server with no dedicated reference (Django, Starlette, Sanic, Quart, aiohttp) | server-python |
auth0-api-python / Python API JWT validator (resource server / verify_access_token) | api-python |
| Spring Boot | springboot-api |
| Java MVC / servlet | java-mvc |
| ASP.NET Core web app / API | aspnetcore-auth / aspnetcore-api |
| MAUI / WinForms / WPF | maui / winforms / wpf |
| PHP web app / PHP API | php / php-api |
| Laravel web app / Laravel API | laravel / laravel-api |
| Go / Golang API | go |
Kotlin Multiplatform / KMP / shared Android+iOS auth code / com.auth0.kmp | kmp |
| Swift / iOS | swift |
| Android / Kotlin | android |
| Flutter (native / web / Windows) | flutter-native / flutter-web / flutter-windows |
| React Native / Expo | react-native / expo |
| Ionic (Angular/React/Vue) | ionic-angular / ionic-react / ionic-vue |
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:
| Base | Web-app variant | API variant | Choose API when… |
|---|---|---|---|
| express | express | express-jwt | protecting API routes / validating JWTs, no server-rendered UI |
| fastify | fastify | fastify-api | resource server / JWT validation only |
| fastapi | server-python | fastapi-api | resource server / JWT validation only; a web app with login/logout UI uses server-python |
| php | php | php-api | building/protecting a PHP API, no web UI |
| laravel | laravel | laravel-api | API-only (token guard), no Blade UI |
| aspnetcore | aspnetcore-auth | aspnetcore-api | Web API / JWT bearer, no cookie login UI |
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 files | tooling-terraform/index.md |
| Auth0 MCP server active in this agent session | tooling-mcp/index.md |
| A request for the Auth0 Vercel Marketplace/native integration, or to connect Auth0 to a Vercel project | tooling-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.
# Initialize ACUL project locally with Auth0 CLI
auth0 acul init
auth0 acul screen addSubgroup 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:
- Tenant / login flow - the tenant decides MFA is required (Guardian policy or Action). This is what actually challenges the user. See "Tenant configuration".
- 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 throwawaypython -c/node -eprobes 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.jsonto confirm the installed version meets it — onlyverify — X+rows call for a check. - Don't add dependencies you weren't asked for. A
barcode_uri/barcodeUriis a string you can render or return as-is; don'tnpm installa 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 buildpasses if quick). Stop there.
The mechanic: browser step-up
Recipe (do in order):
- Trigger. Send the authorization request with the PAPE multi-factor
acr_valuesand -
max_age=0(forces a fresh challenge so a live session cannot satisfy it silently). - Every SDK wraps this one shape:
GET /authorize?...&acr_values=http://schemas.openid.net/pape/policies/2007/06/multi-factor&max_age=0
```- Verify completion with the signal that matches your audience (table below).
- Enforce that same signal server-side on every protected endpoint, rejecting when it
- is absent. A frontend check is UX, never security.
- Conditional MFA: require it from a post-login Action calling
api.multifactor.enable(...).
| Your context | Verify with |
|---|---|
| Session-managing SDK (web app) | The amr claim (contains mfa when MFA completed) off the SDK's own session / current-user accessor - already validated, so trust it as-is. Accessor name is SDK-specific -> see the 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. |
| Frontend | Nothing - treat any amr check as UX, never enforcement. |
Notes: a silent token request may instead surface an mfa_required error - handle it by
re-authenticating interactively. Never re-decode or re-verify the token by hand (see
"Common mistakes"). 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):
- Read the token. Catch
mfa_required; readmfa_tokenoff it. - Branch on enrollment.
- - *No factor yet* -> associate (enroll). OTP returns a
barcode_urito render as a QR - code; out-of-band (SMS/voice/email/push) returns an
oob_codeyou confirm by - submitting it directly to the token endpoint (MFA OOB grant, plus a
binding_code - when the channel requires one) - a just-associated authenticator needs no separate
-
/mfa/challenge. Recovery codes are returned only on first enrollment. - - *Already enrolled* -> challenge the existing authenticator so the user is prompted for
- 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:
| Symbol | Meaning |
|---|---|
acr_values | Authorization-request parameter used to request MFA |
http://schemas.openid.net/pape/policies/2007/06/multi-factor | The PAPE value that requests multi-factor |
max_age=0 | Forces fresh authentication so a live session cannot satisfy the step-up silently |
amr | ID-token claim listing the methods used; contains mfa when MFA completed. Not present on access tokens by default |
acr | Claim echoing the satisfied authentication context |
| high-value scope | An 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):
| Value | Meaning |
|---|---|
pwd | Password |
mfa | Multi-factor authentication completed |
otp | One-time password (TOTP authenticator app) |
sms | SMS code |
email | Email code |
hwk | Hardware key (WebAuthn security key) |
swk | Software key |
pop | Proof of possession |
fed | Federated (social / enterprise) |
Error responses
Returned by the token/authorization endpoints during an MFA flow (KEEP INLINE):
| Error | Cause | Handling |
|---|---|---|
mfa_required | MFA has not been completed | Browser 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_required | The user has no authenticator enrolled | Send the user through enrollment (self-service or enrollment ticket), then challenge |
unsupported_challenge_type | The app supports none of the challenge types the user is enrolled with, or the user is not enrolled | Align the app's supported challenge types with the user's enrolled authenticators (or enroll the user) - do NOT change tenant config first |
mfa_invalid_code | Wrong OTP entered | Prompt to retry |
too_many_attempts | Repeated failures | Back 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.
| SDK | Min version | Flow(s) | Reference (Read this file) |
|---|---|---|---|
@auth0/auth0-react | 2.14.0 (MFA API) · 2.15.0 (popup step-up) | popup step-up, MFA API | references/feature-mfa/auth0-react.md |
@auth0/auth0-vue | 2.6.0 | popup step-up, MFA API | references/feature-mfa/auth0-vue.md |
@auth0/auth0-angular | 2.9.0 | popup step-up, MFA API | references/feature-mfa/auth0-angular.md |
@auth0/auth0-spa-js | 2.16.0 | popup step-up | references/feature-mfa/auth0-spa-js.md |
@auth0/nextjs-auth0 | 4.15.0 (MFA + APIs) · verify — 4.19+ (popup step-up) | step-up, MFA API, popup | references/feature-mfa/nextjs-auth0.md |
@auth0/auth0-auth-js | 1.8.0 | MFA API | references/feature-mfa/auth0-auth-js.md |
@auth0/auth0-server-js | 1.5.0 | MFA API | references/feature-mfa/auth0-server-js.md |
express-openid-connect | 2.17.0 | redirect step-up | references/feature-mfa/express-oidc.md |
Auth0.swift (iOS/macOS) | verify — 3.0+ | MFA API | references/feature-mfa/auth0-swift.md |
Auth0.Android | verify — 3.13+ | MFA API | references/feature-mfa/auth0-android.md |
react-native-auth0 | 5.10.0 | MFA API | references/feature-mfa/react-native-auth0.md |
auth0-server-python | 1.0.0b10 (MFA API) · 1.0.0b15 (step-up) | step-up, MFA API | references/feature-mfa/auth0-server-python.md |
Auth0.OidcClient.* (.NET native/desktop) | 4.4.0 | client-initiated step-up | references/feature-mfa/oidc-client-net.md |
go-jwt-middleware (Go, resource server) | v2.2.1 | API-side scope gate | references/feature-mfa/go-jwt-middleware.md |
Auth0.AspNetCore.Authentication.Api (resource server) | 1.0.1 | API-side scope gate | references/feature-mfa/aspnetcore-api.md |
spring-boot-starter-oauth2-resource-server (Java) | Spring Boot 3.3 / Security 6 | API-side scope gate | references/feature-mfa/spring-security-api.md |
auth0-api-python (Python resource server) | 1.0.0b10 | API-side scope gate | references/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/policiesto["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 theall-applicationspolicy, 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:
# TOTP / Authenticator app
auth0 guardian factors set otp --enabledSMS — 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
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:
| Operation | Endpoint | Authorized by |
|---|---|---|
| Enroll (associate) a new authenticator | POST /mfa/associate | mfa_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 authenticators | GET /mfa/authenticators | mfa_token |
| Challenge an enrolled authenticator | POST /mfa/challenge | mfa_token |
| Remove an enrolled authenticator | DELETE /mfa/authenticators/{id} | post-MFA access token, remove:authenticators scope, mfa audience |
| Complete sign-in with the verified factor | POST /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:
| Operation | Endpoint |
|---|---|
| List a user's authentication methods | GET users/{id}/authentication-methods |
| Delete one authentication method | DELETE users/{id}/authentication-methods/{authentication_method_id} |
| Send an enrollment ticket | POST guardian/enrollments/ticket |
Common mistakes
| Mistake | Why it breaks | Correct approach |
|---|---|---|
| Setting a Guardian policy before enabling any factor | Users are required to do MFA but have no factor to complete it with | Enable the factor first, then set the policy |
| Treating an enabled factor as enforcement | A factor with no policy never challenges anyone | Enforce with a policy or a post-login Action |
Reading guardian/policies as [] and assuming MFA is on | [] means available but not required | Confirm a non-empty policy (or an Action that enables MFA) |
| Trusting a frontend MFA check | The client can be bypassed entirely | Enforce server-side: amr on a web/session backend, the high-value scope on a resource API |
Checking amr on a resource API's access token | Access tokens carry no amr by default, so valid stepped-up callers are rejected | Gate the API on the high-value scope; add amr as a custom claim only if this API also validates it |
Hand-decoding the token to read amr (jwt.decode, PyJWKClient, id_token.split, manual JWKS) | Reinvents validation the SDK already performed, and usually disables exp/iss/audience checks in the process | Read amr from the SDK's session/current-user accessor; its claims are already verified |
Assuming the session accessor always carries amr | Some SDKs persist only a default claim subset, so amr is silently absent and the check never passes | Opt the claim in: @auth0/nextjs-auth0 v4 needs a beforeSessionSaved hook to copy amr into the session (session.user.amr); express-openid-connect keeps the full claims on req.oidc.idTokenClaims, not the filtered req.oidc.user |
Omitting max_age=0 on step-up | A still-valid session satisfies the request with no fresh challenge | Send max_age=0 (or the SDK's fresh-auth option) for step-up |
Ignoring mfa_required from a silent token call | The step-up silently fails and the action proceeds unverified | Catch it and re-authenticate interactively |
| Preferring SMS by default | SMS is vulnerable to SIM-swap | Prefer TOTP or WebAuthn; treat SMS as a fallback |
| No recovery codes enabled | Users get locked out when they lose a device | Enable recovery codes during enrollment |
Wrong HTTP verb on guardian/policies | Returns a misleading 404 | Use auth0 guardian policies set --policy all-applications |
Sending message_type or provider to guardian/factors/sms directly | Returns a 400 — those fields are not accepted on that endpoint | Use 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 flow | Forces the app to hold Management API admin scopes and ignores the mfa_token the flow already issued | List 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 verify | Diverges from the SDK's documented enrolled-factor flow and breaks for out-of-band factors (SMS/push), whose challenge is what delivers the code | Challenge the enrolled authenticator, then verify |
Related capabilities
- Tenant setup and Actions -
tooling-cliandtooling-terraformown Guardian - factor/policy configuration and Action deployment (
auth0 actions ...). - SDK-side step-up trigger - the SDK's own step-up call, its
mfa_requiredhandling, - 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
amrby default), and on a web/session backend - check the
amrclaim. - 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-mfareference (thehwkfactor 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:webauthnthe app exchanges a verified credential for tokens on. Must be enabled on the application.
SDK integration
Passkeys need setup in two independent places:
- 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".
- 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:
- Challenge — ask Auth0 for a WebAuthn challenge (signup or login). Returns an
authSessionplus the public-key options the browser/authenticator needs. - Ceremony — the platform runs WebAuthn:
navigator.credentials.create()(signup) or.get()(login) on web;ASAuthorizationControlleron Apple platforms;CredentialManageron Android. This produces a signed credential. - Token exchange — send the credential +
authSessionback 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) — onepasskey.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_modulesto confirm it unless averify — X+row tells you to. - You're done when the app code is in place (and, for a JS/TS app,
npm run buildpasses if quick). Stop there.
Tenant configuration
Passkeys require three things set before any flow works; all three are independent of the app code:
# 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:
| Symbol | Meaning |
|---|---|
urn:okta:params:oauth:grant-type:webauthn | The passkey grant the credential is exchanged for tokens on |
authSession | Opaque session string returned by the challenge; passed back to the token exchange |
authnParamsPublicKey / authParamsPublicKey / publicKey | The 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 |
clientDataJSON | Part of the browser credential response (both create and get) |
attestationObject | Registration (signup) credential response field |
authenticatorData + signature + userHandle | Assertion (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):
| Operation | Endpoint | Key params | Authorized by |
|---|---|---|---|
| Signup / registration challenge | POST /passkey/register | client_id, realm (optional; connection), user_profile { email, name? } | Public client (client_id) |
| Login / authentication challenge | POST /passkey/challenge | client_id, realm (optional) | Public client (client_id) |
| Token exchange | POST /oauth/token | grant_type=urn:okta:params:oauth:grant-type:webauthn, client_id, auth_session, authn_response, plus optional realm/scope/audience | Client credentials in the request body |
| Enroll — begin (My Account) | POST /me/v1/authentication-methods body { "type": "passkey" } | returns auth_session + authn_params_public_key | Bearer token, scope create:me:authentication_methods, audience https://{yourDomain}/me/ |
| Enroll — verify (My Account) | POST /me/v1/authentication-methods/{id}/verify | auth_session, authn_response | Same 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_methodsscope, minted for thehttps://{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).
| Error | Cause | Handling |
|---|---|---|
mfa_required / MfaRequiredError | The tenant requires a second factor after the passkey | Continue with the MFA API flow (feature-mfa) using the mfa_token |
| passkey challenge / registration error | User cancelled, no authenticator, or rpId/domain mismatch | Surface a retry; confirm the custom domain matches the rpId |
login_required on a later silent token call | The passkey flow created no session cookie and refresh tokens were not enabled | Configure 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.
| SDK | Min version | Flow(s) | Reference (Read this file) |
|---|
@auth0/auth0-react | 2.18.0 | signup, login | references/feature-passkeys/auth0-react.md |
|---|---|---|---|
@auth0/auth0-vue | 2.8.0 | signup, login | references/feature-passkeys/auth0-vue.md |
@auth0/auth0-angular | 2.10.0 | signup, login | references/feature-passkeys/auth0-angular.md |
@auth0/auth0-spa-js | 2.21.0 | signup, login | references/feature-passkeys/auth0-spa-js.md |
@auth0/nextjs-auth0 | 4.22.0 | signup, login, enrollment | references/feature-passkeys/nextjs-auth0.md |
@auth0/auth0-server-js | 1.7.0 | signup, login | references/feature-passkeys/auth0-server-js.md |
@auth0/auth0-auth-js | 1.7.0 | signup, login | references/feature-passkeys/auth0-auth-js.md |
auth0-server-python | 1.0.0b13 | signup, login | references/feature-passkeys/auth0-server-python.md |
Auth0.swift (iOS/macOS/visionOS) | 2.12.0 · 2.13.0 (enrollment) | signup, login, enrollment | references/feature-passkeys/auth0-swift.md |
Auth0.Android | 3.2.0 | signup, login | references/feature-passkeys/auth0-android.md |
react-native-auth0 | 5.7.0 (native) · 5.11.0 (web) | signup, login, enrollment | references/feature-passkeys/react-native-auth0.md |
Common mistakes
| Mistake | Why it breaks | Correct approach |
|---|---|---|
No custom domain (using *.auth0.com) | Passkeys are bound to the rpId; the default domain cannot be a relying party | Configure and verify a custom domain first |
| Passkey grant not enabled on the app | The token exchange is rejected | Enable urn:okta:params:oauth:grant-type:webauthn on the application |
SPA SDK without useRefreshTokens | Passkey flow creates no session cookie, so getAccessTokenSilently() later fails login_required | Configure useRefreshTokens: true (+ Refresh Token Rotation) on SPA SDKs |
| Using a public client for the server token exchange | Server SDK passkey token exchange requires client authentication | Use a confidential client (clientSecret / private-key JWT / mTLS) per the leaf |
| Treating a passkey as a second factor | Passkeys here are the primary credential; the MFA reference covers the second-factor case | For 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 ceremonies | Use 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 it | Leaks Auth0 access / ID / refresh tokens to the client | Let 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 app | A different session/token model from react-native-auth0; splits the flow across two SDKs | Keep 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
| Concept | Description |
|---|---|
| Organization | An isolated tenant within your Auth0 tenant. Has an id (org_xxx) and name (slug). |
| Member | A user belonging to an organization. A user can belong to multiple orgs. |
| Org-level role | A role granted to a user within a specific org (not globally). |
| Connection | A login method enabled for an org (database, enterprise SSO, social). |
| Invitation | A 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.
| SDK | Min version | Read this file |
|---|---|---|
@auth0/auth0-react | 2.x | Read: references/feature-organizations/auth0-react.md |
@auth0/auth0-spa-js | 2.x | Read: references/feature-organizations/auth0-spa-js.md |
@auth0/auth0-vue | 2.x | Read: references/feature-organizations/auth0-vue.md |
@auth0/auth0-angular | 2.x | Read: references/feature-organizations/auth0-angular.md |
@auth0/nextjs-auth0 | 4.x | Read: references/feature-organizations/nextjs-auth0.md |
express-openid-connect | 2.x/3.x | Read: references/feature-organizations/express-oidc.md |
react-native-auth0 | 5.x | Read: references/feature-organizations/react-native-auth0.md |
Auth0.swift | 2.x/3.x | Read: references/feature-organizations/auth0-swift.md |
Auth0.Android | 2.x–4.x | Read: 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-js | 1.9.0 | Read: references/feature-organizations/auth0-server-js.md |
@auth0/auth0-auth-js | 1.10.0 | Read: references/feature-organizations/auth0-auth-js.md |
auth0-server-python | 1.0.0b11 | Read: references/feature-organizations/auth0-server-python.md |
@auth0/auth0-api-js (API) | 1.3.0 | Read: references/feature-organizations/auth0-api-js.md |
express-oauth2-jwt-bearer (API) | 1.0.0 | Read: references/feature-organizations/express-oauth2-jwt-bearer.md |
go-jwt-middleware (API, Go) | v3.3.0 | Read: references/feature-organizations/go-jwt-middleware.md |
auth0-api-python (API, Python) | 1.0.0b4 | Read: references/feature-organizations/auth0-api-python.md |
Auth0.AspNetCore.Authentication.Api (API, .NET) | 1.0.0 | Read: 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_namefrom the ID token. Auth0's guidance is that web applications - validate
org_idfrom 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_idfrom 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_idon 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_idfrom 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_idis 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.
| Operation | CLI | Terraform |
|---|---|---|
| Create an organization | auth0 orgs create --name <slug> --display "<Name>" | auth0_organization |
| List / show / update / delete | auth0 orgs list / show / update / delete | auth0_organization |
| Add a member | auth0 api post "organizations/<org-id>/members" --data '{"members":["<user-id>"]}' | auth0_organization_member |
| Enable a connection | auth0 api post "organizations/<org-id>/enabled_connections" --data '{"connection_id":"<con-id>","assign_membership_on_login":true}' | auth0_organization_connections |
| Assign an org-scoped role | auth0 api post "organizations/<org-id>/members/<user-id>/roles" --data '{"roles":["<role-id>"]}' | auth0_organization_member_roles |
| Create an invitation | auth0 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 ...:
| Field | Values | Meaning |
|---|---|---|
organization_usage | deny / allow / require | Whether a login may (allow) or must (require) carry an organization. require forbids any login without one |
organization_require_behavior | no_prompt / pre_login_prompt / post_login_prompt | How 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: allowwithorganization_require_behavior: no_prompt
# 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_promptauth0 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:
# 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:
# 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_prompt2. 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.
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
authorizationParamson 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+organizationoff the login request and forward both; only fall back to the default org when noorganizationis present. - If you accept cross-org invitations, pass
organizationper login call - a client-wide default org is validated against the returnedorg_idat login completion and rejects invites to other orgs.
Common mistakes
| Mistake | Fix |
|---|---|
Not passing organization at login | Pass 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 invite | Read invitation + organization from the callback URL and forward both to /authorize |
| Using your default org for an invitation link | Forward the invite's own organization param - it may differ from your configured default |
Login route overwriting an incoming organization with the default | Forward 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 invitations | A 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 token | Web/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 backend | Validate 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-list | The 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 org | Keep 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 undefined | Guarantee 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 integration | Wire the change into the project's existing login/callback/route/config files; keep the diff minimal |
Hand-decoding a token to read org_id | Use the SDK's claim accessor (getUser() / getIdTokenClaims() / session user) - the claim is already exposed |
Mixing up org id (org_xxx) and name (slug) | id for API calls, name for display |
| Granting global roles instead of org-level roles | Use the org member roles endpoint, not the user roles endpoint |
| Not enabling a connection for the org | auth0 api post "organizations/<org-id>/enabled_connections", or Dashboard → Organization → Connections |
A space or underscore in a new connection's name | Alphanumerics and hyphens only, starting and ending alphanumeric. Anything else is a 400 |
| Creating a connection and enabling it for no app | Nothing 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 api | Use 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 first | It is tenant-wide. Capture the old value, and restore or disclose it |
Guessing a auth0 orgs subcommand for membership, roles, or connections | Verify 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 tenant | Both are hard 400s. Configure them first (see Invitation flow) |
Leaving organization_usage at allow when the app must reject non-org logins | allow 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
| Concept | Description |
|---|---|
| DPoP Proof | A short-lived signed JWT attached to each request proving key possession |
| DPoP Nonce | A server-issued value that must be included in the proof to prevent replay |
useDpop: true | SDK option that enables automatic DPoP proof generation |
createFetcher() | SDK helper that returns a fetch-compatible function handling proofs automatically |
UseDpopNonceError | Error thrown when the server rotates its nonce mid-flight; retry with the new nonce |
Step 1: Enable DPoP on Your API
Via Auth0 Dashboard
- Go to Applications → APIs
- Select the API your SPA calls
- Under the Settings tab, confirm the API identifier matches your
audience - No additional toggle is needed in the dashboard — DPoP is enabled per-request
- by the client when the API resource server is configured to accept DPoP tokens
Via Auth0 CLI
# 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 theproof_of_possessionpayload - Terraform:
auth0_resource_serverproof_of_possessionblock +auth0_clientrequire_proof_of_possession - MCP:
auth0_update_resource_serveracceptsproof_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
- Add
useDpop: trueto your Auth0 client/provider configuration alongside youraudience - Use
createFetcher()instead of attaching tokens manually — the SDK handles - proof generation, nonce management, and header injection for you
- Handle
UseDpopNonceErrorin cases where the server rotates its nonce
Environment variables
Ensure your .env includes the API audience:
# Vite
VITE_AUTH0_DOMAIN=your-tenant.auth0.com
VITE_AUTH0_CLIENT_ID=your-client-id
VITE_AUTH0_AUDIENCE=https://your-api-identifierFramework Examples
Vue.js
Uses @auth0/auth0-vue.
1. Enable DPoP in plugin config
// 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
<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
// 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
const { createFetcher } = useAuth0();const ordersApi = createFetcher({ baseUrl: 'https://orders.example.com' }); const inventoryApi = createFetcher({ baseUrl: 'https://inventory.example.com' }); ```
5. Manual DPoP management (advanced)
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
// 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
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
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
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+):
// 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:
// 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
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
// 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
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
// 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
const response = await apiFetch('/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'example' })
});4. Multiple APIs with separate fetchers
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:
// 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
| Issue | Cause | Fix |
|---|---|---|
API returns 401 with error: use_dpop_nonce | Server issued a new nonce | Catch UseDpopNonceError and retry |
API returns 401 with invalid_dpop_proof | Clock skew or wrong htm/htu values | Ensure system clock is accurate; verify baseUrl matches API URL exactly |
| Token still issued as Bearer instead of DPoP | useDpop: true missing or audience not set | Confirm both options are present in client config |
createFetcher is undefined | SDK version too old | Upgrade 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 workflow | Claude Code | Other agents (Codex, Cursor, Aider, Continue, Cline, etc.) |
|---|---|---|
AskUserQuestion (mode selector, per-finding triage, confirmations) | Native multi-choice button UI | Ask 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 tool | Use your agent's progress mechanism, or skip — progress tracking is optional. |
${CLAUDE_SKILL_DIR} env var (path to skill folder) | Auto-set by Claude Code | Substitute 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
--versionfails, 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$PATHwithsudo mv ./auth0 /usr/local/binif 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, downloadauth0-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/binis on$PATH) - Manual — download the release archive for your OS/architecture from the auth0-cli GitHub releases page and extract it; make sure
PATHandHOMEinclude the extraction folder
Autocompletion instructions for supported shells are available by running auth0 completion -h.
- If
tenants listreturns 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:
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:rulesGrant 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.
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
nullwhen 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:
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 \
a0checkmate4.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:
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" sectionThese 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 namestate/setup.json.tenant_domain— canonical tenant URL
- Produce three files following the locked report skeleton:
- A markdown brief following the markdown report template in
assets/audit/ - A styled HTML brief following
assets/audit/report-template.html— same data, render-ready - 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 finding | Minimum plan | Loop |
|---|---|---|
Application Grant Types (remove implicit, Auth Code + PKCE) | Free (app config) | Immediate |
| Cross Origin Authentication (disable) | Free | Immediate |
| Refresh Tokens (enable rotating) | Free | Immediate |
| PKCE Enforcement / Token Sender-Constraining | Free | Immediate |
| Databases – Password Complexity / Email Attribute Verification | Free (connection config) | Immediate |
| Brute Force Protection / Suspicious IP Throttling | Free (all plans) | Immediate |
| Session Management / Tenant Login URI / Allowed Logout URLs | Free (tenant settings) | Immediate |
| Management API Access Control (M2M scopes) | Free | Immediate |
| Custom Domains | Free (1, CC verification) | Immediate |
| Email Providers / Email Templates (Email Workflow) | Essentials | After Upgrading |
| Log Streams | Essentials (1 stream) | After Upgrading |
| MFA factors / Multifactor Policy / MFA for Password Reset† | Essentials (Pro MFA Factors) | After Upgrading |
| Breached Password Detection (blocking) / Enhanced Password Protection | Professional | After Upgrading |
| Security Center | Professional (B2C) | After Upgrading |
| Tenant Access Control List (ACL) | Enterprise (add-on) | After Upgrading |
| OIDC Back-Channel Logout | Enterprise | After Upgrading |
| Private Key JWT / JAR / PAR | Enterprise | After Upgrading |
| CIBA (A4AA) | Essentials + A4AA add-on | After Upgrading |
| Token Vault (A4AA) | Free gets 2; more via A4AA add-on | After 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.
- Header metadata uses the customer's actual
company_name,business_model,product_summary_short, and the derived<recommended_plan>— no template placeholders. - Section 1 metrics show real
apps_countfromauth0 apps list, real CheckMate severity counts, real login activity derived fromauth0 logs list. - 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. - Section 2 opening paragraph lists the customer's products by name (e.g. "Customer Portal, Admin Console, Mobile App") — never "the customer's apps".
- 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.
- Section 4 plan rationale names the customer's products and ties them to the recommended plan's specific features.
- 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"). - 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):
| File | Purpose |
|---|---|
setup.json | cache: tenant_domain, checkmate_client_id, company_domain, last_run_at — secrets excluded |
operator.json | reviewer name + optional org/role for the report header |
enrichment_<domain>_<ts>.json | one per enrichment run |
queue.json | queued / skipped / pending_upgrade items across runs |
history.jsonl | append-only log of applied/queued/skipped findings per run |
Pitfalls to remember
read:hooks/read:rulesare 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_PATHis 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.envis not auto-sourced. The workflow does it in Phase 4; outside the workflow, the user has to eithersourceit 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.
~/Documentsexists by default on macOS and Windows; on Linux it depends on user setup. If~/Documentsdoesn't exist, fall back to~/auth0-checkmate-reports(create it on first write).
- *Shells**
~/.auth0-checkmate.envuses POSIXexportsyntax — works for bash, zsh, dash, ksh, and Git Bash.- Fish shell users: the env file syntax differs (
set -gxinstead ofexport). The workflow writes the POSIX form by default; a fish-specific file at~/.config/fish/conf.d/auth0-checkmate.fishcan be added by the user. Phase 4'ssource ~/.auth0-checkmate.envwon't work in fish — pass env vars inline instead.
- *PDF renderer**
scripts/render_pdf.shsearches Chrome / Chromium / Edge / Brave / Arc on macOS, Linux, and WSL paths automatically. Falls back towkhtmltopdfand thenweasyprintif 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 installas the universal fallback.
- *Self-audit vs CS-audit modes**
reviewer_orgblank → the report subtitle is omitted (clean self-audit look).reviewer_orgpopulated → 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.