Skill: auth0/express-oauth2-jwt-bearer (version 1.0.1)

--- name: express-oauth2-jwt-bearer description: Use when adding Auth0 token validation to Express or Node.js APIs - integrates express-oauth2-jwt-bearer SDK to protect Node.js API endpoints with JWT Bearer authentication, scope-based RBAC, claim validation, and optional DPoP support license: Apache-2.0 metadata: author: Auth0 <[REDACTED_SECRET]> version: '1.0.1' openclaw: emoji: "\U0001F510" homepage: https://github.com/auth0/agent-skills --- # Node OAuth2 JWT Bearer Integration The `express-oauth2-jwt-bearer` package provides Express middleware for validating Auth0 JWT Bearer access tokens. It handles token extraction, signature verification, audience and issuer validation, and expiry checks per RFC 6750 — letting you focus on business logic rather than JWT parsing. > **Agent instruction:** Before providing SDK setup instructions, fetch the latest release version by running: > ```bash > gh api repos/auth0/node-oauth2-jwt-bearer/releases/latest --jq '.tag_name' > ``` > Use the returned version in all `package.json` dependency lines instead of any hardcoded version below. ## Prerequisites - **Node.js** 18+ (20+ recommended; required for bootstrap script automation) - **Express** 4.x or 5.x - **npm** or **yarn** - An **Auth0 account** with a configured API (Resource Server) - **Auth0 CLI** (for automatic setup): `npm install -g @auth0/auth0-cli` ## When NOT to Use | Use Case | Recommended Skill | |----------|------------------| | Building a server-side web app with login UI (Express sessions) | `auth0-express` | | Building a Next.js app with server-side auth | `auth0-nextjs` | | Building a React/Angular/Vue SPA | `auth0-react`, `auth0-angular`, `auth0-vue` | | Building a React Native or mobile app | `auth0-react-native`, `auth0-android`, `auth0-swift` | | ASP.NET Core Web API | `auth0-aspnetcore-api` | | Go API with JWT middleware | `go-jwt-middleware` | | Python API (Flask/FastAPI) | `auth0-api-python` | | Node.js API using the older `express-jwt` package | `express-jwt` | ## Quick Start Workflow > **Agent instruction:** Follow these steps to integrate `express-oauth2-jwt-bearer` into the user's Node.js API project. > > 1. **Fetch latest version** (see instruction above). > > 2. **Install the SDK:** > ```bash > npm install express-oauth2-jwt-bearer > ``` > > 3. **Configure Auth0** — follow `references/setup.md`. If the user already provided their Auth0 Domain and API Audience in the prompt, use them directly — skip the bootstrap script and do NOT call `AskUserQuestion` to re-confirm. Otherwise, offer automatic setup via bootstrap script or manual setup. > > 4. **Set up middleware** — add to `app.js` or `server.js`: > ```javascript > import { auth } from 'express-oauth2-jwt-bearer'; > > const checkJwt = auth({ > issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`, > audience: process.env.AUTH0_AUDIENCE, > }); > > app.use(checkJwt); // apply globally, or per-route > ``` > > 5. **Protect endpoints** — apply middleware globally or to specific routes: > ```javascript > // Global protection > app.use(checkJwt); > > // Or per-route > app.get('/api/private', checkJwt, (req, res) => { > res.json({ sub: req.auth.payload.sub }); > }); > ``` > > 6. **Add RBAC** (optional) — use `requiredScopes()` or `claimIncludes()` for permission-based access: > ```javascript > import { auth, requiredScopes, claimIncludes } from 'express-oauth2-jwt-bearer'; > > app.get('/api/messages', checkJwt, requiredScopes('read:messages'), (req, res) => { > res.json({ messages: [] }); > }); > ``` > > **Important:** `requiredScopes` accepts a single argument — a space-separated string or an array. Do NOT pass multiple string arguments: `requiredScopes('read:msg', 'write:msg')` silently ignores everything after the first. Use `requiredScopes('read:msg write:msg')` or `requiredScopes(['read:msg', 'write:msg'])` instead. > > 7. **Verify the integration** — build and test: > ```bash > node server.js > curl http://localhost:3000/api/private # should return 401 > curl -H "Authorization: Bearer <token>" http://localhost:3000/api/private # should return 200 > ``` > > 8. **Failcheck:** If the server fails to start or tokens are rejected unexpectedly, check `references/api.md` for common issues. After 5-6 failed iterations, use `AskUserQuestion` to ask the user for more details about their environment. ## Detailed Documentation - **[Setup Guide](./references/setup.md)** — Auth0 API registration, .env configuration, bootstrap script for automated setup, and secret management - **[Integration Patterns](./references/integration.md)** — Protected endpoints, RBAC with scopes and claims, DPoP, CORS setup, error handling, and testing with curl - **[API Reference & Testing](./references/api.md)** — Full configuration options, claims reference, complete code example, testing checklist, and common issues ## Common Mistakes | Mistake | Symptom | Fix | |---------|---------|-----| | Created an **Application** instead of an **API** in Auth0 Dashboard | Token validation fails; wrong audience | Create a new **API** (Resource Server) in Auth0 Dashboard → APIs | | Audience doesn't match API identifier exactly | `401 Unauthorized` — "Audience mismatch" | Copy the exact API Identifier string from Auth0 Dashboard → APIs | | Domain includes `https://` prefix | `Error: Invalid URL` at startup | Use hostname only: `your-tenant.us.auth0.com`, not `https://...` | | Checking `scope` claim instead of `permissions` for RBAC | 403 always returned or permissions ignored | Use `requiredScopes()` for scope-based RBAC; use `claimIncludes('permissions', 'read:data')` for Auth0 RBAC permission claims | | CORS not configured before auth middleware | Preflight OPTIONS requests return 401 | Add `cors()` middleware before `auth()` in the middleware chain | | `.env` file not loaded | `undefined` for domain/audience | Add `import 'dotenv/config'` at the top of the entry file | | `req.auth` is undefined | `TypeError: Cannot read properties of undefined` | Verify `checkJwt` middleware runs before the handler | ## Related Skills - **[auth0-express](../auth0-express)** — For Express web apps with login UI (sessions, cookies) - **[auth0-nextjs](../auth0-nextjs)** — For Next.js server-side web apps - **[auth0-aspnetcore-api](../auth0-aspnetcore-api)** — BACKEND_API reference implementation for .NET - **[go-jwt-middleware](../go-jwt-middleware)** — JWT middleware for Go APIs - **[auth0-api-python](../auth0-api-python)** — JWT validation for Python APIs (Flask/FastAPI) - **[auth0-cli](../auth0-cli)** — Manage Auth0 resources from the terminal ## Quick Reference ### Core Middleware | Function | Description | Returns | |----------|-------------|---------| | `auth(options?)` | JWT Bearer validation middleware | `Handler` — 401 if token invalid/missing | | `requiredScopes(scopes)` | Validates token has all required scopes | `Handler` — 403 if scopes missing | | `scopeIncludesAny(scopes)` | Validates token has at least one scope | `Handler` — 403 if no match | | `claimEquals(claim, value)` | Validates a claim equals a value | `Handler` — 401 if mismatch | | `claimIncludes(claim, ...values)` | Validates claim includes all values | `Handler` — 401 if incomplete | | `claimCheck(fn, desc?)` | Custom claim validation function | `Handler` — 401 if fn returns false | ### Configuration Options | Option | Type | Description | |--------|------|-------------| | `issuerBaseURL` | `string` | Auth0 domain with `https://` (required unless using env vars) | | `audience` | `string` | API Identifier from Auth0 Dashboard (required unless using env vars) | | `tokenSigningAlg` | `string` | Signing algorithm (default: `RS256`; use `HS256` for symmetric) | | `authRequired` | `boolean` | Set `false` to make authentication optional (default: `true`) | | `clockTolerance` | `number` | Clock skew tolerance in seconds (no default; undefined unless set) | | `dpop` | `DPoPOptions` | DPoP configuration (see integration.md) | ### Environment Variables | Variable | Description | |----------|-------------| | `ISSUER_BASE_URL` | Auth0 domain with `https://` (auto-detected by SDK) | | `AUDIENCE` | API Identifier (auto-detected by SDK) | ### Request Object After successful validation, `req.auth` contains: ```typescript req.auth.payload // Decoded JWT payload (sub, iss, aud, exp, permissions, etc.) req.auth.header // JWT header (alg, typ, kid) req.auth.token // Raw JWT string ``` ## SDK Architecture The `node-oauth2-jwt-bearer` monorepo contains three packages: | Package | Purpose | |---------|---------| | `express-oauth2-jwt-bearer` | **Main package.** Express middleware for JWT Bearer validation. Published to npm. | | `access-token-jwt` | Low-level JWT verification utilities (used internally). | | `oauth2-bearer` | RFC 6750 Bearer token extraction (used internally). | In practice, you only install and import `express-oauth2-jwt-bearer`. ## Auth Flow Comparison | Auth Pattern | SDK | When to Use | |-------------|-----|-------------| | JWT Bearer (stateless) | `express-oauth2-jwt-bearer` | APIs called by SPAs, mobile apps, M2M clients | | Session-based (stateful) | `@auth0/express-openid-connect` | Web apps with login UI and server-side sessions | ## Testing Quick Reference ```bash # Get test token from Auth0 Dashboard → APIs → your API → Test tab # Copy the token, then: # 1. Verify 401 on protected route (no token) curl -v http://localhost:3000/api/private # 2. Verify 200 with valid token curl -H "Authorization: Bearer <paste-token-here>" http://localhost:3000/api/private # 3. Verify 403 with valid token but missing scope curl -H "Authorization: Bearer <paste-token-here>" http://localhost:3000/api/admin # 4. Verify CORS preflight curl -v -X OPTIONS http://localhost:3000/api/private \ -H "Origin: http://localhost:5173" \ -H "Access-Control-Request-Method: GET" \ -H "Access-Control-Request-Headers: Authorization" ``` ## References - [express-oauth2-jwt-bearer on npm](https://www.npmjs.com/package/express-oauth2-jwt-bearer) - [GitHub: auth0/node-oauth2-jwt-bearer](https://github.com/auth0/node-oauth2-jwt-bearer) - [Auth0 Node.js API Quickstart](https://auth0.com/docs/quickstart/backend/nodejs/interactive) - [Auth0 APIs Dashboard](https://manage.auth0.com/#/apis) - [RFC 6750 — Bearer Token Usage](https://datatracker.ietf.org/doc/html/rfc6750)

