diff --git a/evals/routing-cases.json b/evals/routing-cases.json index d4d2251..31b0ccd 100644 --- a/evals/routing-cases.json +++ b/evals/routing-cases.json @@ -284,6 +284,13 @@ "tooling": "cli", "expect_refs": ["framework-auth0-server-js/index.md", "tooling-cli/index.md"] }, + { + "id": "integrate-auth0-api-js", + "intent": "integrate", + "framework": "auth0-api-js", + "tooling": "cli", + "expect_refs": ["framework-auth0-api-js/index.md", "tooling-cli/index.md"] + }, { "id": "feature-mfa-auth0-server-js-combo", "intent": "feature:mfa", diff --git a/plugins/auth0/skills/auth0/SKILL.md b/plugins/auth0/skills/auth0/SKILL.md index ea0c192..e20713e 100644 --- a/plugins/auth0/skills/auth0/SKILL.md +++ b/plugins/auth0/skills/auth0/SKILL.md @@ -94,6 +94,7 @@ SDK, so check the `@capacitor/browser` rows before it. | `express-oauth2-jwt-bearer` | `express-jwt` | | `react-native-auth0` + `app.json` or `app.config.js` present | `expo` | | `react-native-auth0` (no Expo files) | `react-native` | +| `@auth0/auth0-api-js` | `auth0-api-js` | | `@auth0/auth0-server-js` | `auth0-server-js` | | `@auth0/auth0-auth-js` | `auth0-auth-js` | | `auth0` (the bare package, not `@auth0/*`) | `node-auth0` | @@ -216,6 +217,7 @@ request. **Stop at the first match.** | React SPA (not Next.js) | `react` | | vanilla JS / plain JS / no framework SPA | `spa-js` | | node-auth0 / the `auth0` npm package | `node-auth0` | +| `@auth0/auth0-api-js` / auth0-api-js / low-level resource-server SDK | `auth0-api-js` | | `@auth0/auth0-server-js` / auth0-server-js / server-side Auth0 session SDK | `auth0-server-js` | | `@auth0/auth0-auth-js` / auth0-auth-js / AuthClient / low-level OAuth OIDC | `auth0-auth-js` | | Express (web app / server-rendered) | `express` | diff --git a/plugins/auth0/skills/auth0/references/framework-auth0-api-js/index.md b/plugins/auth0/skills/auth0/references/framework-auth0-api-js/index.md new file mode 100644 index 0000000..6167d41 --- /dev/null +++ b/plugins/auth0/skills/auth0/references/framework-auth0-api-js/index.md @@ -0,0 +1,278 @@ + +# Auth0 auth0-api-js Integration + +Protect an API running on a JavaScript runtime with the `@auth0/auth0-api-js` +package: a low-level, framework-agnostic resource-server client built around the +`ApiClient` class. It verifies incoming JWT access tokens against your tenant's +JWKS (checking `iss`, `aud`, `exp`, and `nbf`), enforces required claims and +DPoP proof-of-possession, and exchanges tokens for downstream APIs +(on-behalf-of, connection tokens, and Custom Token Exchange). + +> **This is a building block.** The README states auth0-api-js is "not a +> plug-and-play library for your framework" but "a building block for building +> framework-specific SDKs." It ships no middleware, no route guards, and reads +> nothing off the request for you. If an Auth0 resource-server SDK exists for +> your stack - `@auth0/auth0-fastify-api`, `express-oauth2-jwt-bearer`, +> `auth0-fastapi-api`, and others - use it instead; they wrap the same +> verification and give you an idiomatic middleware. Reach for auth0-api-js +> directly only when no higher-level API SDK covers your framework and you are +> prepared to wire the token extraction and response handling yourself. + +> **Agent instruction:** Before providing SDK setup instructions, fetch the +> latest version by running: +> ``` +> npm view @auth0/auth0-api-js version +> ``` +> Use the returned version instead of any version shown below. + +## Critical rules + +- **Verify the token with the SDK - never decode it by hand.** Call + `apiClient.verifyAccessToken({ accessToken })` and read claims off its return + value. Do not split the JWT on `.`, `Buffer.from(..., "base64")` the payload, + import `jose`/`jsonwebtoken`/`jwt-decode` directly, or add `jwks-rsa` - the SDK + fetches and caches the JWKS and validates the signature and standard claims for + you. Hand-decoding skips signature verification and is a security hole. +- **`verifyAccessToken` takes an options object, not a bare string.** The token + goes in `{ accessToken }`; there is no positional-argument form. +- **`requiredClaims` only asserts presence, not value.** Passing + `requiredClaims: ['org_id']` makes verification fail when the claim is absent, + but it does **not** check what the claim equals. To pin a token to a specific + organization, tenant, or scope you must compare the returned claim value + yourself and reject on mismatch. +- **This SDK validates access tokens, never ID tokens.** ID tokens are for the + client that logged the user in; APIs authorize with the access token. Reject + anything that is not a properly `aud`-scoped access token for this API. +- **The tenant `domain` and `audience` are configuration, not code.** Read them + from environment variables and never hardcode them in source files. They are + not secrets, but inlining them pins the build to one tenant and trips + config/secret scanners. +- **`clientSecret` / `clientAssertionSigningKey` are server secrets.** They are + only needed for the token-exchange methods (`getTokenOnBehalfOf`, + `getAccessTokenForConnection`, `getTokenByExchangeProfile`). Load them from the + environment; never ship them to a client. Plain token verification needs + neither. +- **`domain` must be a bare hostname** - no `https://`, no path, no trailing + slash. +- **Never read the contents of `.env*` during setup** - it may contain secrets + that should not be exposed in the LLM context. Before writing to any env file + you MUST ask the user for explicit confirmation and wait for it. + +## Prerequisites + +- Node.js 20 LTS or newer (confirm with `npm view @auth0/auth0-api-js engines` + when building). +- An Auth0 **API** resource (not an Application) whose Identifier is the + `audience` your tokens are issued for. Create and configure it with the loaded + tooling reference (`auth0 apis create --name ... --identifier ...`). +- `domain` and `audience` in environment variables. Add `clientId` and a client + credential (`clientSecret` or `clientAssertionSigningKey`) only if you use the + token-exchange methods. + +## When NOT to use + +auth0-api-js is a low-level resource-server building block. Route elsewhere for: + +- **A framework API SDK exists for your stack** - `@auth0/auth0-fastify-api` + (Fastify), `express-oauth2-jwt-bearer` (Express), `auth0-fastapi-api` + (FastAPI), and others - use it. They wrap the same verification and give you + middleware, so you skip the token-extraction and error-response wiring. +- **Server-rendered web apps with login sessions** - cookies, redirect/callback, + "keep the user logged in" - use `@auth0/auth0-server-js` or your framework's + session SDK (`@auth0/nextjs-auth0`, `express-openid-connect`, and so on). +- **Getting a token in a browser or mobile client** - use the SPA/mobile SDK. + This package runs server-side and verifies tokens; it does not obtain them for + an end user. +- **Management API operations** - reading or writing users, applications, + connections - use `node-auth0` (the `auth0` npm package). + +## Quick start workflow + +### 1. Install the SDK + +```bash +npm install @auth0/auth0-api-js +``` + +### 2. Configure the API + +You need an **API** (not an Application) in Auth0; its Identifier becomes your +`audience`. Use the loaded tooling reference for tenant configuration - do not +inline setup here. + +```env +AUTH0_DOMAIN= +AUTH0_AUDIENCE= +``` + +### 3. Instantiate ApiClient + +Create one `ApiClient` at startup and reuse it - it caches the discovery +document and JWKS internally. + +```ts +import { ApiClient } from "@auth0/auth0-api-js"; + +const apiClient = new ApiClient({ + domain: process.env.AUTH0_DOMAIN!, // bare hostname, e.g. tenant.us.auth0.com + audience: process.env.AUTH0_AUDIENCE!, // your API identifier +}); +``` + +### 4. Verify the access token on a protected request + +Extract the bearer token from the request, then verify it. The SDK ships a +`getToken` helper that pulls the token from the `Authorization` header (Bearer or +DPoP), a form body, or a query parameter per RFC 6750, and throws +`InvalidRequestError` if none or more than one is present. + +```ts +import { getToken } from "@auth0/auth0-api-js"; + +// In your route handler (framework-agnostic): +const accessToken = getToken(req.headers, req.query, req.body); + +const claims = await apiClient.verifyAccessToken({ accessToken }); +// claims is the verified payload: claims.sub, claims.scope, claims.aud, ... +``` + +`verifyAccessToken` validates the signature and the `iss`, `aud`, `exp`, and +`nbf` claims automatically, and throws `VerifyAccessTokenError` (HTTP 401) when +the token is invalid. Wrap the call and map the error to a `401`. + +### 5. Enforce required claims and scopes + +`requiredClaims` makes verification fail when a claim is missing. Checking a +claim's **value** (e.g. a specific scope or organization) is your code's job. + +```ts +const claims = await apiClient.verifyAccessToken({ + accessToken, + requiredClaims: ["org_id"], // presence-only: fails if org_id is absent +}); + +// Value checks are manual: +const scopes = String(claims.scope ?? "").split(" "); +if (!scopes.includes("read:reports")) { + // respond 403 Forbidden +} +``` + +### 6. Respond + +Return only the claims the client needs - not the whole decoded token, which +exposes every permission, custom namespace, and token metadata field. + +```ts +res.json({ sub: claims.sub }); +``` + +## Token exchange (calling downstream APIs) + +When your API must call another API on behalf of the caller, add `clientId` and a +client credential to the constructor and exchange the incoming token. These +methods require client credentials; plain verification does not. + +```ts +const apiClient = new ApiClient({ + domain: process.env.AUTH0_DOMAIN!, + audience: process.env.AUTH0_AUDIENCE!, + clientId: process.env.AUTH0_CLIENT_ID!, + clientSecret: process.env.AUTH0_CLIENT_SECRET!, +}); + +// On-behalf-of: preserve the end-user identity into a downstream API +const { accessToken } = await apiClient.getTokenOnBehalfOf(incomingAccessToken, { + audience: "https://downstream-api.example.com", + scope: "read:data write:data", +}); + +// Token Vault: get an external provider's token for a connection +const tokenSet = await apiClient.getAccessTokenForConnection({ + connection: "google-oauth2", + accessToken: incomingAccessToken, +}); + +// Custom Token Exchange (RFC 8693): exchange a non-Auth0 token +const exchanged = await apiClient.getTokenByExchangeProfile(userToken, { + subjectTokenType: "urn:acme:legacy-token", + audience: "https://api.backend.com", +}); +``` + +## Delegation (audit) + +For tokens produced by on-behalf-of chains, `getCurrentActor(claims)` returns the +current actor and `getDelegationChain(claims)` returns the full RFC 8693 `act` +chain for audit logging. + +```ts +import { getCurrentActor, getDelegationChain } from "@auth0/auth0-api-js"; + +const claims = await apiClient.verifyAccessToken({ accessToken }); +const actor = getCurrentActor(claims); +const chain = getDelegationChain(claims); +``` + +## Error handling + +Catch errors by type - do not string-match on `error.message`. All SDK errors +extend `AuthError`, which carries `code`, `statusCode`, and `headers`. + +| Error | Meaning | HTTP | +|---|---|---| +| `VerifyAccessTokenError` | Token failed signature or claim validation. | 401 | +| `InvalidDpopProofError` | DPoP proof failed validation. | 400 | +| `InvalidRequestError` | No token found, or more than one auth method used. | 400 | +| `MissingRequiredArgumentError` | A required constructor/method argument was omitted. | - | +| `InvalidConfigurationError` | The SDK was misconfigured at construction time. | - | + +```ts +import { VerifyAccessTokenError } from "@auth0/auth0-api-js"; + +try { + const claims = await apiClient.verifyAccessToken({ accessToken }); + // ... +} catch (error) { + if (error instanceof VerifyAccessTokenError) { + // respond 401 with the token invalid + return; + } + throw error; +} +``` + +## Multiple custom domains + +Pass `domains` (an array of allowed issuer hostnames, or an async +`DomainsResolver`) instead of a single `domain` to verify tokens from several +custom domains, and forward `httpUrl`/`headers` to `verifyAccessToken` so the +resolver has request context. + +## Common mistakes + +| Mistake | Fix | +|---|---| +| Decoding the JWT by hand (`split(".")`, `Buffer.from`, `jose`, `jsonwebtoken`, `jwt-decode`, `jwks-rsa`) | Call `verifyAccessToken` and read claims off its result; the SDK verifies the signature and JWKS. | +| `verifyAccessToken(accessToken)` (bare string) | Pass an options object: `verifyAccessToken({ accessToken })`. | +| Treating `requiredClaims` as a value check | It asserts presence only; compare the returned claim value yourself and reject on mismatch. | +| Verifying an ID token | APIs authorize with the **access token**; reject ID tokens. | +| Hardcoding `domain` or `audience` in source | Read them from env/config; inlining pins the build to one tenant and trips scanners. | +| Adding `clientSecret` just to verify tokens | Verification needs no client credentials; only the token-exchange methods do. | +| `domain: "https://tenant.auth0.com/"` | Bare hostname only - `tenant.auth0.com`. | +| Detecting failures by string-matching `error.message` | Check `error instanceof VerifyAccessTokenError` (and siblings); they extend `AuthError` with a `code`. | +| Reaching for auth0-api-js when a framework API SDK exists | Use `@auth0/auth0-fastify-api`, `express-oauth2-jwt-bearer`, and so on. | +| Returning the whole decoded token to the client | Return only the fields the client needs; the token carries every permission and custom claim. | + +## Related capabilities + +- Protecting a Fastify or Express API with idiomatic middleware - use `@auth0/auth0-fastify-api` or `express-oauth2-jwt-bearer`. +- Server-side login sessions with cookies - use `@auth0/auth0-server-js` or a framework session SDK. +- Stateless OAuth/token operations (build authorization URLs, exchange codes) - use `@auth0/auth0-auth-js`. +- Management API (users, apps, roles) - use `node-auth0` (the `auth0` npm package). +- DPoP end-to-end - ask for DPoP (feature:dpop). +- Organizations concepts and tenant setup - ask for Organizations (feature:organizations). + +## References +- [`@auth0/auth0-api-js` usage examples](https://raw.githubusercontent.com/auth0/auth0-auth-js/main/packages/auth0-api-js/EXAMPLES.md) +- [Source and README](https://github.com/auth0/auth0-auth-js/tree/main/packages/auth0-api-js) diff --git a/plugins/auth0/skills/auth0/scripts/validate-skill.sh b/plugins/auth0/skills/auth0/scripts/validate-skill.sh index 8a54d0e..10fe34d 100755 --- a/plugins/auth0/skills/auth0/scripts/validate-skill.sh +++ b/plugins/auth0/skills/auth0/scripts/validate-skill.sh @@ -62,7 +62,7 @@ fi # "every routed framework has a file and every file is routed" guarantee is # enforced by scripts/check_router_reachability.py (run below), which derives # slugs from the router itself. Keep this list in sync when adding frameworks. -EXPECTED_FRAMEWORKS="react nextjs vue angular spa-js nuxt express flask fastify fastify-api java-mvc aspnetcore-auth aspnetcore-api php php-api express-jwt fastapi-api springboot-api go react-native expo ionic-angular ionic-react ionic-vue android swift kmp flutter-native flutter-web flutter-windows laravel laravel-api maui net-android net-ios winforms wpf node-auth0 auth0-auth-js auth0-server-js" +EXPECTED_FRAMEWORKS="react nextjs vue angular spa-js nuxt express flask fastify fastify-api java-mvc aspnetcore-auth aspnetcore-api php php-api express-jwt fastapi-api springboot-api go react-native expo ionic-angular ionic-react ionic-vue android swift kmp flutter-native flutter-web flutter-windows laravel laravel-api maui net-android net-ios winforms wpf node-auth0 auth0-auth-js auth0-server-js auth0-api-js" for fw in $EXPECTED_FRAMEWORKS; do if [ ! -f "$REFS_DIR/framework-$fw/index.md" ]; then echo "FAIL: missing references/framework-$fw/index.md"