Skip to content
Gauge

ax-check.com

Auth0

auth0.com

ax-check.com

Checked 4 November 2026

A

97/100

Agents need your login

Auth0 docs and pricing are clear and agent-ready.

22 of 23 checks passed. Quickstart, SDK install, and pricing tables are all readable without interaction; only the MCP server link 404s.

2 fixes to start with

Concrete changes, backed by what we found.

Onboarding

Give agents a clear login handoff and a way to resume setup.

Agents cannot progress through onboarding without human help. Provide an agent-accessible path through authentication and setup.

Docs

Correct the MCP connection instructions.

llms.txt links the Auth0 MCP Server page, but that URL returns 404 Page Not Found.

Linked from auth0.com/llms.txt

Recorded coding sessions

Same task, 3 models, no auth0.com account. Recorded 4 November 2026 on Pi.

3 recorded · 3m 0s

3m 0s · recorded

Session findings

Select a finding to see it in context.

piv0.84.2
modelQwen 3.8 Maxcwd/sandbox/repo
Help me build a simple example using Auth0. Tell me how pricing works, and briefly tell me whether this product will be easy for you to manage. Let me know if you get blocked. If this product has no developer workflow you can act on, say so plainly and stop. Stay light: use the hosted product through its SDK or API. Do not start local service stacks or wait for long-running commands; if the quickstart requires either, say so plainly and stop.
Thinking…

I need to understand what they're looking for—an Auth0 example using the SDK and API with pricing details, plus whether this is feasible for me to work with. Let me check the repo and see what environment I'm working in, including whether I have network access to pull Auth0 packages.

pwd; ls -la; cat README* 2>/dev/null | head -50; ls /sandbox 2>/dev/null
/sandbox/repo
/sandbox/repo
total 16
drwxr-xr-x 3 root root 4096 Sep 18 08:00 .
drwxr-xr-x 7 root root 4096 Nov  4 18:37 ..
drwxr-xr-x 8 root root 4096 Sep 24 20:05 .git
-rw-r--r-- 1 root root  408 Sep 18 08:00 .gitignore
repo
node -v; npm -v; python3 -V; curl -s -m 8 -o /dev/null -w "%{http_code}\n" https://registry.npmjs.org/auth0 || echo "no-net"
v22.23.4
v22.23.4
10.9.8
Python 3.12.3
200
Thinking…

The repo is basically empty. Network works (npm registry reachable). Let's check auth0 pricing page fetch — probably blocked by JS but let's try quickly.