path: references/api.md
content: # express-oauth2-jwt-bearer API Reference & Testing

## Configuration Reference

All options are passed to the `auth()` function or set via environment variables.

### auth() Options

| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| `issuerBaseURL` | `string` | Yes (or `ISSUER_BASE_URL` env var) | — | Auth0 domain with `https://`, e.g. `https://your-tenant.us.auth0.com` |
| `audience` | `string` | Yes (or `AUDIENCE` env var) | — | API Identifier from Auth0 Dashboard, e.g. `https://my-api.com` |
| `secret` | `string` | For HS256 only | — | Shared secret for symmetric JWT signing (HS256). Not required for RS256. |
| `tokenSigningAlg` | `string` | No | `RS256` | JWT signing algorithm. Use `HS256` for symmetric keys. |
| `issuer` | `string` | No (alternative to `issuerBaseURL`) | — | Issuer claim value — use with `jwksUri` for non-standard setups |
| `jwksUri` | `string` | No | Derived from `issuerBaseURL` | Custom JWKS endpoint URL |
| `authRequired` | `boolean` | No | `true` | Set `false` to allow unauthenticated requests through (attach auth info if present) |
| `clockTolerance` | `number` | No | `(none)` | Clock skew tolerance in seconds (undefined unless explicitly set) |
| `validators` | `Validators` | No | — | Custom validator overrides. Set `{ iss: false }` to skip issuer validation. |
| `dpop` | `DPoPOptions` | No | — | DPoP configuration (see below) |

### DPoPOptions

| Option | Type | Description |
|--------|------|-------------|
| `enabled` | `boolean` | Enable DPoP token binding. Default is `true` (hybrid Bearer+DPoP mode). |
| `required` | `boolean` | Set `true` to reject plain Bearer tokens (DPoP-only mode). Default: `false`. |
| `iatOffset` | `number` | Max age of a DPoP proof in seconds. |
| `iatLeeway` | `number` | Leeway for `iat` claim in DPoP proofs. |

### Environment Variables (auto-detected)

When no options are passed to `auth()`, these variables are read automatically:

| Variable | Description |
|----------|-------------|
| `ISSUER_BASE_URL` | Auth0 domain with `https://` prefix: `https://your-tenant.us.auth0.com` |
| `AUDIENCE` | API Identifier: `https://your-api.example.com` |

**Note:** `AUTH0_DOMAIN` / `AUTH0_AUDIENCE` are the conventional `.env` keys used in this skill. Pass them explicitly:
```javascript
auth({
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  audience: process.env.AUTH0_AUDIENCE,
})
```

## Claims Reference

