# `api-service` example A standalone backend service whose API routes are gated by OAuth2 bearer tokens issued by an **external** identity provider. Use this shape when you have a backend (microservice, M2M API, worker-fronting endpoint) that needs to accept tokens but has no frontend of its own. ## What this example includes - `HonoResourceServer` with `IntrospectionTokenReader` validating bearer tokens against the external IDP's `/oauth2/introspect` endpoint (RFC 7662). - API routes demonstrating different scope requirements: - `GET /api/public` — no token required - `GET /api/private` — any valid token - `GET /api/write` — token must carry `write` scope - `GET /api/admin` — token must carry `admin` scope - An interactive `GET /` walkthrough page that drives every OAuth2 grant type against the companion `app-with-own-auth/` example and tests the resulting token against `/api/*`. - A dev-only `/dev/callback` redirect target and `/dev/token` proxy so the homepage can exercise the auth-code flow with server-side client credentials. Displayed curl commands use a client-secret placeholder for local terminal use. Real api-services don't host these. The auth-code flow uses PKCE (required by default under OAuth 2.1, even for confidential clients): the homepage mints a verifier, stashes it in an HttpOnly cookie, and puts only the S256 challenge in the authorize URL; `/dev/callback` reads the verifier back to complete the token exchange. - Tests that stub `tokenReader.getToken` directly (no introspection wire format faked in test code — the framework owns that). ## Running it This example is a pnpm workspace member that depends on the package through `@udibo/oauth2: workspace:*`, which resolves to the package's built `dist/`. From the repository root, install and build once: ```bash pnpm install pnpm build ``` By default this example points at `app-with-own-auth/` on port 8001 as its IDP. Start both: ```bash # Terminal 1 — the IDP pnpm --filter example-app-with-own-auth start # Terminal 2 — this service pnpm --filter example-api-service start ``` Then open . `pnpm --filter example-api-service dev` restarts the service when a file changes (`node --watch`). Configuration is read from the environment by `config.ts`, which validates it at startup and lists every invalid variable at once. Every variable is optional; the defaults pair this service with the local `app-with-own-auth` example. | Variable | Default | Meaning | | ----------------- | ----------------------- | ---------------------------------------------------- | | `PORT` | `8002` | Port to listen on (`0` picks a free one) | | `PUBLIC_URL` | `http://localhost:PORT` | Origin of this service, used to build `redirect_uri` | | `AUTH_SERVER_URL` | `http://localhost:8001` | Origin of the identity provider | | `CLIENT_ID` | `spa` | Client this service introspects tokens as | | `CLIENT_SECRET` | `spa-secret` | Secret for `CLIENT_ID` | Run the tests with `pnpm --filter example-api-service test`. They stub `tokenReader.getToken` and call the app in-process; `serve.test.ts` also starts the real Node server on a free port. ## What to change to make this production-ready The example deliberately skips operational concerns to keep the OAuth2 wiring readable. When you copy this to a real project: - **Replace the demo credential defaults in `config.ts`.** `CLIENT_ID` and `CLIENT_SECRET` fall back to the demo client so the example stands up without setup; a real deployment sets them from your secret store and drops the fallbacks so a missing value fails at startup. - **Register this service as a client with the external IDP** (e.g. Udibo, Auth0, Cognito). The `CLIENT_ID` / `CLIENT_SECRET` here authenticate the _introspection request_ — the IDP gates who can introspect tokens. - **Drop the demo `/` walkthrough and `/dev/*` mounts.** Token acquisition is a client-app concern; this service shouldn't host it. - **Persist anything you cache on the introspection response.** The example calls introspection on every request; production deployments typically cache active tokens in memory (or Redis) until expiry to avoid hitting the IDP on every call. - **Add rate limiting and logging** on `/api/*` — both standard hardening, neither shown here. - **Wire OpenTelemetry / your tracing system** through the resource server's `getContext(c)` so you can correlate API calls back to the client and user that made them. - **If you need user data beyond `sub` and `username`**, enrich it in `getUser(data)` by joining against your own user table on `data.sub` (the IDP's stable user identifier), or by calling the IDP's `/userinfo` endpoint (OIDC). A machine token (client credentials) carries no `sub`, so `data.sub` is present only when there is a real user to join against. The introspection wire format itself is covered by `@udibo/oauth2/server/resource`'s `IntrospectionTokenReader` — you do not write that code yourself, just the `getClient` / `getUser` projection mappers in `oauth2/server.ts`.