Plan:

  1. Build a simple Auth0 example: Node.js/Express web app using express-openid-connect (Auth0's own SDK) — that's the hosted-product SDK approach. No local service stack needed. Plus optionally a Management API client example (auth0 npm package) for admin tasks.
  2. Include .env.example, README with quickstart, and a note that creating an Auth0 tenant is a web-console step (I can't do it; user must supply credentials).
  3. Pricing: fetch from auth0.com/pricing quickly; if blocked, report from knowledge and flag it.
  4. Assess "easy for you to manage" — i.e., whether I (the agent) can manage this product: API/SDK-driven, but tenant creation and keys require human/console.

Let me try fetching pricing.

cd /tmp && curl -sL -m 12 -A "Mozilla/5.0" https://auth0.com/pricing -o pricing.html -w "%{http_code}\n"; wc -c pricing.html
200
200
512283 pricing.html
npm view express-openid-connect version dist-tags.latest 2>&1 | head; npm view auth0 version 2>&1 | head -3
version = '3.4.0'
version = '3.4.0'
dist-tags.latest = '3.4.0'
7.7.0
Thinking…

Pricing page fetched. Let's extract text. Use python to strip tags, grep for pricing tiers (Free, B2C Essentials, Professional, Enterprise, MAU).

cd /tmp && python3 - <<'EOF'
import re,html
s=open('pricing.html',encoding='utf-8',errors='ignore').read()
s=re.sub(r'<script.*?</script>','',s,flags=re.S)
s=re.sub(r'<style.*?</style>','',s,flags=re.S)
s=re.sub(r'<[^>]+>','\n',s)
s=html.unescape(s)
lines=[l.strip() for l in s.split('\n')]
lines=[l for l in lines if l]
out=[]
for l in lines:
    if not out or out[-1]!=l: out.append(l)
txt='\n'.join(out)
open('pricing.txt','w').write(txt)
print(len(out))
EOF
grep -n -iE "MAU|/month|\\$[0-9]|free|essentials|professional|enterprise|b2c|b2b|custom domain" /tmp/pricing.txt | head -120
729
729
68:B2B
82:Enterprise
83:B2B SaaS
84:B2B Connect
85:B2C
182:B2B
194:ENTERPRISE CONNECTIVITY
212:Enterprise
213:B2B SaaS
214:B2B Connect
215:B2C
270:Start building for free
273:B2C
274:B2B
288:Free
294:Start building for free
297:1 Custom Domain*
305:1 Enterprise Connection
311:*credit card verification required for custom domains.
313:Essentials
321:Everything in Free, plus...¹
329:¹ Upgrade to B2B to continue using EC, Self-Service SSO, and SCIM.
330:Professional
338:Everything in Essentials, plus...¹
340:Enterprise Multi-Factor Authentication
346:¹ Upgrade to B2B to continue using EC, Self-Service SSO, and SCIM.
347:Enterprise
348:For enterprises that need to scale. Top-tier SLAs, advanced security, white-glove support and more.
351:Everything in Professional plus...
354:Enterprise Rate Limits
355:Enterprise Administration & Support
367:Enterprise add-ons to fit your needs
368:AI Identity for B2B & AI Identity for Commerce
385:Easily facilitate secure communication between your API and both non interactive external clients as well as internal APIs with the flip of a switch and standard based protocols. M2M add-ons are also available on Professional self-service plans.
398:Congrats on building something extraordinary. Let us make your customer login experience awesome so you can focus on everything else. Did we mention our startup program is free for a year?
405:B2C
406:B2B
407:Free
409:Essentials
411:Professional
413:Enterprise
441:Enterprise Connections
457:Select Enterprise Plans
459:Select Enterprise Plans
502:Custom Domains*
557:Enterprise MFA Factors
628:*credit card verification required for custom domains
634:For enterprise questions, you can
654:If I surpass the 25,000 users Free Plan do I need to select a Paid Plan with a higher MAU Quota?
675:The trial period lasts 22 days. After that, the Free Plan gets activated automatically. If you wish to continue using some of the features from our paid plans, please choose one from above.
677:Yes, Auth0 and our network of partners want to see you succeed! We provide a special plan for Startups that includes 100K monthly external active users, Enterprise Identity Providers, Pro MFA & Password Protection and SSO for Auth0 Dashboard free for a whole year. You can also apply for $5,000 in AWS credits. Check the requirements and
695:Get started for free
Thinking…

The prices themselves are apparently not in the static HTML (rendered by JS?). Let's look at sections around lines 288-360 and 640-700 for FAQ details (MAU definition).

sed -n '260,420p' /tmp/pricing.txt
Customer Stories
Customer Stories
FEATURED CONTENT
Report
The Customer Identity
Trends Report
Pricing
Sign up
Let's connect
The first and last auth platform you'll ever need
Flexible pricing for all your apps and AI agents from user 1 to 1,000,000+
Start building for free
Talk to our Sales team
USE CASE
B2C
B2B
BILLING
Monthly
Yearly
How many monthly active users?
?
500
1,000
2,500
5,000
7,500
10,000
20,000
30,000+
Free
$
0
/ month
No credit card needed
to sign up.
Start building for free
Up to 25,000 monthly active users
Includes
1 Custom Domain*
Secure Agentic AI workflows
Passwordless Authentication
Unlimited Social Connections**
5 Organizations
Brand Customization
Basic Attack Protection
Community Support
1 Enterprise Connection
New
Self-Service SSO
New
SCIM
New
*credit card verification required for custom domains.
**subject to system limitations.
Essentials
$
35
/ month
For projects with higher production demands.
Sign up
Up to 500 monthly active users
Includes
Everything in Free, plus...¹
Higher Auth, API limits, and Feature limits
Pro Multi-Factor Authentication
Role-based Access Control Per Organization
10 Organizations (How we model your customers)
Stream Auth0 Audit Logs to Datadog, Splunk, AWS, Azure, etc.
Separate Production & Development Environments
Standard Support
¹ Upgrade to B2B to continue using EC, Self-Service SSO, and SCIM.
Professional
$
240
/ month
Best for teams and projects that need added security.
Sign up
Up to 500 monthly active users
Includes
Everything in Essentials, plus...¹
Use your existing User Database for Logins
Enterprise Multi-Factor Authentication
Custom Token
Exchange
Enhanced Attack Protection
M2M Tokens
Add-on
¹ Upgrade to B2B to continue using EC, Self-Service SSO, and SCIM.
Enterprise
For enterprises that need to scale. Top-tier SLAs, advanced security, white-glove support and more.
Contact us
Includes
Everything in Professional plus...
Custom User & SSO Tiers
99.99% SLA
Enterprise Rate Limits
Enterprise Administration & Support
Advanced Security Features
Add-on
Private Deployment
Add-on
Compare plans
Explore special pricing for
startups
and
nonprofits
Read Customer Stories
→
Enterprise add-ons to fit your needs
AI Identity for B2B & AI Identity for Commerce
Connect your AI agents to apps, APIs, and MCP servers in a more secure manner.  Give users control over the actions AI agents can perform and the data they can access. Enable human confirmation for critical agent actions. All with just a few lines of code.
Read the docs
→
Fine-Grained Authorization
A highly scalable authorization service for developers that helps them implement authorization for any kind of application, and enable user collaboration and granular access control using easy-to-use APIs. Available in Public Cloud and Private Cloud AWS.
Read the docs
Attack Protection
Keep your users and services safe from password leaks, intruders, and scripted attacks from bots. Protect and notify your users when credentials are leaked or during an attempted account takeover. Use risk intelligence enhanced with third-party supplemental signals to detect threats and inform access decisions. With Tenant ACL, use out-of-the-box blocklists of known malicious networks & anonymizers.
Read the docs
Private Cloud
A low-friction, dedicated Auth0 deployment model for enhanced performance, security, and compliance over our standard public cloud offering.
Read the docs
Adaptive MFA
Protect against malicious attacks with minimal impact to your users. Adaptive MFA is designed to only be triggered when a login is suspected to be risky via different risk profiles.
Read the docs
Machine-to-Machine Tokens
Easily facilitate secure communication between your API and both non interactive external clients as well as internal APIs with the flip of a switch and standard based protocols. M2M add-ons are also available on Professional self-service plans.
Read the docs
Highly Regulated Identity
Secure sensitive customer operations beyond login with Financial Grade IdentityTM controls (Strong Customer Authentication, FAPI protocols) and Customer Managed Keys.
Read the docs
Advanced Extensibility
Unlock deep customization to meet your unique needs for Customer Identity. With Actions, Forms, and Auth0 Marketplace, leverage the maximum number of Actions and Forms (within platform performance limits) to customize and extend Identity with both pro- and no-code solutions.
Read the docs
HIPAA BAA & PCI
Auth0 can provide compliance for several standards including HIPAA via a Business Associate Agreement and PCI compliant environments.
Read the docs
Special plans for special organizations
Startups
Congrats on building something extraordinary. Let us make your customer login experience awesome so you can focus on everything else. Did we mention our startup program is free for a year?
Auth0 for Startups
Nonprofits
Our mission is to support your mission. Reduce security and compliance risks, improve your donor and volunteer experience, and enhance your technology investments - all with a simple login box.
Auth0 for Nonprofits
Compare Plans
USE CASE
B2C
B2B
Free
Start building →
Essentials
Sign up
Professional
Sign up
Enterprise
Contact us →
Authentication
External Active Users
Up to 25,000
Custom Tiers Available
Machine-to-Machine Authentication
1,000
sed -n '630,700p' /tmp/pricing.txt; grep -o -iE "\"price[^,]{0,60}|\\$[0-9][0-9,]*" /tmp/pricing.html | sort -u | head -30
Frequently asked questions
Frequently asked questions
Can't find the question you’re looking for? Post on our
community forum
to receive help from our developer support team and the community.
For enterprise questions, you can
contact an expert
.
Why should I trust Auth0 with my users?
Security is our utmost priority. For starters, we follow security best practices for storing user credentials: they are never stored in plain text, and all passwords are hashed and salted using the
bcrypt
algorithm, a state-of-the-art algorithm designed to prevent brute-force attacks even in case of a breach. Furthermore, our implementation of TLS has received an “A+” score in
Qualsys’ SSL Labs SSL Server test
.
We also implement proactive security measures for your projects such as password breach detection, using a huge database of leaked passwords, and brute-force attack detection. Automated rate-limiting checks and denial of service mitigation procedures are also in place.
You can learn more about security at Auth0 at our
security page
.
What happens if I stop using Auth0? How do I know my user's data is safe?
Your data is safe as long as it’s stored in our servers. If you choose to stop using Auth0, exporting your data is a simple matter. Head over to the Auth0 Dashboard and go to the extensions page. Select “User Import/Export” and proceed to enable the extension. If you need help using the extension, check our
docs
.
If you feel the export extension does not fill your needs, the
Auth0 management API
lets you fully inspect all the data from your account. With it, you can export all data to any format.
If I surpass the 25,000 users Free Plan do I need to select a Paid Plan with a higher MAU Quota?
Yes, if you pass the 25,000 users mark, you need to select an applicable paid Plan and select the right amount of users for your use case. Paid plans let you pick a custom number of users, from 1,000 up to 100,000 or more. Several plans are available to best suit the needs of different use cases in addition to providing access to a greater amount of features that increases what you can do with Auth0.
What are the different deployment models?
Auth0 can run as a third-party service on the Auth0 public cloud or in an isolated private deployment. In particular, Auth0 supports
four different types of deployments
:
Public Cloud
: multi-tenant (shared-instance)
Private Cloud Basic
: Dedicated option that builds on Public Cloud performance and management that addresses specific data residency needs
Private Cloud Performance
: Includes all Private Cloud Basic capabilities and increases requests per second (RPS) to 500, and includes options for upgrade control, load testing, and a GEO-HA add-on.
Private Cloud Performance Plus
: Includes all Private Cloud Performance capabilities and increases RPS to 1,500
What's Auth0's uptime and status?
Auth0 systems have over 99.99% monthly uptime figures for most regions. You can check the current and past status and uptime figures at the
status
and
uptime
pages.
How long does my trial last?
The trial period lasts 22 days. After that, the Free Plan gets activated automatically. If you wish to continue using some of the features from our paid plans, please choose one from above.
Do you have any special offers for Startups?
Yes, Auth0 and our network of partners want to see you succeed! We provide a special plan for Startups that includes 100K monthly external active users, Enterprise Identity Providers, Pro MFA & Password Protection and SSO for Auth0 Dashboard free for a whole year. You can also apply for $5,000 in AWS credits. Check the requirements and
apply here
!
How do I get Special Pricing and Services for Nonprofits and Charitable Organizations?
We are committed to Secure Access For Everyone and making the world a SAFEr place. With our
Social Impact Program
, we want to make sure your technology is running alongside your ambitions from day one. All paid plans are eligible for a Social Impact discount.
Request the Nonprofit and Social Good Benefits and Pricing Sheet
here
which includes the Criteria and Validation Steps.
Not sure which plan is right for you?
Talk to an expert
STARTING WITH AUTH0
Auth0 CIAM
Intro to IAM
Auth0 platform
Auth0 features
Pricing
Get started for free
DOCUMENTATION
Documentation
Quickstarts
APIs
SDK Libraries
"price": "0"
"priceCurrency": "USD"
"priceValidUntil": "2026-12-31"
-9
-96
-97
-98
-99
Thinking…

Good — real pricing data fetched today. B2C monthly: Free $0 (up to 25k MAU), Essentials $35/mo (up to 500 MAU at that slider position — prices scale with MAU slider), Professional $240/mo, Enterprise custom. Note prices vary by MAU count selected (the 500 MAU tier shown). Let me also grab B2B tab numbers? That's JS-rendered toggle; probably same HTML includes both. Let me check comparison table lines 405-630 for feature limits — good enough.

Now build the example. Keep it light: Node + Express + express-openid-connect (Auth0's official Node SDK) for a web app, plus a small Management API script using auth0 npm package (v4/v5? version 7.7.0 now — that's the new versioning? Actually auth0 npm package latest is 4.x historically... npm view says 7.7.0. Hmm, Auth0 node SDK moved to 4.x. 7.7.0? Let me check the package description to make sure it's the right one.)

Also, note I can't create the tenant — user must do that in the dashboard (or I could use Management API only if they give credentials). I should not run the server (long-running). I'll do a quick syntax check instead (node --check).

Let me check auth0 package identity.

npm view auth0 name description homepage license 2>&1 | head -10; npm view express-openid-connect description deprecated 2>&1 | head -5
name = 'auth0'
name = 'auth0'
description = 'Auth0 Node.js SDK for the Management API v2.'
homepage = 'https://github.com/auth0/node-auth0'
license = 'MIT'
Express middleware to protect web applications using OpenID Connect.
sed -n '420,560p' /tmp/pricing.txt
1,000
1,000
5,000
M2M Add-on
No
Yes
Passwordless
Included
Social Connections
Unlimited**
Custom Social Connections
Included
Passkeys
Included
Auth0 Database Connection
Included
Custom Database Connections
Not included
Included
Cross APP SSO
Not included
Included
Enterprise Connections
1
Custom Tiers Available
Inbound SCIM
Included
Not included
Included
Okta Connections
Unlimited**
Express Configuration
Included
Organizations
5
10
Custom Tiers Available
Self-Service SSO
Select Enterprise Plans
Machine-to-Machine Access for Organizations
Select Enterprise Plans
Home Realm Discovery
Not included
Included
Long Lived Sessions
Not included
Included
Custom Token Exchange
Not included
Included
AI
CIBA
ADD-ON
+
Included
ADD-ON
Token Vault
2
3
+
ADD-ON
3
+
ADD-ON
4
+
ADD-ON
Auth for MCP
Included
+
Included
ADD-ON
+
Included
ADD-ON
+
Included
ADD-ON
Branding
Configurable Login Experience
Included
Accessibility
Included
Custom Domains*
1
20
+
ADD-ON
Email Workflow
Not included
Included
Customize Signup & Login
Not included
Included
Extensibility
Actions + Forms
5
10
15
30
+
ADD-ON
The Actions Library
Included
Event Streams
1
3
4
8
Marketplace
Included
Pro Forms
Not included
Included
Security & Compliance
Brute Force Protection
Included
Suspicious IP Throttling
Included
Enhanced Password Protection
Not included
Included
Basic Breached Password Detection
Not included
Included
Credential Guard
ADD-ON
Bot Detection
ADD-ON
Tenant Access Control List (ACL)
1
+
ADD-ON
Integration with Okta Universal Logout
Included
Pro MFA Factors
Not included
Included
Enterprise MFA Factors
Included
Adaptive MFA
ADD-ON
Thinking…

Now build the example project. Structure:

auth0-example/
  package.json
  .env.example
  .gitignore
  README.md
  src/server.js          # Express + express-openid-connect (hosted login page)
  src/routes.js? keep simple
  views? use inline HTML strings to stay light (no template engine)
  scripts/tenant-check.js # Management API: list clients/users, create an app + API via API (dev workflow I can act on)
  scripts/provision.mjs   # creates Application + API via Management API (idempotent), prints env values
  public/app.js? optional SPA token handling — keep minimal: show how to call protected API with access token.

Keep it light but useful:

  • server.js: login/logout/profile + a protected API endpoint requiring an access token with audience (using express-oauth2-jwt-bearer? That's another dep). Simpler: server-side session app + /api/me protected by session, plus separate resource-server example using express-oauth2-jwt-bearer to verify JWT. I'll include express-oauth2-jwt-bearer since it's official and small — gives a complete picture (login app + API).

Also a provisioning script using auth0 SDK (Management API) — that's the developer workflow I can act on given credentials, i.e., I can create the app, API, and print config via API rather than clicking the dashboard. That directly answers "easy for you to manage".

Install deps and run node --check / dry-run without starting server. Also can run a quick unit-ish check: start server briefly? Instruction says don't wait for long-running commands; I can do a quick smoke test with a 3-second timeout — that's borderline. Better: use node --check and a config-validation dry run (node scripts/... --check), no server start. Maybe a tiny smoke test that imports the app module without listening. I'll structure server.js to export app and only listen when run directly, then smoke-test by importing app and making a supertest-less request via http? Skip; just verify app builds via importing (which requires valid env). I'll make auth config load lazily with defaults so import works with placeholder env. Simpler: smoke test = node --check on all files + npm ls.

Let's write files.

package.json deps: express, express-openid-connect, express-oauth2-jwt-bearer, auth0, dotenv.

Check express-openid-connect v3 requires express 4? It supports express 4/5? v3.4.0 — peer deps likely express >=4. Let me install and see.

Write the code.

server.js:

import 'dotenv/config';
import express from 'express';
import { auth, requiresAuth } from 'express-openid-connect';
...

Use ESM ("type": "module").

Config:

const config = {
  authRequired: false,
  auth0Logout: true,
  baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
  clientID: process.env.AUTH0_CLIENT_ID,
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  secret: process.env.SESSION_SECRET,
  authorizationParams: {
    audience: process.env.AUTH0_AUDIENCE,
    scope: 'openid profile email offline_access',
    response_type: 'code',
  },
};

Routes: / public, /profile requiresAuth, /api/me returns access token claims? Use req.oidc.accessToken.

api.js (resource server) using auth0 middleware:

import { auth } from 'express-oauth2-jwt-bearer';

Mount in same server on /api for simplicity: app.use('/api', jwtCheck, apiRouter). Good — one process, two roles demonstrated.

scripts/provision.mjs: uses ManagementApplication/ManagementClient from 'auth0' v4+:

import { ManagementClient } from 'auth0';
const mgmt = new ManagementClient({ domain, clientId, clientSecret, audience: `https://${domain}/api/v2/` });

Steps: ensure API (audience), ensure web app (RegularWebApplication) with redirect urls, print .env. Use idempotent finds by name. Support --dry-run to print plan without network (so I can run it now safely).

Also scripts/tenant-check.mjs: prints plan-ish info? Management API doesn't expose plan/pricing. Could print limits via /api/v2/stats? There's /api/v2/jobs... There is GET /api/v2/tenants/settings and daily stats /api/v2/stats/daily (logins), and active users count isn't in API directly. Keep tenant-check simple: list applications, APIs, users count, connections — useful for managing.

Write README with quickstart, pricing summary, and an "agent-manageability" note.

Let's write files now.

Repo is empty and npm/network work, so I'll scaffold a light Auth0 example (hosted login via Auth0's own SDKs + Management API scripts). No local service stack needed.

File edits

1 file

Completed
Writtenpackage.jsonView changes

/sandbox/repo/package.json

Written content · previous contents were not recorded

{
"name": "auth0-simple-example",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Minimal Auth0 example: hosted login page for a web app + JWT-protected API + Management API scripts",
"scripts": {
"start": "node src/server.js",
"check": "node --check src/server.js && node scripts/provision.mjs --dry-run",
"provision": "node scripts/provision.mjs",
"tenant": "node scripts/tenant-check.mjs"
},
"dependencies": {
"auth0": "^4.10.0",
"dotenv": "^16.4.5",
"express": "^4.21.0",
"express-oauth2-jwt-bearer": "^1.6.0",
"express-openid-connect": "^3.4.0"
}
}

File edits

1 file

Completed
Written.env.exampleView changes

/sandbox/repo/.env.example

Written content · previous contents were not recorded

# --- Runtime config for the sample app (src/server.js) ---
# Your Auth0 tenant domain, e.g. my-tenant.us.auth0.com
AUTH0_DOMAIN=your-tenant.us.auth0.com
# Native "Regular Web Application" client in your tenant (created by scripts/provision.mjs)
AUTH0_CLIENT_ID=
AUTH0_CLIENT_SECRET=
# Identifier of the API resource the app requests tokens for (created by scripts/provision.mjs)
AUTH0_AUDIENCE=https://api.example.com
# URL this app is reachable at; must match an allowed callback URL in Auth0
BASE_URL=http://localhost:3000
# Random string used to encrypt the local session cookie
SESSION_SECRET=change-me-to-a-long-random-string
# --- Management API credentials, only needed by scripts/*.mjs ---
# Use a dedicated "Management API" machine-to-machine app with read/create scopes:
# read:clients create:clients read:resource_servers create:resource_servers read:users read:connections
AUTH0_MANAGEMENT_CLIENT_ID=
AUTH0_MANAGEMENT_CLIENT_SECRET=
Thinking…

Check auth0 package latest major: npm view said 7.7.0? Earlier npm view auth0 version returned 7.7.0. Hmm that was output "7.7.0". Wait, the first output block: npm view express-openid-connect version dist-tags.latest gave version=3.4.0 twice; then npm view auth0 version gave 7.7.0. So auth0 SDK latest is 7.7.0? Hmm, node-auth0 released v4.x in 2024... but maybe it's now v4.10 or higher. It says 7.7.0 — could be. Let me pin ^7.7.0 and check API surface after install. Actually let me just install and inspect.

cd /sandbox/repo && npm view auth0 version dist-tags.latest time.modified 2>&1 | head; npm view express-oauth2-jwt-bearer version 2>&1|head -2
version = '7.7.0'
version = '7.7.0'
dist-tags.latest = '7.7.0'
time.modified = '2026-11-04T10:48:51.056Z'
1.11.0
Thinking…

auth0 SDK is 7.7.0. Update package.json to ^7.7.0. Install and inspect API (ManagementClient usage may have changed in v5+: still new ManagementClient({domain, clientId, clientSecret}) with methods like clients.create, resourceServers.create). Let's install then verify.

cd /sandbox/repo && sed -i 's/"auth0": "\^4.10.0"/"auth0": "^7.7.0"/; s/"express-oauth2-jwt-bearer": "\^1.6.0"/"express-oauth2-jwt-bearer": "^1.11.0"/' package.json && npm install --no-audit --no-fund 2>&1 | tail -5
added 174 packages in 12s

added 174 packages in 12s
Thinking…

Wait — express-openid-connect v3 with express 4: fine. Let's verify installed versions and check the auth0 SDK exports/API.

cd /sandbox/repo && npm ls --depth=0 2>&1 | head -12; node -e "
import('auth0').then(m=>{const {ManagementClient}=m; const c=new ManagementClient({domain:'x.us.auth0.com',clientId:'a',clientSecret:'b'}); console.log('methods sample:', Object.getOwnPropertyNames(Object.getPrototypeOf(c)).slice(0,25).join(', ')); console.log('has clients:', !!c.clients, 'has resourceServers:', !!c.resourceServers); console.log('clients.create:', typeof c.clients?.create, 'resourceServers.create:', typeof c.resourceServers?.create, 'getAll:', typeof c.clients?.getAll);})
"
auth0-simple-example@1.0.0 /sandbox/repo
auth0-simple-example@1.0.0 /sandbox/repo
├── auth0@7.7.0
├── dotenv@16.6.1
├── express-oauth2-jwt-bearer@1.11.0
├── express-openid-connect@3.4.0
└── express@4.22.3

methods sample: constructor
has clients: true has resourceServers: true
clients.create: function resourceServers.create: function getAll: undefined
Thinking…

clients.getAll undefined — in v5+, the managers are lazily created proxies with methods like getAll? Let's inspect available methods on c.clients.

cd /sandbox/repo && node -e "
import('auth0').then(m=>{const {ManagementClient}=m; const c=new ManagementClient({domain:'x.us.auth0.com',clientId:'a',clientSecret:'b'});
const keys=o=>Object.getOwnPropertyNames(Object.getPrototypeOf(o));
console.log('clients:', keys(c.clients).join(','));
console.log('resourceServers:', keys(c.resourceServers).join(','));
console.log('users:', keys(c.users).join(','));
console.log('connections:', keys(c.connections).join(','));
})"
clients: constructor,credentials,connections,list,create,__create,previewCimdMetadata,__previewCimdMetadata,registerCimdClient,__registerCimdClient,search,get,_
clients: constructor,credentials,connections,list,create,__create,previewCimdMetadata,__previewCimdMetadata,registerCimdClient,__registerCimdClient,search,get,__get,delete,__delete,update,__update,rotateSecret,__rotateSecret
resourceServers: constructor,list,create,__create,search,get,__get,delete,__delete,update,__update
users: constructor,authenticationMethods,authenticators,connectedAccounts,effectivePermissions,effectiveRoles,enrollments,groups,identities,logs,multifactor,organizations,permissions,riskAssessments,roles,refreshToken,sessions,list,create,__create,listUsersByEmail,__listUsersByEmail,get,__get,delete,__delete,update,__update,listConsentSigningKeys,__listConsentSigningKeys,getConsentSigningKey,__getConsentSigningKey,deleteConsentSigningKey,__deleteConsentSigningKey,regenerateRecoveryCode,__regenerateRecoveryCode,revokeAccess,__revokeAccess
connections: constructor,directoryProvisioning,scimConfiguration,clients,keys,users,list,create,__create,get,__get,delete,__delete,update,__update,checkStatus,__checkStatus
Thinking…

I need to note that the v7 API uses list instead of getAll, then move on to writing the source files.

File edits

1 file

Completed
Writtensrc/server.jsView changes

/sandbox/repo/src/server.js

Written content · previous contents were not recorded

// Minimal Auth0 example.
//
// One process, two roles:
// 1. Web app -> users log in on Auth0's *hosted* login page (we never handle passwords).
// Session/cookie handling is done by Auth0's official `express-openid-connect` SDK.
// 2. API -> /api/* is a resource server that verifies Auth0-issued JWTs with
// `express-oauth2-jwt-bearer` (no SDK state, just JWKS verification).
//
// Nothing runs locally except this process: all auth happens on the hosted Auth0 tenant.
import 'dotenv/config';
import express from 'express';
import { auth, requiresAuth } from 'express-openid-connect';
import { auth as jwtAuth } from 'express-oauth2-jwt-bearer';
const {
AUTH0_DOMAIN,
AUTH0_CLIENT_ID,
AUTH0_CLIENT_SECRET,
AUTH0_AUDIENCE = 'https://api.example.com',
BASE_URL = 'http://localhost:3000',
SESSION_SECRET = 'dev-only-insecure-secret',
PORT = 3000,
} = process.env;
const issuerBaseURL = `https://${AUTH0_DOMAIN}`;
const app = express();
// --- 1. Web app: OpenID Connect against the Auth0 tenant -------------------
app.use(
auth({
authRequired: false, // "/" stays public; protect routes with requiresAuth()
auth0Logout: true, // logging out here also ends the Auth0 session
baseURL: BASE_URL,
clientID: AUTH0_CLIENT_ID,
clientSecret: AUTH0_CLIENT_SECRET, // confidential client (Regular Web Application)
issuerBaseURL,
secret: SESSION_SECRET,
authorizationParams: {
response_type: 'code',
audience: AUTH0_AUDIENCE, // ask for an access token for our API
scope: 'openid profile email offline_access',
},
})
);
const page = (title, body) =>
`<!doctype html><html><head><meta charset="utf-8"><title>${title}</title>
<style>body{font:16px system-ui;margin:3rem auto;max-width:44rem}pre{background:#f5f5f5;padding:1rem;overflow:auto}
a{margin-right:1rem}</style></head><body>${body}</body></html>`;
app.get('/', (req, res) => {
const user = req.oidc.user;
res.send(
page(
'Auth0 example',
user
? `<p>Signed in as <strong>${user.nickname ?? user.name}</strong>.</p>
<a href="/profile">Profile</a><a href="/api/me">Call protected API (session)</a>
<a href="/token">Show access token</a><a href="/logout">Log out</a>`
: `<p>Not signed in.</p><a href="/login">Log in</a>
<p><em>/api/me</em> is a separate resource server: it needs a Bearer access token,
not this cookie session.</em></p>`
)
);
});
app.get('/profile', requiresAuth(), (req, res) => {
res.send(page('Profile', `<pre>${JSON.stringify(req.oidc.user, null, 2)}</pre><a href="/">Back</a>`));
});
// The SDK stores the access token (and refreshes it via offline_access) for us.
app.get('/token', requiresAuth(), async (req, res) => {
const token = await req.oidc.accessToken;
res.send(
page(
'Access token',
`<p>Use it as <code>Authorization: Bearer &lt;token&gt;</code> against /api/*.</p>
<pre>${token?.access_token ?? '(no access token - check AUTH0_AUDIENCE)'}</pre>
<a href="/">Back</a>`
)
);
});
// --- 2. Resource server: verify Auth0 JWTs -------------------------------
const checkJwt = jwtAuth({
auth0Domain: AUTH0_DOMAIN,
audience: AUTH0_AUDIENCE,
issuerBaseURL, // validates iss, aud, exp, signature against the tenant JWKS
});
app.get('/api/me', checkJwt, (req, res) => {
res.json({
message: 'Hello from the protected API',
subject: req.auth.sub,
scopes: req.auth.scope?.split(' ') ?? [],
permissions: req.auth.permissions ?? [],
});
});
// Same endpoint, but reached through the browser session: the SDK supplies the Bearer token.
app.get('/api/me-via-session', requiresAuth(), async (req, res) => {
const token = await req.oidc.accessToken;
const r = await fetch(`${BASE_URL}/api/me`, {
headers: { authorization: `Bearer ${token.access_token}` },
});
res.json(await r.json());
});
app.get('/healthz', (_req, res) => res.json({ ok: true }));
const start = () =>
app.listen(PORT, () => console.log(`listening on ${BASE_URL} (port ${PORT})`));
// Only listen when run directly, so tests/scripts can import the app.
if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/^.*\//, '/'))) {
start();
}
export { app, start };
Thinking…

Wait: express-openid-connect v3 requires config at middleware creation time; if AUTH0_DOMAIN is undefined, https://undefined — the SDK may throw at startup. That's fine for real use but breaks smoke test import. I'll add a validation that throws clear error if missing, and allow smoke test with placeholder env vars set. Let's add a small config check.

Also import.meta.url.endsWith(...) trick is fragile. Simpler: if (process.env.NODE_ENV !== 'test') or check import.meta.url === pathToFileURL(process.argv[1]).href. Use node:url pathToFileURL.

Let me fix those.

File edits

1 file

Completed
Modifiedsrc/server.js+10−2View changes

/sandbox/repo/src/server.js

Replacement excerpt

import 'dotenv/config';
+import { pathToFileURL } from 'node:url';
import express from 'express';

Replacement excerpt

+if (!AUTH0_DOMAIN || !AUTH0_CLIENT_ID || !SESSION_SECRET) {
+ throw new Error(
+ 'Missing config. Copy .env.example to .env and fill in AUTH0_DOMAIN, AUTH0_CLIENT_ID and SESSION_SECRET.\n' +
+ 'Tip: `npm run provision` creates the application/API in your tenant and prints these values.'
+ );
+}
+
const issuerBaseURL = `https://${AUTH0_DOMAIN}`;
const app = express();

Replacement excerpt

-// Only listen when run directly, so tests/scripts can import the app.
-if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/^.*\//, '/'))) {
+// Only listen when run directly, so scripts/tests can import the app without opening a port.
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
start();
}
Thinking…

Also fix the stray </em> in the HTML string. And fix double </em>. Let's edit.

Now write scripts/provision.mjs and scripts/tenant-check.mjs.

provision.mjs: uses ManagementClient with M2M creds; ensures API resource server + regular web application; prints .env lines. Supports --dry-run (no network).

In auth0 v7, clients.list({include_totals:true}), clients.create({name, app_type:'regular_web', callbacks, allowed_logout_urls}), then clients.rotateSecret? To get the secret: create returns client_secret. Yes create response includes client_secret.

resourceServers.create({name, identifier, signing_alg:'RS256', token_lifetime, scopes}).

Also need clients.list search by name: use clients.list({q: 'name:"x"', search_engine:'v3'})? Simpler: list all (up to per_page 100) and find by name.

Write it.

cd /sandbox/repo && python3 - <<'EOF'
p='src/server.js'; s=open(p).read()
s=s.replace("""           not this cookie session.</em></p>`""","""           not this cookie session.</p>`""")
s=s.replace("""           <p><em>/api/me</em> is a separate resource server: it needs a Bearer access token,""","""           <p><em>/api/me</em> is a separate resource server: it needs a Bearer access token,""")
open(p,'w').write(s)
EOF
grep -n "em>" src/server.js
71: <p><em>/api/me</em> is a separate resource server: it needs a Bearer access token,
71:           <p><em>/api/me</em> is a separate resource server: it needs a Bearer access token,
Thinking…

I need to continue from where I left off. Now I'm writing the two provision and tenant-check scripts, then updating the README with pricing information and agent-manageability details, followed by a smoke test that checks all files with Node, runs the provision command in dry-run mode, and imports the app with placeholder environment variables.

File edits

1 file

Completed
Writtenscripts/provision.mjsView changes

/sandbox/repo/scripts/provision.mjs

Written content · previous contents were not recorded

// Provisions the two things this example needs in an Auth0 tenant, via the Management API
// (so no dashboard clicking):
// - an API (resource server) with identifier = AUTH0_AUDIENCE
// - a Regular Web Application whose allowed callback/logout URLs match BASE_URL
//
// Idempotent: existing objects with the same name/identifier are reused.
//
// npm run provision # talk to Auth0 and create what is missing
// npm run provision -- --dry-run # print the plan, no network calls
//
// Requires AUTH0_MANAGEMENT_CLIENT_ID / _SECRET from an M2M app authorized on the
// Auth0 Management API with: read:clients create:clients read:resource_servers create:resource_servers
import 'dotenv/config';
import { ManagementClient } from 'auth0';
const dryRun = process.argv.includes('--dry-run');
const APP_NAME = process.env.AUTH0_APP_NAME ?? 'auth0-simple-example';
const API_NAME = process.env.AUTH0_API_NAME ?? 'auth0-simple-example-api';
const AUDIENCE = process.env.AUTH0_AUDIENCE ?? 'https://api.example.com';
const BASE_URL = process.env.BASE_URL ?? 'http://localhost:3000';
const { AUTH0_DOMAIN, AUTH0_MANAGEMENT_CLIENT_ID, AUTH0_MANAGEMENT_CLIENT_SECRET } = process.env;
const callbacks = [`${BASE_URL}/callback`];
const logoutUrls = [BASE_URL];
if (dryRun) {
console.log('DRY RUN - no Auth0 calls made. Would ensure:');
console.log(` API name=${API_NAME} identifier=${AUDIENCE} signing=RS256`);
console.log(` Client name=${APP_NAME} app_type=regular_web callbacks=${callbacks} logout=${logoutUrls}`);
console.log(`\nThen write into .env: AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_AUDIENCE=${AUDIENCE}`);
process.exit(0);
}
if (!AUTH0_DOMAIN || !AUTH0_MANAGEMENT_CLIENT_ID || !AUTH0_MANAGEMENT_CLIENT_SECRET) {
console.error(
'Need AUTH0_DOMAIN, AUTH0_MANAGEMENT_CLIENT_ID, AUTH0_MANAGEMENT_CLIENT_SECRET in .env\n' +
'(create an M2M app in the dashboard, authorize it on the "Auth0 Management API", ' +
'grant the scopes listed at the top of this file).'
);
process.exit(1);
}
const mgmt = new ManagementClient({
domain: AUTH0_DOMAIN,
clientId: AUTH0_MANAGEMENT_CLIENT_ID,
clientSecret: AUTH0_MANAGEMENT_CLIENT_SECRET,
scope: 'read:clients create:clients read:resource_servers create:resource_servers',
});
const ensureApi = async () => {
const existing = await mgmt.resourceServers.list({ per_page: 100 });
const found = existing.data?.find((rs) => rs.identifier === AUDIENCE);
if (found) {
console.log(`= API already exists: ${found.name} (${found.identifier})`);
return found;
}
const created = await mgmt.resourceServers.create({
name: API_NAME,
identifier: AUDIENCE,
signing_alg: 'RS256',
token_lifetime: 86400,
scopes: [{ value: 'read:me', description: 'Read own profile from the example API' }],
});
console.log(`+ created API: ${created.data.name} (${created.data.identifier})`);
return created.data;
};
const ensureClient = async () => {
const existing = await mgmt.clients.list({ per_page: 100 });
const found = existing.data?.find((c) => c.name === APP_NAME);
if (found) {
console.log(`= client already exists: ${found.name} (${found.client_id})`);
console.log(' (client_secret is not re-readable; rotate it in the dashboard if lost)');
return { ...found, client_secret: process.env.AUTH0_CLIENT_SECRET ?? '<already-set-or-rotate>' };
}
const created = await mgmt.clients.create({
name: APP_NAME,
app_type: 'regular_web',
callbacks,
allowed_logout_urls: logoutUrls,
jwt_configuration: { alg: 'RS256', lifetime_in_seconds: 36000 },
token_endpoint_auth_method: 'client_secret_post',
});
console.log(`+ created client: ${created.data.name} (${created.data.client_id})`);
return created.data;
};
const api = await ensureApi();
const client = await ensureClient();
console.log('\n--- paste into .env ----------------------------------------');
console.log(`AUTH0_DOMAIN=${AUTH0_DOMAIN}`);
console.log(`AUTH0_CLIENT_ID=${client.client_id}`);
console.log(`AUTH0_CLIENT_SECRET=${client.client_secret}`);
console.log(`AUTH0_AUDIENCE=${api.identifier}`);
console.log(`BASE_URL=${BASE_URL}`);
console.log('------------------------------------------------------------');
console.log('\nAllowed callback URL registered: ' + callbacks.join(', '));
console.log('Next: npm start, then open ' + BASE_URL);

File edits

1 file

Completed
Writtenscripts/tenant-check.mjsView changes

/sandbox/repo/scripts/tenant-check.mjs

Written content · previous contents were not recorded

// Read-only snapshot of the tenant, handy for day-2 management (all via Management API):
// applications, APIs, connections, user counts, recent logins.
//
// npm run tenant
//
// Scopes needed: read:clients read:resource_servers read:connections read:users read:logs
import 'dotenv/config';
import { ManagementClient } from 'auth0';
const { AUTH0_DOMAIN, AUTH0_MANAGEMENT_CLIENT_ID, AUTH0_MANAGEMENT_CLIENT_SECRET } = process.env;
if (!AUTH0_DOMAIN || !AUTH0_MANAGEMENT_CLIENT_ID || !AUTH0_MANAGEMENT_CLIENT_SECRET) {
console.error('Need AUTH0_DOMAIN + AUTH0_MANAGEMENT_CLIENT_ID/_SECRET in .env');
process.exit(1);
}
const mgmt = new ManagementClient({
domain: AUTH0_DOMAIN,
clientId: AUTH0_MANAGEMENT_CLIENT_ID,
clientSecret: AUTH0_MANAGEMENT_CLIENT_SECRET,
});
const show = (label, rows, fields) => {
console.log(`\n## ${label} (${rows.length})`);
for (const r of rows) console.log(' ' + fields.map((f) => r[f] ?? '-').join(' | '));
};
const clients = (await mgmt.clients.list({ per_page: 100 })).data ?? [];
show('Applications', clients, ['name', 'client_id', 'app_type']);
const apis = (await mgmt.resourceServers.list({ per_page: 100 })).data ?? [];
show('APIs', apis, ['name', 'identifier', 'signing_alg']);
const connections = (await mgmt.connections.list({ per_page: 100 })).data ?? [];
show('Connections', connections, ['name', 'strategy', 'enabled_clients']);
// Total user count: ask for 1 row and read the pagination total.
const users = await mgmt.users.list({ per_page: 1, include_totals: true });
console.log(`\n## Users: ${users.data.total ?? 'unknown'}`);
const logs = (await mgmt.logs.list({ per_page: 5 })).data ?? [];
show('Recent log events', logs, ['type', 'date', 'description']);
Thinking…

Now smoke test: node --check each file (for ESM, node --check works for .mjs? It parses as module for .mjs; for .js with type:module, node --check may treat as CommonJS... Let's instead do dynamic import with placeholder env vars, and run provision --dry-run.

Test:

  • npm run provision -- --dry-run
  • import src/server.js with env placeholders (should construct middleware without network).
  • tenant-check with missing env → exits 1 gracefully.

Also add a tiny test file? Keep light — I'll add tests/smoke.mjs that imports app and hits /healthz and /api/me without token (expect 401) using node's fetch against an ephemeral server. That's short-lived (a second), acceptable and not a "long-running command". I'll do it as a one-off verification but also keep the file as tests/smoke.mjs since it's useful.

File edits

1 file

Completed
Writtentests/smoke.mjsView changes

/sandbox/repo/tests/smoke.mjs

Written content · previous contents were not recorded

// Fast smoke test: boots the app on an ephemeral port, checks the public route,
// the JWT-protected route without a token (401), and /healthz. Exits immediately.
//
// npm test
//
// Uses placeholder tenant values: no network calls to Auth0 are made because
// no valid Bearer token is presented.
process.env.AUTH0_DOMAIN ||= 'example.us.auth0.com';
process.env.AUTH0_CLIENT_ID ||= 'smoke-test-client-id';
process.env.AUTH0_CLIENT_SECRET ||= 'smoke-test-client-secret';
process.env.AUTH0_AUDIENCE ||= 'https://api.example.com';
process.env.SESSION_SECRET ||= 'smoke-test-secret';
const { app } = await import('../src/server.js');
const server = app.listen(0);
await new Promise((r) => server.once('listening', r));
const base = `http://127.0.0.1:${server.address().port}`;
let failures = 0;
const check = async (path, want, headers = {}) => {
const res = await fetch(base + path, { headers, redirect: 'manual' });
const ok = res.status === want;
if (!ok) failures++;
console.log(`${ok ? 'PASS' : 'FAIL'} ${path} -> ${res.status} (expected ${want})`);
return res;
};
await check('/healthz', 200);
await check('/', 200);
await check('/api/me', 401); // no Bearer token
await check('/api/me', 401, { authorization: 'Bearer not-a-real-jwt' });
server.close();
console.log(failures === 0 ? '\nall good' : `\n${failures} failure(s)`);
process.exit(failures === 0 ? 0 : 1);
Thinking…

I need to update package.json with a test script and then run the smoke tests.

cd /sandbox/repo && sed -i 's|"tenant": "node scripts/tenant-check.mjs"|"tenant": "node scripts/tenant-check.mjs",\n    "test": "node tests/smoke.mjs"|' package.json && npm test 2>&1 | tail -20
> auth0-simple-example@1.0.0 test

> auth0-simple-example@1.0.0 test
> node tests/smoke.mjs

file:///sandbox/repo/src/server.js:14
import { auth, requiresAuth } from 'express-openid-connect';
               ^^^^^^^^^^^^
SyntaxError: Named export 'requiresAuth' not found. The requested module 'express-openid-connect' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'express-openid-connect';
const { auth, requiresAuth } = pkg;

    at ModuleJob._instantiate (node:internal/modules/esm/module_job:226:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:335:5)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:681:26)
    at async file:///sandbox/repo/tests/smoke.mjs:15:17

Node.js v22.23.4
cd /sandbox/repo && npm run provision -- --dry-run 2>&1 | tail -12; echo "--- tenant-check with no creds ---"; node scripts/tenant-check.mjs; echo "exit=$?"
> auth0-simple-example@1.0.0 provision

> auth0-simple-example@1.0.0 provision
> node scripts/provision.mjs --dry-run

DRY RUN - no Auth0 calls made. Would ensure:
  API     name=auth0-simple-example-api identifier=https://api.example.com signing=RS256
  Client  name=auth0-simple-example app_type=regular_web callbacks=http://localhost:3000/callback logout=http://localhost:3000

Then write into .env: AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_AUDIENCE=https://api.example.com
--- tenant-check with no creds ---
Need AUTH0_DOMAIN + AUTH0_MANAGEMENT_CLIENT_ID/_SECRET in .env
exit=1
Thinking…

Fix ESM import for express-openid-connect (CJS) — use default import destructure. Also express-oauth2-jwt-bearer likely ESM-friendly? It's CJS too maybe; named import auth failed only for openid-connect so far. Fix both defensively.

File edits

1 file

Completed
Modifiedsrc/server.js+5−2View changes

/sandbox/repo/src/server.js

Replacement excerpt

import express from 'express';
-import { auth, requiresAuth } from 'express-openid-connect';
-import { auth as jwtAuth } from 'express-oauth2-jwt-bearer';
+import oidc from 'express-openid-connect'; // CJS package: destructure from default export
+import jwtBearer from 'express-oauth2-jwt-bearer';
+
+const { auth, requiresAuth } = oidc;
+const { auth: jwtAuth } = jwtBearer;
Thinking…

I should continue with the task at hand.

cd /sandbox/repo && npm test 2>&1 | tail -15
PASS /api/me -> 401 (expected 401)
PASS  /api/me -> 401 (expected 401)
InvalidTokenError: Invalid Token or Protected Header formatting
    at /sandbox/repo/node_modules/express-oauth2-jwt-bearer/dist/index.js:593:19
    at Object.verify (/sandbox/repo/node_modules/express-oauth2-jwt-bearer/dist/index.js:1000:35)
    at /sandbox/repo/node_modules/express-oauth2-jwt-bearer/dist/index.js:1252:39
    at Layer.handle [as handle_request] (/sandbox/repo/node_modules/express/lib/router/layer.js:95:5)
    at next (/sandbox/repo/node_modules/express/lib/router/route.js:149:13)
    at Route.dispatch (/sandbox/repo/node_modules/express/lib/router/route.js:119:3)
    at Layer.handle [as handle_request] (/sandbox/repo/node_modules/express/lib/router/layer.js:95:5)
    at /sandbox/repo/node_modules/express/lib/router/index.js:284:15
    at Function.process_params (/sandbox/repo/node_modules/express/lib/router/index.js:346:12)
    at next (/sandbox/repo/node_modules/express/lib/router/index.js:280:10)
PASS  /api/me -> 401 (expected 401)

all good
Thinking…

All pass. The InvalidTokenError stack is just the SDK logging the invalid token error — it returns 401 correctly but prints noise. We could silence by handling the error, but it's expected behavior. Maybe add a small error handler to keep logs clean:

import { InvalidTokenError } from 'express-oauth2-jwt-bearer';
app.use((err, req, res, next) => { if (err instanceof InvalidTokenError) return res.status(401).json({error: err.message}); next(err); });

Wait, does the middleware log or does express default handler log? The log appears because express default error handler prints stack in development. Adding our own handler silences it. Let's add it.

Then check full test output for the first lines.

File edits

1 file

Completed
Modifiedsrc/server.js+7−1View changes

/sandbox/repo/src/server.js

Replacement excerpt

const { auth, requiresAuth } = oidc;
-const { auth: jwtAuth } = jwtBearer;
+const { auth: jwtAuth, InvalidTokenError } = jwtBearer;

Replacement excerpt

app.get('/healthz', (_req, res) => res.json({ ok: true }));
+
+// Turn bad/expired tokens into a clean 401 instead of a stack trace in the logs.
+app.use((err, _req, res, next) => {
+ if (err instanceof InvalidTokenError) return res.status(401).json({ error: err.message });
+ next(err);
+});
Thinking…

I should continue working through the task at hand.

cd /sandbox/repo && npm test 2>&1 | tail -8
at Route.dispatch (/sandbox/repo/node_modules/express/lib/router/route.js:119:3)
    at Route.dispatch (/sandbox/repo/node_modules/express/lib/router/route.js:119:3)
    at Layer.handle [as handle_request] (/sandbox/repo/node_modules/express/lib/router/layer.js:95:5)
    at /sandbox/repo/node_modules/express/lib/router/index.js:284:15
    at Function.process_params (/sandbox/repo/node_modules/express/lib/router/index.js:346:12)
    at next (/sandbox/repo/node_modules/express/lib/router/index.js:280:10)
PASS  /api/me -> 401 (expected 401)

all good
Thinking…

Still logs — maybe the error handler is placed after routes but express-openid-connect's requiresAuth? No, the error comes from jwt middleware on /api/me. My app.use error handler is registered after routes, should catch. Unless the middleware itself logs via console.error before calling next(err)? Let's check dist/index.js around 1252 for a console call... Actually likely the InvalidTokenError thrown has status and express-oauth2-jwt-bearer's error handling... Let me check whether it's UnauthorizedError subclass. Let's inspect quickly.

cd /sandbox/repo && grep -n "class .*Error" node_modules/express-oauth2-jwt-bearer/dist/index.js | head -20; grep -n "console\." node_modules/express-oauth2-jwt-bearer/dist/index.js | head
12:class UnauthorizedError extends Error {
12:class UnauthorizedError extends Error {
21:class InvalidRequestError extends UnauthorizedError {
35:class InvalidTokenError extends UnauthorizedError {
44:class InsufficientScopeError extends UnauthorizedError {
53:class InvalidProofError extends UnauthorizedError {
Thinking…

No console logging in the package, so the log comes from Express's default error handler — meaning my handler isn't catching. Why? Because InvalidTokenError imported from default export destructuring — maybe jwtBearer.InvalidTokenError is undefined, so err instanceof undefined throws TypeError → falls to next(err)? instanceof undefined throws "Right-hand side of 'instanceof' is not an object", which would propagate... Actually inside error handler, throwing calls next(err2) → default handler prints original? It would print the new error. Hmm but log shows InvalidTokenError stack.

Check whether the dist index is CJS/ESM and what it exports.

cd /sandbox/repo && node -e "import('express-oauth2-jwt-bearer').then(m=>console.log(Object.keys(m), typeof m.default, m.default&&Object.keys(m.default)))"; node -p "require('./node_modules/express-oauth2-jwt-bearer/package.json').exports" 2>/dev/null; cat node_modules/express-oauth2-jwt-bearer/package.json | head -30
[
[
  'InsufficientScopeError',
  'InvalidRequestError',
  'InvalidTokenError',
  'UnauthorizedError',
  'auth',
  'claimCheck',
  'claimEquals',
  'claimIncludes',
  'default',
  'requiredScopes',
  'scopeIncludesAny'
] object [
  'InsufficientScopeError',
  'InvalidRequestError',
  'InvalidTokenError',
  'UnauthorizedError',
  'auth',
  'claimCheck',
  'claimEquals',
  'claimIncludes',
  'requiredScopes',
  'scopeIncludesAny'
]
undefined
{
  "name": "express-oauth2-jwt-bearer",
  "description": "Authentication middleware for Express.js that validates JWT bearer access tokens.",
  "version": "1.11.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "repository": "auth0/node-oauth2-jwt-bearer",
  "scripts": {
    "test": "jest test --coverage",
    "lint": "eslint --fix --ext .ts ./src",
    "prebuild": "rimraf dist",
    "build": "rollup -c"
  },
  "files": [
    "dist"
  ],
  "author": "Auth0 <support@auth0.com>",
  "homepage": "https://github.com/auth0/node-oauth2-jwt-bearer/tree/main/packages/express-oauth2-jwt-bearer",
  "license": "MIT",
  "devDependencies": {
    "@rollup/plugin-node-resolve": "^15.0.0",
    "@rollup/plugin-typescript": "^11.1.6",
    "@tsconfig/node12": "^1.0.11",
    "@types/express": "^4.17.17",
    "@types/jest": "^27.5.2",
    "@types/node": "^14.18.42",
    "@typescript-eslint/eslint-plugin": "^5.57.0",
    "@typescript-eslint/parser": "^5.57.0",
    "eslint": "^8.37.0",
    "express": "^4.22.0",
Thinking…

Exports exist on default. So destructuring works. Then why isn't my handler catching? Perhaps because the error is thrown asynchronously and the middleware calls next(err) — should reach my handler. Unless my error handler was added before the /api/me route? Let me look at file order.

Actually wait: my app.use((err,...)) is registered after routes — fine. But maybe the printed stack comes from the second /api/me check... and the handler returns 401 — status was 401 either way. Hmm, maybe the handler catches but Express still logs? No.

Let me check the file content and test manually.

cd /sandbox/repo && grep -n "app.use\|app.get\|InvalidTokenError" src/server.js
18:const { auth: jwtAuth, InvalidTokenError } = jwtBearer;
18:const { auth: jwtAuth, InvalidTokenError } = jwtBearer;
42:app.use(
64:app.get('/', (req, res) => {
80:app.get('/profile', requiresAuth(), (req, res) => {
85:app.get('/token', requiresAuth(), async (req, res) => {
104:app.get('/api/me', checkJwt, (req, res) => {
114:app.get('/api/me-via-session', requiresAuth(), async (req, res) => {
122:app.get('/healthz', (_req, res) => res.json({ ok: true }));
125:app.use((err, _req, res, next) => {
126:  if (err instanceof InvalidTokenError) return res.status(401).json({ error: err.message });
Thinking…

Order looks right. Why is it still logged? Maybe the middleware throws synchronously inside a promise and express-openid-connect's auth() middleware also registers an error handler? No...

Maybe the error is thrown but Express's finalhandler logs because my handler called next(err) — because err is not instanceof InvalidTokenError? The thrown class comes from the same module instance so instanceof should hold. Unless... the error thrown is created inside dist/index.js but my import resolves to a different module instance (ESM vs CJS dual loading: express-openid-connect? no). Hmm — the package main is dist/index.js (CJS, .js with no type:module?). Check package.json type field. If it's CJS, both my import and the middleware use the same cached instance. Fine.

Wait — maybe the log I saw was from before my edit? I re-ran npm test after edit and still saw the stack. Let me debug directly.

cd /sandbox/repo && node -e "
process.env.AUTH0_DOMAIN='example.us.auth0.com';process.env.AUTH0_CLIENT_ID='x';process.env.AUTH0_CLIENT_SECRET='y';process.env.SESSION_SECRET='s';
const {app}=await import('./src/server.js');
const s=app.listen(0); await new Promise(r=>s.once('listening',r));
const res=await fetch('http://127.0.0.1:'+s.address().port+'/api/me',{headers:{authorization:'Bearer bad'}});
console.log('status',res.status, await res.text()); s.close();
" --input-type=module 2>&1 | tail -5
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
    at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:681:26)
    at async file:///sandbox/repo/[eval1]:3:13

Node.js v22.23.4
Thinking…

The eval failed (top-level await with --input-type=module and import?). Let me write a temp debug file instead.

cd /sandbox/repo && cat > /tmp/dbg.mjs <<'EOF'
process.env.AUTH0_DOMAIN='example.us.auth0.com';process.env.AUTH0_CLIENT_ID='x';process.env.AUTH0_CLIENT_SECRET='y';process.env.SESSION_SECRET='s';
const {app}=await import('/sandbox/repo/src/server.js');
const s=app.listen(0); await new Promise(r=>s.once('listening',r));
const res=await fetch('http://127.0.0.1:'+s.address().port+'/api/me',{headers:{authorization:'Bearer bad'}});
console.log('status',res.status, await res.text()); s.close();
EOF
node /tmp/dbg.mjs 2>&1 | tail -6
at file:///sandbox/repo/src/server.js:43:3
    at file:///sandbox/repo/src/server.js:43:3
    at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:681:26)
    at async file:///tmp/dbg.mjs:2:13

Node.js v22.23.4
Thinking…

I need to see the actual error message at line 43 where the auth() call is failing.

cd /sandbox/repo && node /tmp/dbg.mjs 2>&1 | head -20
/sandbox/repo/node_modules/express-openid-connect/lib/config.js:416
/sandbox/repo/node_modules/express-openid-connect/lib/config.js:416
    throw new TypeError(error.details[0].message);
          ^

TypeError: "secret" does not match any of the allowed types
    at module.exports.get (/sandbox/repo/node_modules/express-openid-connect/lib/config.js:416:11)
    at auth (/sandbox/repo/node_modules/express-openid-connect/middleware/auth.js:24:18)
    at file:///sandbox/repo/src/server.js:43:3
    at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:681:26)
    at async file:///tmp/dbg.mjs:2:13

Node.js v22.23.4
Thinking…

Important finding: express-openid-connect requires the session secret to be at least 8 characters? Actually the rule: secret must be a string of length >= 8 (or array). 's' too short. In smoke test the secret was 'smoke-test-secret' (fine). So this error is only my debug script. Fine — but note in README that SESSION_SECRET must be ≥ 8 chars. Let me use longer secret in debug and re-check error handler.

cd /sandbox/repo && sed -i "s/SESSION_SECRET='s'/SESSION_SECRET='debug-secret-value'/" /tmp/dbg.mjs && node /tmp/dbg.mjs 2>&1 | tail -6
status 401 {"error":"Invalid Token or Protected Header formatting"}
status 401 {"error":"Invalid Token or Protected Header formatting"}
Thinking…

Error handler works now (earlier run must have been cached? Whatever — it works now). Re-run npm test to confirm clean output.

Then write README, add .gitignore additions (.env, node_modules — check existing .gitignore).

cd /sandbox/repo && npm test 2>&1 | tail -8; echo "=== gitignore ==="; cat .gitignore
at Route.dispatch (/sandbox/repo/node_modules/express/lib/router/route.js:119:3)
    at Route.dispatch (/sandbox/repo/node_modules/express/lib/router/route.js:119:3)
    at Layer.handle [as handle_request] (/sandbox/repo/node_modules/express/lib/router/layer.js:95:5)
    at /sandbox/repo/node_modules/express/lib/router/index.js:284:15
    at Function.process_params (/sandbox/repo/node_modules/express/lib/router/index.js:346:12)
    at next (/sandbox/repo/node_modules/express/lib/router/index.js:280:10)
PASS  /api/me -> 401 (expected 401)

all good
=== gitignore ===
# Keep generated and vendored files out of source diffs.

# Logs / OS cruft
*.log
.DS_Store

# JavaScript / TypeScript
node_modules/
.next/
dist/
build/
coverage/

# Python
.venv/
venv/
__pycache__/
*.py[cod]
.pytest_cache/
*.egg-info/

# Rust
target/

# Go / PHP / Ruby vendored deps
vendor/
.bundle/

# Java / Kotlin / Scala
.gradle/

# .NET
bin/
obj/

# Elixir
_build/
deps/

# Dart / Flutter
.dart_tool/
Thinking…

In npm test the stack still prints for the first /api/me (no auth header at all → maybe that path raises InvalidRequestError? no, missing header → 401 UnauthorizedError "authorization header not provided"?). The printed stack is for one of the two calls. Let's see full test output head.

Likely the no-header case raises UnauthorizedError (base class), not InvalidTokenError, so instanceof InvalidTokenError is false → next(err) → default handler logs. Fix: check for base UnauthorizedError instead (covers all subclasses).

cd /sandbox/repo && sed -i 's/const { auth: jwtAuth, InvalidTokenError } = jwtBearer;/const { auth: jwtAuth, UnauthorizedError } = jwtBearer;/; s/if (err instanceof InvalidTokenError) return res.status(401).json({ error: err.message });/if (err instanceof UnauthorizedError) return res.status(err.status ?? 401).json({ error: err.message });/' src/server.js && npm test 2>&1 | tail -8
> node tests/smoke.mjs
> node tests/smoke.mjs

PASS  /healthz -> 200 (expected 200)
PASS  / -> 200 (expected 200)
PASS  /api/me -> 401 (expected 401)
PASS  /api/me -> 401 (expected 401)

all good
Thinking…

Clean. Now README with:

  • what this is
  • quickstart (create tenant — human/console step; provision script; npm start)
  • architecture / files
  • how to get an access token / call API with curl
  • pricing summary (fetched from auth0.com/pricing today, B2C monthly numbers)
  • agent-manageability assessment + what blocks me
  • limitations

Also add .env to .gitignore.

Pricing facts (from fetched page, B2C tab, monthly billing, 500 MAU slider):

  • Free: $0/mo, up to 25,000 MAU, 1 custom domain (credit card verification), passwordless, unlimited social connections, 5 organizations, 1 enterprise connection, community support.
  • Essentials: from $35/mo (up to 500 MAU at that tier), pro MFA, RBAC per org, 10 orgs, log streaming, separate prod/dev environments, standard support.
  • Professional: from $240/mo, custom DB connections, enterprise MFA, custom token exchange, enhanced attack protection, M2M add-on.
  • Enterprise: contact sales, 99.99% SLA, custom tiers, private deployment add-on.
  • Billed on External/Monthly Active Users (MAU) with tiered volume; paid plans pick custom user counts 1,000–100,000+; M2M auth limited (1,000 on Free / 5,000 Essentials per comparison table).
  • Trial: 22 days then Free plan auto-activates.
  • Startup program free for a year (100k MAU); nonprofit discounts.
  • Add-ons: FGA, Attack Protection, Private Cloud, Adaptive MFA, M2M tokens, HIPAA/PCI, AI identity.

Let me double check M2M numbers from comparison table: "Machine-to-Machine Authentication 1,000 ... 5,000" for Free/Essentials. And "External Active Users Up to 25,000 / Custom Tiers Available". Good.

Note the prices scale with the MAU slider — the numbers shown ($35/$240) correspond to the 500-MAU selection; higher MAU costs more. I should say that clearly and note I can't read exact prices for other tiers without the JS widget.

Write README.

File edits

1 file

Completed
WrittenREADME.mdView changes

/sandbox/repo/README.md

Written content · previous contents were not recorded

# Simple Auth0 example
A minimal, dependency-light example of using **Auth0 as a hosted identity service**:
users log in on Auth0's hosted login page (this app never sees a password), and the app
also acts as a JWT-protected API. Everything auth-related happens on Auth0's cloud — there
is no local identity server, database, or container stack.
```
Browser ──/login──▶ Auth0 hosted login page (your tenant) ──code──▶ this Express app
│ session cookie
Browser ──Bearer <Auth0 access token>──▶ /api/* (JWT verified against tenant JWKS)
```
## Files
| Path | What it shows |
| --- | --- |
| `src/server.js` | Web app login/logout/profile via `express-openid-connect` **plus** `/api/*` resource server via `express-oauth2-jwt-bearer` |
| `scripts/provision.mjs` | Creates the Application + API in your tenant through the **Management API** (idempotent, `--dry-run` supported) — no dashboard clicking |
| `scripts/tenant-check.mjs` | Read-only tenant snapshot: apps, APIs, connections, user count, recent logs |
| `tests/smoke.mjs` | Boots the app on an ephemeral port and asserts 200/401 behaviour. No Auth0 network calls. |
## Quickstart
1. **Create a tenant** (one-time, ~2 min, in a browser): sign up at <https://auth0.com/signup>
and pick a region, e.g. `your-tenant.us.auth0.com`. *This step needs a human — see
"Blocked on" below.*
2. `cp .env.example .env` and set `AUTH0_DOMAIN`, plus `SESSION_SECRET` (≥ 8 chars, random).
3. Create an M2M client for management (dashboard → Applications → *Machine to Machine* →
authorize it on the **Auth0 Management API** → grant `read:clients create:clients
read:resource_servers create:resource_servers`), then put its id/secret in `.env` as
`AUTH0_MANAGEMENT_CLIENT_ID` / `_SECRET`.
4. `npm install`
5. `npm run provision` → creates the web app + API and prints the `.env` values to paste
(`AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET`, `AUTH0_AUDIENCE`).
6. `npm start` → open <http://localhost:3000>, click **Log in** (hosted page), then
**Profile** / **Show access token**.
Call the protected API with the token from `/token`:
```bash
curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/me
# {"message":"Hello from the protected API","subject":"auth0|...","scopes":[...],"permissions":[]}
```
Without a valid token you get `401 {"error":"..."}` — asserted by `npm test`.
Day-2 management: `npm run tenant` (list apps/APIs/connections/users/logs).
## How Auth0 pricing works
Pulled from <https://auth0.com/pricing> (B2C tab, monthly billing) at build time:
- **Billed on External Monthly Active Users (MAU)** — an "active user" is a distinct user who
authenticates (or whose token is refreshed) in the month, not total registered users.
M2M clients and dashboard/tenant admins don't count as external users.
- **Free — $0/mo**, up to **25,000 MAU**: hosted login, passwordless, unlimited social
connections, 1 custom domain (card verification required), 5 Organizations, 1 Enterprise
Connection, basic attack protection, community support. New tenants get a **22-day trial**
of paid features, then drop to Free automatically.
- **Essentials — from $35/mo**: higher auth/API limits, Pro MFA, RBAC per Organization,
10 Organizations, log streaming (Datadog/Splunk/AWS/Azure), separate prod & dev
environments, standard support.
- **Professional — from $240/mo**: custom database connections (use your own user store),
Enterprise MFA, custom token exchange, enhanced attack protection, M2M tokens as an add-on.
- **Enterprise — contact sales**: custom user/SSO tiers, 99.99% SLA, higher rate limits,
private deployment add-on.
- The `$35` / `$240` figures above are the entry prices at the **500 MAU** slider position;
price rises with the MAU tier you select (paid plans let you choose a custom user count
from 1,000 to 100,000+). Free also caps non-user things, e.g. 1,000 M2M authentications
(5,000 on Essentials).
- **Add-ons priced separately**: Fine-Grained Authorization, Attack Protection, Adaptive MFA,
Private Cloud (dedicated deployment), M2M tokens, Credential Guard/Bot Detection,
HIPAA BAA & PCI, AI identity/Token Vault.
- Discounts: **startups** get 100k MAU + pro features free for a year; nonprofits get a
Social Impact discount.
Practical read for this example: a hobby/dev deployment stays on **Free** (25k MAU is a lot
of logins); the first thing that pushes you to Essentials (~$35+/mo) is needing log
streaming, separate prod/dev environments, or Pro MFA — not user count.
## Is this easy for an agent like me to manage?
**Mostly yes — the runtime is fully code-driven; only tenant bootstrapping needs a human.**
Easy for me (no human, no console):
- All login/session/JWT behaviour lives in `src/server.js` and is testable offline
(`npm test` passes here with placeholder credentials).
- Applications, APIs, connections, users, roles, Actions and logs are all CRUD-able through
the **Management API** (`scripts/*.mjs`), so provisioning and day-2 checks are scriptable
and idempotent. `--dry-run` lets me show a plan before touching a live tenant.
- Debugging is plain HTTP: 401s from `express-oauth2-jwt-bearer` carry precise messages
(bad signature vs wrong `aud` vs expired).
Hard / blocked for me:
- **Tenant creation and credentials.** Sign-up, region choice, and M2M secret retrieval are
dashboard/identity-verified steps; the Management API can't create a tenant and never
re-exposes an existing `client_secret` (only rotate). You must hand me
`AUTH0_DOMAIN` + management id/secret (or run `npm run provision` yourself).
- **Interactive login testing.** Verifying a real login needs a browser session against your
hosted page with a real user; I can't complete that flow headlessly. I can only assert the
unauthenticated paths (401/redirects).
- **Billing/plan changes** are dashboard- or sales-driven; the Management API does not expose
plan, MAU quota, or invoices, so I can't verify what you'll be charged — only the published
pricing above.
- Anything requiring custom domains, Enterprise Connections (SAML/Okta), or Private Cloud
involves DNS, certificates and often sales/contracts.
## Blocked on (what I need from you)
1. A tenant domain (`*.auth0.com`) — from <https://auth0.com/signup>.
2. Management API M2M credentials in `.env` (step 3 above), if you want me to run
`npm run provision` / `npm run tenant` against it.
3. Confirmation of the `AUTH0_AUDIENCE` you want (default here: `https://api.example.com`).
Everything else in this repo already works offline: `npm install && npm test` and
`npm run provision -- --dry-run` both pass without any Auth0 account.
Thinking…

Continue: add .env to .gitignore, run final verification (npm test, provision dry-run), review server.js once, and give the summary answer including pricing + manageability assessment.

cd /sandbox/repo && printf '\n# Local secrets / Auth0 credentials\n.env\n.env.*\n!.env.example\n' >> .gitignore && git status --short && echo "=== verify ===" && npm test 2>&1 | tail -7 && npm run provision -- --dry-run 2>&1 | tail -6
M .gitignore
 M .gitignore
?? .env.example
?? README.md
?? package-lock.json
?? package.json
?? scripts/
?? src/
?? tests/
=== verify ===

PASS  /healthz -> 200 (expected 200)
PASS  / -> 200 (expected 200)
PASS  /api/me -> 401 (expected 401)
PASS  /api/me -> 401 (expected 401)

all good

DRY RUN - no Auth0 calls made. Would ensure:
  API     name=auth0-simple-example-api identifier=https://api.example.com signing=RS256
  Client  name=auth0-simple-example app_type=regular_web callbacks=http://localhost:3000/callback logout=http://localhost:3000

Then write into .env: AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_AUDIENCE=https://api.example.com
read /sandbox/repo/src/server.js{"content":[{"text":"\nconst issuerBaseURL = `https://${AUTH0_DOMAIN}`;\n\nconst app = express();\n\n// --- 1. Web app: OpenID Connect against the Auth0 tenant …ctrl+o
{"content":[{"text":"\nconst issuerBaseURL = `https://${AUTH0_DOMAIN}`;\n\nconst app = express();\n\n// --- 1. Web app: OpenID Connect against the Auth0 tenant -------------------\napp.use(\n  auth({\n    authRequired: false, // \"/\" stays public; protect routes with requiresAuth()\n    auth0Logout: true, // logging out here also ends the Auth0 session\n    baseURL: BASE_URL,\n    clientID: AUTH0_CLIENT_ID,\n    clientSecret: AUTH0_CLIENT_SECRET, // confidential client (Regular Web Application)\n    issuerBaseURL,\n    secret: SESSION_SECRET,\n    authorizationParams: {\n      response_type: 'code',\n      audience: AUTH0_AUDIENCE, // ask for an access token for our API\n      scope: 'openid profile email offline_access',\n    },\n  })\n);\n\nconst page = (title, body) =>\n  `<!doctype html><html><head><meta charset=\"utf-8\"><title>${title}</title>\n   <style>body{font:16px system-ui;margin:3rem auto;max-width:44rem}pre{background:#f5f5f5;padding:1rem;overflow:auto}\n   a{margin-right:1rem}</style></head><body>${body}</body></html>`;\n\napp.get('/', (req, res) => {\n  const user = req.oidc.user;\n  res.send(\n    page(\n      'Auth0 example',\n      user\n        ? `<p>Signed in as <strong>${user.nickname ?? user.name}</strong>.</p>\n           <a href=\"/profile\">Profile</a><a href=\"/api/me\">Call protected API (session)</a>\n           <a href=\"/token\">Show access token</a><a href=\"/logout\">Log out</a>`\n        : `<p>Not signed in.</p><a href=\"/login\">Log in</a>\n           <p><em>/api/me</em> is a separate resource server: it needs a Bearer access token,\n           not this cookie session.</p>`\n\n[64 more lines in file. Use offset=76 to continue.]","type":"text"}]}
Thinking…

Everything verified. One nit: the /api/me link on the signed-in home page will 401 in a browser (cookie ≠ bearer). Better to link to /api/me-via-session. Fix that. Then commit? Repo is a git repo; committing is reasonable. Let me fix link and commit.

cd /sandbox/repo && sed -i 's|<a href="/api/me">Call protected API (session)</a>|<a href="/api/me-via-session">Call protected API</a>|' src/server.js && grep -n "api/me-via-session\">" src/server.js && npm test 2>&1 | tail -3
71: <a href="/profile">Profile</a><a href="/api/me-via-session">Call protected API</a>
71:           <a href="/profile">Profile</a><a href="/api/me-via-session">Call protected API</a>
PASS  /api/me -> 401 (expected 401)

all good
Thinking…

Now commit and give the final summary answering the three questions: how pricing works, whether it's easy for me to manage, and blocked items.

cd /sandbox/repo && git add -A && git -c user.email=dev@example.com -c user.name=dev commit -q -m "Add minimal Auth0 example: hosted login + JWT-protected API + Management API scripts" && git log --oneline -1 && ls -R --ignore=node_modules . | head -20
c153453 Add minimal Auth0 example: hosted login + JWT-protected API + Management API scripts
c153453 Add minimal Auth0 example: hosted login + JWT-protected API + Management API scripts
.:
README.md
package-lock.json
package.json
scripts
src
tests

./scripts:
provision.mjs
tenant-check.mjs

./src:
server.js

./tests:
smoke.mjs
Thinking…

Everything's done and committed. Give the final answer: what was built, pricing, manageability assessment, blocked items. Keep concise but complete.

Verify claims I make: files exist, npm test passes, dry-run passes, pricing fetched from auth0.com/pricing today.

Final summary.

Built and committed (c153453) in /sandbox/repo — no local service stack, no long-running commands. Everything verified offline: npm install && npm test passes, npm run provision -- --dry-run passes.

What I built

FileRole
src/server.jsOne Express process, two roles: web app using Auth0's hosted login page via the official express-openid-connect SDK (code flow, session cookie, /login /logout /profile /token), and resource server at /api/* verifying Auth0 JWTs with express-oauth2-jwt-bearer. Clean 401s instead of stack traces.
scripts/provision.mjsCreates the Application + API in your tenant through the Management API (idempotent, --dry-run), prints .env values — no dashboard clicking.
scripts/tenant-check.mjsRead-only day-2 snapshot: apps, APIs, connections, user total, recent logs.
tests/smoke.mjsBoots on an ephemeral port, asserts /healthz 200, / 200, /api/me 401 (no token and bad token). Exits immediately.
.env.example, README.md, .gitignoreConfig template (.env ignored), quickstart + pricing + manageability notes.

Verified: PASS /healthz 200, PASS / 200, PASS /api/me 401 ×2, all good.

How Auth0 pricing works

Read from auth0.com/pricing (B2C, monthly) rather than recalled:

  • Billed on External Monthly Active Users (MAU) — distinct users authenticating per month, not registered users. M2M clients and dashboard admins don't count.
  • Free $0/mo, up to 25,000 MAU: hosted login, passwordless, unlimited social connections, 1 custom domain (card verification), 5 Organizations, 1 Enterprise Connection. New tenants get a 22-day trial, then fall to Free automatically.
  • Essentials from $35/mo: higher limits, Pro MFA, RBAC per org, 10 orgs, log streaming, separate prod/dev environments.
  • Professional from $240/mo: custom DB connections, Enterprise MFA, custom token exchange, enhanced attack protection.
  • Enterprise: contact sales — 99.99% SLA, custom tiers, private deployment.
  • Those prices are the 500-MAU slider position; cost rises with the MAU tier (paid plans pick 1,000–100,000+). Free also caps e.g. 1,000 M2M auths (5,000 on Essentials). Add-ons billed separately: FGA, Attack Protection, Adaptive MFA, Private Cloud, Credential Guard, HIPAA/PCI, AI identity. Startups get 100k MAU free for a year; nonprofits get discounts.
  • For this example: dev/hobby use stays on Free; log streaming, prod/dev split, or Pro MFA pushes you to Essentials — not user count.

Will this be easy for me to manage?

Mostly yes. Runtime behaviour is all code and testable offline; apps, APIs, connections, users, roles and logs are CRUD-able via the Management API, so provisioning and audits are scriptable and idempotent, with --dry-run for safe plans. Auth errors are precise (bad signature vs wrong aud vs expired), which makes debugging straightforward.

Where I'm blocked (needs you)

  1. Tenant + credentials. Sign-up, region choice and M2M secret retrieval are dashboard/identity-verified; the Management API can't create a tenant and never re-exposes an existing client_secret (rotate only). I need AUTH0_DOMAIN + management id/secret in .env.
  2. Real login testing. Completing a hosted-page login needs a browser and a real user; I can only assert unauthenticated paths (done).
  3. Billing. Plan, MAU quota and invoices aren't exposed by the API, so I can't verify what you'd actually be charged — only published pricing.
  4. Custom domains, SAML/Okta Enterprise Connections, Private Cloud → DNS, certs, contracts.

Hand me the domain + management credentials and I'll run npm run provision and npm run tenant against the live tenant.

AX Fundamentals

23 checks. Open an area for details.

This check is a quick preview. Test more tasks and models with Gauge Agents.

Test your product with Gauge Agents

AX Check by Gauge