| Claim | Type | Description |
|-------|------|-------------|
| `sub` | `string` | Subject identifier — the user's or M2M app's unique Auth0 ID |
| `iss` | `string` | Issuer — your Auth0 tenant URL (e.g. `https://your-tenant.us.auth0.com/`) |
| `aud` | `string \| string[]` | Audience — must match your API Identifier |
| `exp` | `number` | Expiration timestamp (Unix epoch) |
| `iat` | `number` | Issued-at timestamp (Unix epoch) |
| `scope` | `string` | Space-separated scopes granted to the token |
| `permissions` | `string[]` | Array of RBAC permissions (Auth0-specific, enabled via RBAC settings on the API) |
| `azp` | `string` | Authorized party — client ID of the application that requested the token |
| `org_id` | `string` | Organization ID (Auth0 Organizations feature) |

**Accessing claims in a handler:**
```javascript
app.get('/api/me', checkJwt, (req, res) => {
  const { sub, permissions, scope } = req.auth.payload;
  res.json({ sub, permissions });
});
```

## Code Examples

### Complete minimal example

```javascript
// server.js
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import { auth, requiredScopes, claimIncludes } from 'express-oauth2-jwt-bearer';

const app = express();

// 1. CORS before auth (required for preflight requests)
app.use(cors({
  origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
  allowedHeaders: ['Authorization', 'Content-Type', 'DPoP'],
}));

app.use(express.json());

// 2. JWT validation middleware
const checkJwt = auth({
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  audience: process.env.AUTH0_AUDIENCE,
});

// 3. Public endpoint (no auth required)
app.get('/api/public', (req, res) => {
  res.json({ message: 'This is a public endpoint' });
});

// 4. Private endpoint (JWT required)
app.get('/api/private', checkJwt, (req, res) => {
  res.json({
    message: 'Authenticated',
    sub: req.auth.payload.sub,
  });
});

// 5. Scoped endpoint (specific scope required)
app.get('/api/messages', checkJwt, requiredScopes('read:messages'), (req, res) => {
  res.json({ messages: ['Hello', 'World'] });
});

// 6. Permission-based endpoint (RBAC permissions claim)
app.get('/api/admin', checkJwt, claimIncludes('permissions', 'admin:access'), (req, res) => {
  res.json({ message: 'Admin access granted' });
});

// 7. RFC 6750 error handler
app.use((err, req, res, next) => {
  if (err.status) {
    return res.status(err.status).json({
      error: err.code,
      message: err.message,
    });
  }
  next(err);
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`API listening on port ${PORT}`));
```

### Environment configuration (.env)

```env
AUTH0_DOMAIN=your-tenant.us.auth0.com
AUTH0_AUDIENCE=https://your-api.example.com
PORT=3000
CORS_ORIGIN=http://localhost:5173
```

### TypeScript example

```typescript
import 'dotenv/config';
import express, { Request, Response, NextFunction } from 'express';
import { auth, requiredScopes } from 'express-oauth2-jwt-bearer';

// Note: express-oauth2-jwt-bearer already declares req.auth on the Express
// Request interface in its own .d.ts — no need to redeclare it here.

const app = express();

const checkJwt = auth({
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  audience: process.env.AUTH0_AUDIENCE,
});

app.get('/api/private', checkJwt, (req: Request, res: Response) => {
  const sub = req.auth?.payload.sub;
  res.json({ sub });
});
```

## Testing Checklist

- [ ] **Public endpoint** returns `200` without a token: `curl http://localhost:3000/api/public`
- [ ] **Protected endpoint** returns `401` without a token: `curl http://localhost:3000/api/private`
- [ ] **Protected endpoint** returns `200` with valid M2M token: `curl -H "Authorization: Bearer <token>" http://localhost:3000/api/private`
- [ ] **Scoped endpoint** returns `403` with token missing required scope
- [ ] **Scoped endpoint** returns `200` with token that has the required scope
- [ ] **Expired token** returns `401` with error description
- [ ] **Wrong audience** returns `401`
- [ ] **CORS preflight** (`OPTIONS`) returns `200` from protected routes
- [ ] `req.auth.payload.sub` contains the expected subject
- [ ] `req.auth.payload.permissions` array is populated (if RBAC is enabled on the Auth0 API)

### Getting a test token with M2M credentials

```bash
curl --request POST \
  --url "https://YOUR_AUTH0_DOMAIN/oauth/token" \
  --header "content-type: application/json" \
  --data '{
    "client_id": "YOUR_M2M_CLIENT_ID",
    "client_secret": "YOUR_M2M_CLIENT_SECRET",
    "audience": "YOUR_API_AUDIENCE",
    "grant_type": "client_credentials"
  }'
```

## Common Issues

| Error | Cause | Fix |
|-------|-------|-----|
| `UnauthorizedError: No authorization token was found` | No `Authorization: Bearer ...` header | Add the bearer token to the request header |
| `UnauthorizedError: invalid_token — jwt audience invalid` | Audience mismatch | Verify `AUTH0_AUDIENCE` matches the API Identifier in Auth0 Dashboard exactly |
| `UnauthorizedError: invalid_token — jwt issuer invalid` | Domain mismatch | Verify `AUTH0_DOMAIN` is the Auth0 tenant hostname (no `https://`) |
| `UnauthorizedError: invalid_token — jwt expired` | Token has expired | Request a new token; check system clock drift (`clockTolerance` option) |
| `Error: JWKS request failed` | Network or domain misconfiguration | Verify `AUTH0_DOMAIN` is reachable; check network/proxy settings |
| `InsufficientScopeError: Insufficient scope` | Token lacks required scope | Verify the requesting app has the scope granted; check `requiredScopes()` call |
| `CORS error` on OPTIONS preflight | Auth middleware running before CORS | Move `cors()` middleware before `auth()` in the middleware chain |
| `TypeError: Cannot read properties of undefined (reading 'payload')` | `req.auth` is undefined | Check that `checkJwt` middleware runs before the handler |

## Security Considerations

- **Never log tokens.** Full JWT strings contain sensitive claims. Log only `sub` or `jti` for tracing.
- **CORS before auth.** Always register `cors()` before `auth()`. Auth middleware rejects OPTIONS preflight requests with 401 if CORS isn't set first.
- **Audience validation is critical.** Without a matching `audience`, your API would accept tokens issued for other services.
- **Issuer validation.** The `issuerBaseURL` is used to fetch the JWKS and validate the `iss` claim. Never disable issuer validation in production.
- **RBAC via `permissions` claim.** Auth0 RBAC stores user permissions in the `permissions` JWT claim (not `scope`). Enable "Add Permissions in the Access Token" on your Auth0 API settings.
- **DPoP.** For APIs requiring sender-constrained tokens, enable DPoP with `dpop: { enabled: true, required: true }`. This prevents token theft — stolen tokens cannot be replayed without the original private key.
- **Helmet.** Pair with `helmet` for security headers: `npm install helmet` + `app.use(helmet())`.
- **Production secrets.** Never commit `.env` to source control. Use environment variables in production (Railway, Heroku, Fly.io, etc.).


path: references/integration.md
content: # Integration Patterns

## Authentication Flow

```text
Client → API
  1. Client obtains access token from Auth0 (via /oauth/token)
  2. Client sends request with "Authorization: Bearer <token>" header
  3. express-oauth2-jwt-bearer middleware:
     a. Extracts bearer token from Authorization header
     b. Fetches public key from Auth0 JWKS endpoint (cached)
     c. Verifies token signature, issuer, audience, expiry
     d. Attaches decoded token to req.auth
  4. Route handler accesses req.auth.payload
```

## Protected Endpoints

### Global protection

Apply `checkJwt` middleware globally to protect all routes:

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

const checkJwt = auth({
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  audience: process.env.AUTH0_AUDIENCE,
});

// All routes below this require a valid JWT
app.use(checkJwt);
app.get('/api/users', (req, res) => {
  res.json({ sub: req.auth.payload.sub });
});
```

### Per-route protection

Apply middleware to specific routes only:

```javascript
// Public — no auth
app.get('/api/public', (req, res) => {
  res.json({ message: 'Public endpoint' });
});

// Protected — JWT required
app.get('/api/private', checkJwt, (req, res) => {
  res.json({ sub: req.auth.payload.sub });
});
```

### Optional authentication

Allow unauthenticated requests but attach auth info when present:

```javascript
const optionalAuth = auth({
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  audience: process.env.AUTH0_AUDIENCE,
  authRequired: false,
});

app.get('/api/profile', optionalAuth, (req, res) => {
  if (req.auth) {
    res.json({ sub: req.auth.payload.sub, authenticated: true });
  } else {
    res.json({ authenticated: false });
  }
});
```

## RBAC — Scope-Based Authorization

Use `requiredScopes()` to enforce scopes on access tokens:

```javascript
import { auth, requiredScopes } from 'express-oauth2-jwt-bearer';

// All scopes must be present
app.get('/api/messages', checkJwt, requiredScopes('read:messages'), (req, res) => {
  res.json({ messages: [] });
});

// Multiple scopes required
app.post('/api/messages', checkJwt, requiredScopes('read:messages write:messages'), (req, res) => {
  res.json({ created: true });
});
```

### Permission-based RBAC (Auth0 RBAC feature)

When Auth0 RBAC is enabled on the API, permissions are stored in the `permissions` claim:

```javascript
import { auth, claimIncludes } from 'express-oauth2-jwt-bearer';

// Require 'read:messages' in the permissions claim
app.get('/api/messages', checkJwt, claimIncludes('permissions', 'read:messages'), (req, res) => {
  res.json({ messages: [] });
});

// Require multiple permissions
app.delete('/api/messages/:id', checkJwt, claimIncludes('permissions', 'delete:messages'), (req, res) => {
  res.json({ deleted: true });
});
```

## Claim Validation

### claimEquals — exact value match

```javascript
import { auth, claimEquals } from 'express-oauth2-jwt-bearer';

// Require org_id to equal a specific value
app.get('/api/org-data', checkJwt, claimEquals('org_id', 'org_123'), (req, res) => {
  res.json({ org: 'org_123' });
});
```

### claimIncludes — array contains all values

```javascript
import { auth, claimIncludes } from 'express-oauth2-jwt-bearer';

// Require the roles claim to include 'admin'
app.get('/api/admin', checkJwt, claimIncludes('roles', 'admin'), (req, res) => {
  res.json({ admin: true });
});
```

### claimCheck — custom validation logic

```javascript
import { auth, claimCheck } from 'express-oauth2-jwt-bearer';

// Custom validation function
app.get('/api/premium', checkJwt, claimCheck((payload) => {
  return payload?.subscription === 'premium' && payload?.active === true;
}, 'Premium subscription required'), (req, res) => {
  res.json({ premium: true });
});
```

## CORS Configuration

**Critical:** CORS middleware must come before auth middleware. Auth rejects OPTIONS preflight requests with 401 if CORS isn't configured first.

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

// 1. CORS first (handles OPTIONS preflight)
app.use(cors({
  origin: 'http://localhost:5173',  // Your frontend URL
  allowedHeaders: ['Authorization', 'Content-Type', 'DPoP'],
  exposedHeaders: ['WWW-Authenticate'],
}));

// 2. Auth second
const checkJwt = auth({
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  audience: process.env.AUTH0_AUDIENCE,
});
```

## DPoP Support

DPoP (Demonstration of Proof-of-Possession) binds tokens to the client's key pair, preventing token theft. The SDK supports DPoP natively.

### Hybrid mode (Bearer or DPoP both accepted — default)

```javascript
const checkJwt = auth({
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  audience: process.env.AUTH0_AUDIENCE,
  dpop: {
    enabled: true,
    required: false,  // Accept both Bearer and DPoP tokens
  },
});
```

### DPoP-only mode (rejects plain Bearer tokens)

```javascript
const checkJwt = auth({
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  audience: process.env.AUTH0_AUDIENCE,
  dpop: {
    enabled: true,
    required: true,  // Reject plain Bearer tokens
  },
});
```

### Bearer-only mode (disable DPoP)

```javascript
const checkJwt = auth({
  issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
  audience: process.env.AUTH0_AUDIENCE,
  dpop: { enabled: false },
});
```


## Error Handling

The SDK throws RFC 6750-compliant errors with `.status` and `.headers` properties. Add an error handler after your routes:

```javascript
app.use((err, req, res, next) => {
  if (err.status) {
    // JWT validation error — send WWW-Authenticate header per RFC 6750
    res.set(err.headers);
    return res.status(err.status).json({
      error: err.code,
      error_description: process.env.NODE_ENV === 'production' ? undefined : err.message,
    });
  }
  // Other errors
  console.error(err);
  res.status(500).json({ error: 'internal_error' });
});
```

### Error types

| Error Class | Status | Code | Cause |
|------------|--------|------|-------|
| `UnauthorizedError` | 401 | `invalid_token` | Missing, expired, or malformed token |
| `InvalidRequestError` | 400 | `invalid_request` | Malformed Authorization header |
| `InvalidTokenError` | 401 | `invalid_token` | Token signature/claims validation failed |
| `InsufficientScopeError` | 403 | `insufficient_scope` | Token lacks required scope |

```javascript
import {
  UnauthorizedError,
  InvalidTokenError,
  InsufficientScopeError
} from 'express-oauth2-jwt-bearer';

app.use((err, req, res, next) => {
  if (err instanceof UnauthorizedError || err instanceof InvalidTokenError) {
    return res.status(401).json({ error: 'unauthorized' });
  }
  if (err instanceof InsufficientScopeError) {
    return res.status(403).json({ error: 'forbidden' });
  }
  next(err);
});
```

## Testing Patterns

### Manual testing with curl

```bash
# 1. Get a test token (from Auth0 Dashboard → APIs → Test, or via M2M credentials)
ACCESS_TOKEN=$(curl -s --request POST \
  --url "https://YOUR_AUTH0_DOMAIN/oauth/token" \
  --header "content-type: application/json" \
  --data '{
    "client_id": "YOUR_M2M_CLIENT_ID",
    "client_secret": "YOUR_M2M_CLIENT_SECRET",
    "audience": "YOUR_API_AUDIENCE",
    "grant_type": "client_credentials"
  }' | jq -r '.access_token')

# 2. Test protected endpoint
curl -H "Authorization: Bearer $ACCESS_TOKEN" http://localhost:3000/api/private

# 3. Test scoped endpoint
curl -H "Authorization: Bearer $ACCESS_TOKEN" http://localhost:3000/api/messages
```

### Unit testing with Jest/Vitest

```javascript
import request from 'supertest';
import app from './app.js';

describe('API Authentication', () => {
  it('returns 401 without token', async () => {
    const res = await request(app).get('/api/private');
    expect(res.status).toBe(401);
  });

  it('returns 200 with valid token', async () => {
    // Use a test token from Auth0 or a mocked JWT
    const res = await request(app)
      .get('/api/private')
      .set('Authorization', `Bearer ${process.env.TEST_TOKEN}`);
    expect(res.status).toBe(200);
  });
});
```

### Mocking in unit tests

For unit tests, you can mock the `auth` middleware to bypass JWT validation:

```javascript
// test-utils.js
import { jest } from '@jest/globals';

export function mockAuth(payload = { sub: 'test-user' }) {
  jest.mock('express-oauth2-jwt-bearer', () => ({
    auth: () => (req, res, next) => {
      req.auth = { payload };
      next();
    },
    requiredScopes: (scopes) => (req, res, next) => {
      const tokenScopes = req.auth?.payload?.scope?.split(' ') || [];
      const missing = [scopes].flat().filter(s => !tokenScopes.includes(s));
      if (missing.length) return res.status(403).json({ error: 'insufficient_scope' });
      next();
    },
    claimIncludes: () => (req, res, next) => next(),
  }));
}
```


path: references/setup.md
content: # express-oauth2-jwt-bearer Setup Guide

## Auth0 Configuration

> **Agent instruction:**
>
> **Check if credentials are already provided in the user's prompt:** If the user's prompt already includes Auth0 Domain and API Audience (e.g. `your-tenant.us.auth0.com` and `https://api.example.com`), use them directly — skip to "Write the .env file" below. Do NOT call `AskUserQuestion` to re-confirm provided credentials, and do NOT run the bootstrap script.
>
> If credentials are NOT provided, offer setup choices:
>
> Use `AskUserQuestion` to ask the user:
> "How would you like to configure Auth0 for this project?"
> - Option A: **Automatic setup (recommended)** — runs the bootstrap script to create the Auth0 API automatically
> - Option B: **Manual setup** — provide Auth0 credentials manually
>
> **If Automatic Setup (Option A):**
>
> 1. **Pre-flight checks:**
>    - Verify Node.js 20+: `node --version`
>    - Verify Auth0 CLI installed: `auth0 --version`
>    - Verify logged in: `auth0 tenants list --csv --no-input`
>    - If any check fails, guide user to install/login, or fall back to Option B
>
> 2. **Run bootstrap script:**
>    ```bash
>    cd <skill-dir>/scripts && npm install && node bootstrap.mjs <project-path>
>    ```
>    The script will:
>    - Validate the project structure (detect `package.json` with Node.js API patterns)
>    - Discover existing Auth0 APIs
>    - Show a change plan (CREATE or SKIP)
>    - Create the Auth0 API (Resource Server) with the specified identifier
>    - Write the `.env` configuration file with Domain + Audience
>    - Print a summary
>
> **If Manual Setup (Option B):**
>
> Ask the user for:
> - **Auth0 Domain** (e.g., `your-tenant.us.auth0.com`)
> - **API Audience** — the API Identifier you set when creating the Auth0 API (e.g., `https://your-api.example.com`)
>
> Then write the `.env` file (see below).
>
> **Write the .env file** (both paths):
> ```env
> AUTH0_DOMAIN=your-tenant.us.auth0.com
> AUTH0_AUDIENCE=https://your-api.example.com
> PORT=3000
> ```

### Auth0 API Registration (Resource Server)

The bootstrap script automatically runs `auth0 apis create` to register your API as a Resource Server. This produces the `AUTH0_AUDIENCE` value (the API Identifier) that your middleware uses for token validation.

**Auth0 CLI command (for reference):**
```bash
auth0 apis create \
  --name "My Node API" \
  --identifier "https://my-api.example.com" \
  --json --no-input
```

### Creating the Auth0 API manually (if not using bootstrap script)

1. Go to [Auth0 Dashboard → APIs](https://manage.auth0.com/#/apis)
2. Click **Create API**
3. Set:
   - **Name**: Your API name (e.g., "My Node API")
   - **Identifier**: A URL-like identifier (e.g., `https://my-api.example.com`) — this becomes `AUTH0_AUDIENCE`
   - **Signing Algorithm**: `RS256` (recommended)
4. Click **Create**
5. Note the **API Identifier** — this is your Audience value

### Enable RBAC (optional)

To use `claimIncludes('permissions', 'read:data')` with Auth0 RBAC:

1. Go to Auth0 Dashboard → APIs → your API → Settings
2. Enable **"Enable RBAC"**
3. Enable **"Add Permissions in the Access Token"**
4. Add permissions under the **Permissions** tab
5. Assign permissions to roles, and roles to users via Auth0 Dashboard

## Post-Setup Steps

After running the bootstrap script or manual setup:

1. **Verify domain and audience** are correct in `.env`
2. **Test the API is reachable**: `auth0 apis list --json --no-input | grep your-api`
3. **Confirm CORS is configured** before auth middleware in your server file (see integration.md)
4. **Request a test token** using M2M credentials or the Auth0 Dashboard test feature:
   - Go to Auth0 Dashboard → APIs → your API → Test tab
   - Click **Copy Token** to get a test access token

## SDK Installation

```bash
npm install express-oauth2-jwt-bearer
```

**With additional recommended packages:**
```bash
npm install express-oauth2-jwt-bearer dotenv cors helmet
npm install --save-dev @types/express @types/cors  # TypeScript projects
```

**package.json dependency:**
```json
{
  "dependencies": {
    "express-oauth2-jwt-bearer": "^1.7.4",
    "dotenv": "^16.0.0",
    "cors": "^2.8.5",
    "helmet": "^7.0.0"
  }
}
```

## Secret Management

`express-oauth2-jwt-bearer` requires only **Domain** and **Audience** — no Client Secret. The middleware validates tokens using the Auth0 JWKS (JSON Web Key Set) endpoint, which provides the public signing keys. This means:

- **No client secret needed** for token validation
- The JWKS endpoint is publicly accessible at `https://{AUTH0_DOMAIN}/.well-known/jwks.json`
- The middleware fetches and caches keys automatically

### .env file (development)

```env
# .env — Never commit to source control
AUTH0_DOMAIN=your-tenant.us.auth0.com
AUTH0_AUDIENCE=https://your-api.example.com
PORT=3000
```

### Production environment variables

Set these as environment variables in your hosting platform (not in `.env` files):

| Variable | Example Value |
|----------|--------------|
| `AUTH0_DOMAIN` | `your-tenant.us.auth0.com` |
| `AUTH0_AUDIENCE` | `https://your-api.example.com` |
| `PORT` | `3000` |

**Never commit `.env` to source control.** Add `.env` to `.gitignore`:
```bash
echo ".env" >> .gitignore
```

**Load `.env` in your entry file:**
```javascript
import 'dotenv/config'; // must be at the top
// or: require('dotenv').config();
```

## Verification

After setup, verify everything is working:

1. **Start the server:**
   ```bash
   node server.js
   # or: npm start
   ```

2. **Test public endpoint:**
   ```bash
   curl http://localhost:3000/api/public
   # Expected: 200 OK
   ```

3. **Test protected endpoint without token:**
   ```bash
   curl http://localhost:3000/api/private
   # Expected: 401 Unauthorized
   ```

4. **Get a test token** from Auth0 Dashboard → APIs → your API → Test tab, then:
   ```bash
   curl -H "Authorization: Bearer <your-test-token>" http://localhost:3000/api/private
   # Expected: 200 OK with payload data
   ```


path: scripts/bootstrap.mjs
content: #!/usr/bin/env node
import path from "node:path"

import {
  checkNodeVersion,
  checkAuth0CLI,
  getActiveTenant,
  validateApiProject,
} from "./utils/validation.mjs"
import {
  discoverExistingApis,
  buildChangePlan,
  displayChangePlan,
} from "./utils/discovery.mjs"
import { applyApiChanges } from "./utils/apis.mjs"
import { writeEnvFile } from "./utils/env-writer.mjs"
import { confirmWithUser } from "./utils/helpers.mjs"

async function main() {
  console.log("\n  Auth0 Express API Bootstrap\n")

  const projectPath = path.resolve(process.argv[2] || process.cwd())

  // Pre-flight
  checkNodeVersion()
  await checkAuth0CLI()
  const domain = await getActiveTenant()

  // Validate project
  const config = validateApiProject(projectPath)

  // Discover + plan
  const apis = await discoverExistingApis()
  const plan = buildChangePlan(apis, domain, config)
  displayChangePlan(plan)

  // Confirm
  const confirmed = await confirmWithUser("Apply these changes?")
  if (!confirmed) {
    console.log("\n  Aborted by user.\n")
    process.exit(0)
  }

  // Execute
  console.log("")
  const api = await applyApiChanges(plan.api)

  const envPath = path.join(projectPath, ".env")
  await writeEnvFile(
    {
      AUTH0_DOMAIN: domain,
      AUTH0_AUDIENCE: api.identifier,
    },
    envPath
  )

  // Summary
  console.log("\n  Auth0 Express API Setup Complete\n")
  console.log(`  Domain:    ${domain}`)
  console.log(`  Audience:  ${api.identifier}`)
  console.log("")
  console.log("  Next steps:")
  console.log("    1. Install SDK: npm install express-oauth2-jwt-bearer dotenv cors")
  console.log("    2. Add middleware (see references/integration.md)")
  console.log("    3. Test with: curl http://localhost:3000/api/private")
  console.log("")
}

main().catch((e) => {
  console.error(`\n  Bootstrap failed: ${e.message}\n`)
  process.exit(1)
})


path: scripts/package.json
content: {
  "name": "express-oauth2-jwt-bearer-bootstrap",
  "version": "1.0.0",
  "description": "Bootstrap Auth0 configuration for Node OAuth2 JWT Bearer projects",
  "type": "module",
  "scripts": {
    "auth0:bootstrap": "node bootstrap.mjs"
  },
  "dependencies": {
    "execa": "^9.0.0",
    "ora": "^8.0.0"
  }
}


path: scripts/utils/apis.mjs
content: import { $ } from "execa"
import ora from "ora"
import { ChangeAction, createChangeItem } from "./change-plan.mjs"

export function checkApiChanges(domain, apiConfig) {
  const { framework } = apiConfig
  const identifier = `https://${domain}/api/${framework}`

  return createChangeItem(ChangeAction.CREATE, {
    resource: "API",
    name: `${framework}-api`,
    identifier,
  })
}

export async function applyApiChanges(changePlan) {
  const spinner = ora(`Creating API: ${changePlan.name}`).start()
  try {
    const createArgs = [
      "apis", "create",
      "--name", changePlan.name,
      "--identifier", changePlan.identifier,
      "--signing-alg", "RS256",
      "--json",
      "--no-input",
    ]
    const { stdout } = await $({ timeout: 30000 })`auth0 ${createArgs}`
    const api = JSON.parse(stdout)
    spinner.succeed(`Created API: ${changePlan.name} (${api.identifier})`)
    return api
  } catch (e) {
    spinner.fail("Failed to create API")
    throw e
  }
}


path: scripts/utils/auth0-api.mjs
content: import { $ } from "execa"

/**
 * Make a generic API call using auth0 CLI.
 */
export async function auth0ApiCall(method, endpoint, data = null) {
  const args = ["api", method, endpoint, "--no-input"]

  if (data) {
    args.push("--data", JSON.stringify(data))
  }

  try {
    const { stdout } = await $({ timeout: 30000 })`auth0 ${args}`
    return stdout ? JSON.parse(stdout) : null
  } catch (e) {
    if (e.timedOut) {
      console.warn(`Warning: API call timed out: auth0 api ${method} ${endpoint}`)
    } else {
      console.warn(`Warning: API call failed: ${e.message}`)
    }
    throw e
  }
}


path: scripts/utils/change-plan.mjs
content: export const ChangeAction = {
  CREATE: "create",
  UPDATE: "update",
  SKIP: "skip",
}

export function createChangeItem(action, details = {}) {
  return {
    action,
    ...details,
  }
}


path: scripts/utils/discovery.mjs
content: import ora from "ora"
import { auth0ApiCall } from "./auth0-api.mjs"
import { ChangeAction } from "./change-plan.mjs"
import { checkApiChanges } from "./apis.mjs"

export async function discoverExistingApis() {
  const spinner = ora("Discovering existing APIs").start()
  try {
    const apis = (await auth0ApiCall("get", "resource-servers")) || []
    spinner.succeed("Discovered existing APIs")
    return apis
  } catch (e) {
    const msg = e.message || String(e)
    if (msg.includes("404") || msg.includes("Not Found")) {
      spinner.succeed("No existing APIs found")
      return []
    }
    spinner.fail("Failed to discover APIs")
    throw e
  }
}

export function buildChangePlan(apis, domain, apiConfig) {
  const apiPlan = checkApiChanges(domain, apiConfig)
  return { api: apiPlan }
}

export function displayChangePlan(plan) {
  console.log("\n  Change Plan:\n")

  const items = [{ name: "API", ...plan.api }]

  for (const item of items) {
    const icon =
      item.action === ChangeAction.CREATE ? "+" :
      item.action === ChangeAction.UPDATE ? "~" : "="
    const label =
      item.action === ChangeAction.CREATE ? "CREATE" :
      item.action === ChangeAction.UPDATE ? "UPDATE" : "SKIP  "

    let detail = ""
    if (item.identifier) detail = ` (identifier: ${item.identifier})`

    console.log(`  ${icon} [${label}] ${item.name || item.resource}${detail}`)
  }

  console.log("")
}


path: scripts/utils/env-writer.mjs
content: import fs from "node:fs"
import ora from "ora"

export async function writeEnvFile(config, envFilePath) {
  const spinner = ora("Writing .env").start()

  try {
    let content = ""
    if (fs.existsSync(envFilePath)) {
      content = fs.readFileSync(envFilePath, "utf-8")
    }

    for (const [key, value] of Object.entries(config)) {
      const pattern = new RegExp(`^${key}=.*$`, "m")
      if (pattern.test(content)) {
        content = content.replace(pattern, `${key}=${value}`)
      } else {
        content += (content && !content.endsWith("\n") ? "\n" : "") + `${key}=${value}\n`
      }
    }

    fs.writeFileSync(envFilePath, content)
    spinner.succeed(`Updated ${envFilePath}`)
  } catch (e) {
    spinner.fail("Failed to write .env")
    throw e
  }
}


path: scripts/utils/helpers.mjs
content: import readline from "node:readline/promises"

export async function confirmWithUser(message) {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  })

  const answer = await rl.question(`${message} (y/N): `)
  rl.close()

  return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes"
}

export async function getInputFromUser(message) {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  })

  const answer = await rl.question(`${message} `)
  rl.close()

  return answer.trim()
}



path: scripts/utils/validation.mjs
content: import { $ } from "execa"
import fs from "node:fs"
import path from "node:path"
import ora from "ora"

export function checkNodeVersion() {
  const [major] = process.versions.node.split(".").map(Number)
  if (major < 20) {
    console.error(`Node.js 20 or later is required (current: ${process.version})`)
    process.exit(1)
  }
}

export async function checkAuth0CLI() {
  const spinner = ora("Checking Auth0 CLI").start()
  try {
    const versionArgs = ["--version", "--no-input"]
    const { stdout } = await $({ timeout: 10000 })`auth0 ${versionArgs}`
    spinner.succeed(`Auth0 CLI found: ${stdout.trim()}`)
  } catch {
    spinner.fail("Auth0 CLI is not installed")
    console.error(
      "\nInstall it:\n" +
      "  macOS:  brew install auth0/auth0-cli/auth0\n" +
      "  Linux:  curl -sSfL https://raw.githubusercontent.com/auth0/auth0-cli/main/install.sh | sh\n" +
      "  More:   https://github.com/auth0/auth0-cli\n"
    )
    process.exit(1)
  }
}

export async function getActiveTenant() {
  const spinner = ora("Detecting active tenant").start()
  try {
    const tenantsArgs = ["tenants", "list", "--csv", "--no-input"]
    const { stdout } = await $({ timeout: 10000 })`auth0 ${tenantsArgs}`

    const activeLine = stdout
      .split("\n")
      .slice(1)
      .find((line) => line.includes("\u2192"))

    const domain = activeLine?.split(",")[1]?.trim()
    if (!domain) {
      spinner.fail("No active tenant. Run `auth0 login` then re-run this script.")
      process.exit(1)
    }

    spinner.succeed(`Active tenant: ${domain}`)
    return domain
  } catch {
    spinner.fail("Not logged in. Run `auth0 login` then re-run this script.")
    process.exit(1)
  }
}

export function validateApiProject(projectPath) {
  const spinner = ora("Validating API project").start()

  // Detect framework from project files
  const detectors = [
    { file: "*.csproj", framework: "dotnet", port: 5000 },
    { file: "composer.json", framework: "laravel", port: 8000 },
    { file: "Gemfile", framework: "rails", port: 3000 },
    { file: "go.mod", framework: "go", port: 3000 },
    { file: "requirements.txt", framework: "python", port: 8000 },
    { file: "pyproject.toml", framework: "python", port: 8000 },
    { file: "package.json", framework: "node", port: 3000 },
  ]

  let framework = null
  let port = 3000
  for (const d of detectors) {
    const pattern = d.file.includes("*")
      ? fs.readdirSync(projectPath).some((f) => f.endsWith(d.file.replace("*", "")))
      : fs.existsSync(path.join(projectPath, d.file))
    if (pattern) {
      framework = d.framework
      port = d.port
      break
    }
  }

  if (!framework) {
    spinner.fail(`Could not detect project framework in ${projectPath}`)
    process.exit(1)
  }

  spinner.succeed(`API project: ${framework} (port ${port})`)
  return { framework, port }
}


path: tests/evals.json
content: {
  "skill_name": "express-oauth2-jwt-bearer",
  "evals": [
    {
      "id": 1,
      "prompt": "Add Auth0 authentication to an Express Node.js application using the express-oauth2-jwt-bearer SDK.\n\n**Auth0 Credentials:**\n- Domain: `dev-example.auth0.com`\n- Client ID: `abc123def456ghi789jkl012`\n- API Audience: `https://api.example.com`",
      "expected_output": "Working Express Node.js API with Auth0 JWT validation, protected endpoints, and RBAC permissions",
      "expectations": [
        "Auth0 SDK (express-oauth2-jwt-bearer) present in project",
        "Has correct import statement (express-oauth2-jwt-bearer)",
        "Auth0 SDK initialized (auth() middleware configured)",
        "AUTH0_DOMAIN environment variable configured",
        "Configures JWT Bearer token validation middleware",
        "Configures API audience for token validation",
        "Has protected API endpoint with JWT middleware",
        "Configures CORS middleware",
        "Auth0 domain dev-example.auth0.com written to .env config file",
        "API audience https://api.example.com written to .env config file",
        "No login UI methods or callback URLs in stateless API",
        "No client secret in API config (uses JWKS for validation)",
        "Solution correctly configures Auth0 JWT Bearer token validation, RBAC permissions, and CORS in an Express Node.js API",
        "Uses current SDK version (v1.7.4) in dependency declaration",
        "Configures specific token validation options (clockTolerance, issuerBaseURL, or tokenSigningAlg)",
        "Uses claim validation functions (requiredScopes, claimIncludes, claimCheck) for RBAC",
        "Does not use deprecated express-jwt + jwks-rsa combo (uses current express-oauth2-jwt-bearer)",
        "At least 2 skill-specific advanced patterns present (version, advanced validation options, RBAC, DPoP, CORS ordering)"
      ]
    }
  ]
}


path: tests/graders.json
content: [
  {"type": "matches", "pattern": "express-oauth2-jwt-bearer", "description": "Auth0 SDK (express-oauth2-jwt-bearer) present in project"},
  {"type": "matches", "pattern": "require\\s*\\(['\"]express-oauth2-jwt-bearer['\"]\\)|from\\s+['\"]express-oauth2-jwt-bearer['\"]", "description": "Has correct import statement (express-oauth2-jwt-bearer)"},
  {"type": "contains_any", "values": ["auth({", "auth()"], "description": "Auth0 SDK initialized (auth() middleware configured)"},
  {"type": "contains", "value": "AUTH0_DOMAIN", "description": "AUTH0_DOMAIN environment variable configured"},
  {"type": "matches", "pattern": "auth\\s*\\(\\{|const\\s+\\w+\\s*=\\s*auth\\s*\\(|app\\.use\\s*\\(auth\\s*\\(", "description": "Configures JWT Bearer token validation middleware"},
  {"type": "matches", "pattern": "audience:|AUTH0_AUDIENCE|AUDIENCE", "description": "Configures API audience for token validation"},
  {"type": "matches", "pattern": "app\\.(get|post|put|delete|patch)\\s*\\([^,]+,\\s*checkJwt|app\\.use\\s*\\(checkJwt|router\\.(get|post|put|delete|patch)\\s*\\([^,]+,\\s*checkJwt", "description": "Has protected API endpoint with JWT middleware"},
  {"type": "matches", "pattern": "cors\\s*\\(|require\\s*\\(['\"]cors['\"]\\)|from\\s+['\"]cors['\"]", "description": "Configures CORS middleware"},

  {"type": "file_contains", "file_pattern": "**/.env*", "value": "dev-example.auth0.com", "description": "Auth0 domain written to .env config file"},
  {"type": "file_contains", "file_pattern": "**/.env*", "value": "https://api.example.com", "description": "API audience written to .env config file"},

  {"type": "not_contains_any", "values": ["loginWithRedirect", "Auth0Provider", "handleAuth", "/callback"], "description": "No login UI methods or callback URLs in stateless API"},
  {"type": "not_contains", "value": "client_secret", "description": "No client secret in API config (uses JWKS for validation)"},

  {"type": "judge", "question": "Does the solution correctly configure Auth0 JWT Bearer token validation, RBAC permissions, and CORS in an Express Node.js API?", "examples": "PASS: Validates JWT using JWKS endpoint via express-oauth2-jwt-bearer, checks audience, applies checkJwt middleware to protected routes, configures CORS before auth.\nFAIL: Uses client secret for token validation instead of JWKS.\nFAIL: Has login routes or session cookies — APIs should be stateless JWT only.\nFAIL: Uses @auth0/auth0-react or @auth0/auth0-spa-js — those are SPA SDKs, not API middleware.", "framework": "express", "description": "correctness-judge: Solution correctly integrates Auth0 JWT validation in Express API"},

  {"type": "matches", "pattern": "\\^1\\.7|\"1\\.7\\.|~1\\.7|1\\.7\\.4", "description": "Uses current SDK version (v1.7.4) in dependency declaration"},
  {"type": "matches", "pattern": "clockTolerance|issuerBaseURL|tokenSigningAlg", "description": "Configures specific token validation options (clockTolerance, issuerBaseURL, or tokenSigningAlg)"},
  {"type": "matches", "pattern": "payload\\.permissions|requiredScopes|claimIncludes|claimEquals|claimCheck", "description": "Uses claim validation functions (requiredScopes, claimIncludes, claimCheck) for RBAC"},
  {"type": "not_contains_any", "values": ["express-jwt", "jwks-rsa"], "description": "Does not use deprecated express-jwt + jwks-rsa combo (uses current express-oauth2-jwt-bearer)"},

  {"type": "judge", "question": "Check ONLY for these skill-specific advanced patterns (NOT basic SDK usage like auth, checkJwt, app.use(auth, Authorization, Bearer). Look for at least 2 of: (1) SDK version 1.7.4 in a dependency declaration (e.g., '^1.7.4' in package.json), (2) specific validation options like clockTolerance or issuerBaseURL passed to auth() config object, (3) RBAC implementation using requiredScopes() or claimIncludes() with permissions claim, (4) DPoP configuration via dpop option, (5) CORS configured explicitly before auth middleware. These are advanced patterns taught by the skill that are NOT part of standard quickstart knowledge. Answer YES only if at least 2 of these advanced patterns are present, NO otherwise.", "examples": "PASS: Uses '^1.7.4' in package.json AND uses requiredScopes() or claimIncludes() for RBAC authorization.\nPASS: Sets clockTolerance in auth() config AND configures CORS middleware before auth middleware.\nFAIL: Only configures basic auth() with issuerBaseURL and audience — no advanced options, no RBAC, no specific version.\nFAIL: Uses checkJwt correctly but has no permission checking beyond basic JWT validation and no advanced configuration.", "framework": "express", "description": "skill-attribution-judge: At least 2 skill-specific advanced patterns present (version, advanced validation options, RBAC, DPoP, CORS ordering)"}
]


path: tests/prompt.md
content: ---
skills: express-oauth2-jwt-bearer
---

## Agent System

You are a software developer adding Auth0 authentication to an Express Node.js application.
You have access to tools for reading/writing files, running commands, and fetching URLs.
Use these tools to complete the integration task below.

## Task

Add Auth0 authentication to an Express Node.js application using the express-oauth2-jwt-bearer SDK.

**Auth0 Credentials:**
- Domain: `dev-example.auth0.com`
- Client ID: `abc123def456ghi789jkl012`
- API Audience: `https://api.example.com`


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)