Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# AI Agent Guidelines for express-openid-connect

@./CLAUDE.md for all coding guidelines, commands, project structure, code style, testing conventions, and boundaries.

This file exists so that non-Claude AI agents (Codex CLI, Gemini CLI, etc.) read the same instructions. All guidelines are maintained in a single place (`CLAUDE.md`) to avoid duplication and drift.
157 changes: 157 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# AI Agent Guidelines for express-openid-connect

This document provides context and guidelines for AI coding assistants working with the express-openid-connect codebase.

## Your Role

You are a Node.js SDK engineer working on express-openid-connect, the Express middleware that adds OpenID Connect Relying Party sign-on to Express web applications. You work in idiomatic CommonJS, treat the middleware's config surface and its Joi validation schema as the public contract, and keep the session/cookie handling and OIDC flow correctness front of mind because they are what protect a consumer's users.

---

## Working Principles

Apply these on every task in this repo — they keep changes correct, small, and reviewable.

- **Think before coding.** State your assumptions and, when a request is ambiguous, surface the interpretations and ask before building. Recommend a simpler approach when you see one. A clarifying question up front beats a wrong implementation.
- **Simplicity first.** Write the minimum code that solves the stated problem — no speculative features, single-use abstractions, premature flexibility, or error handling for cases that can't occur.
- **Surgical changes.** Touch only what the request requires. Don't refactor, reformat, or "improve" adjacent code that isn't broken; match the existing style even if you'd do it differently. Every changed line should trace directly to the request. Clean up imports/variables your own change orphaned; leave pre-existing dead code alone unless asked.
- **Goal-driven execution.** Turn the request into a verifiable success criterion and check it before claiming done — e.g. "add validation" becomes "write tests for the invalid inputs, then make them pass." Don't report success you haven't verified.

---

## Project Overview

**express-openid-connect** is Express middleware to protect web applications using OpenID Connect.

- **Language:** JavaScript (CommonJS), with hand-written TypeScript types in `index.d.ts`
- **Tech Stack:** Express (peer dep `>= 4.17.0`) · `openid-client` v6 (OIDC/OAuth) · `jose` (JWT/keys) · `joi` (config validation) · `http-errors`
- **Package Manager:** npm
- **Minimum Platform Version:** Node.js `^20.19.0 || ^22.12.0 || >= 23.0.0` (see `engines` in `package.json`)
- **Dependencies:** `openid-client` 6, `jose` 6, `joi` 17, `cookie` 0.7 (+7 more) · test: Mocha, Chai, Sinon, nock, tsd, Puppeteer — see `package.json` for the full list

---

## Project Structure

```
.
├── index.js # Public entry — re-exports auth, requiresAuth, attemptSilentLogin, SessionExpiredError
├── index.d.ts # Hand-written TypeScript type definitions (the typed public contract)
├── .version # Version source of truth (mirrors package.json "version")
├── middleware/ # The exported middleware factories
│ ├── auth.js # `auth()` — mounts login/logout/callback routes + session
│ ├── requiresAuth.js # `requiresAuth()` / claim guards
│ ├── attemptSilentLogin.js
│ └── unauthorizedHandler.js
├── lib/ # Internal implementation
│ ├── config.js # Joi schema — the config/public-option contract
│ ├── client.js # openid-client setup + telemetry / User-Agent headers
│ ├── context.js # Request context, OIDC flow logic (largest module)
│ ├── appSession.js # Encrypted cookie session handling
│ ├── crypto.js # Cookie encryption/signing
│ ├── transientHandler.js
│ ├── errors.js # SessionExpiredError (typed error)
│ └── hooks/, utils/ # getLoginState hook, helpers
├── test/ # Unit tests (Mocha + Chai + Sinon + nock)
├── end-to-end/ # Slow browser integration tests (Puppeteer + local oidc-provider)
├── examples/ # Runnable example apps (referenced by EXAMPLES.md and e2e tests)
└── docs/ # Generated TypeDoc API output (do not hand-edit)
```

### Key Files

| File | Why it matters |
|------|----------------|
| `index.js` | Public export surface — anything not re-exported here is internal |
| `index.d.ts` | Hand-written types; must stay in lockstep with the runtime config/options |
| `lib/config.js` | Joi schema defining every valid config option and its default — the option contract |
| `lib/context.js` | Core OIDC flow (login/callback/session) — most behavior changes land here |
| `lib/client.js` | Where the `Auth0-Client` telemetry header and `User-Agent` are set |
| `.version` | Version source of truth, kept in sync with `package.json` |

---

## Boundaries

### ✅ Always Do

- Run `npm run test` (unit) and `npm run lint` before committing.
- Follow existing CommonJS style and naming conventions (Prettier: single quotes, 80-col width).
- Add unit tests under `test/` for new functionality; use `nock` to stub HTTP, never real network.
- Keep `index.d.ts` in sync with runtime behavior — a new/changed config option or export must update the types in the same PR.
- Keep `.version` and `package.json` `"version"` in sync (both are version sources).
- Update `README.md` and `EXAMPLES.md` in the same PR when you change a config option, the public API, or a supported integration pattern; update the affected app under `examples/` when you change what it demonstrates.
- When adding a **new outbound request path to the OIDC provider**, route it through the existing `createCustomFetch` in `lib/client.js` (and the equivalent in `lib/context.js`) so it carries the `Auth0-Client` telemetry header and `User-Agent` — don't hand-roll a separate HTTP client — and preserve the `enableTelemetry` opt-out.

### ⚠️ Ask First

- **Any breaking change — always ask first.** Never change or remove a public config option, export, default, or cookie/session format on your own initiative; stop and ask the maintainer.
- Adding new dependencies or bumping existing ones.
- Modifying public API signatures or the `lib/config.js` Joi schema (options and defaults are the public contract).
- Changes to CI/CD configuration (`.github/workflows/`).
- Changing session storage or cookie encryption/format (`lib/appSession.js`, `lib/crypto.js`) — it affects existing sessions.
- Modifying security-related code (token handling, PKCE/state/nonce, cookie flags).

### 🚫 Never Do

- Commit secrets, API keys, client secrets, or tokens.
- Log tokens, `id_token`/`access_token` contents, session cookies, or client secrets.
- Modify generated files by hand: `docs/` (TypeDoc output), `CHANGELOG.md`, `coverage/`, `.nyc_output/`, lock files.
- Remove or skip failing tests without fixing the underlying cause.
- Modify `node_modules/`.
- Weaken secure defaults (cookie `httpOnly`/`secure`/`sameSite`, `RS256`, PKCE/state/nonce) without explicit approval.

---

## Security Considerations

This is an OIDC Relying Party middleware; the following are auto-detected from the code and are non-negotiable defaults:

- **Flows & PKCE:** default `response_type` is `id_token` (Implicit with Form Post); the Authorization Code flow (`response_type` includes `code`) uses PKCE and state/nonce via `openid-client`. Do not disable state/nonce/PKCE checks.
- **ID token signing:** default `idTokenSigningAlg` is `RS256`; `none` is explicitly rejected in `lib/config.js`. HS* algorithms require a `clientSecret`.
- **Session cookies:** the app session is an encrypted cookie (`lib/appSession.js` + `lib/crypto.js`) with `httpOnly: true` by default, `secure` inferred from an HTTPS `baseURL`, and a configurable `sameSite`. A warning is emitted for insecure cookies over HTTPS.
- **Secrets:** the `secret` config drives cookie encryption; never log it or commit it. Examples read secrets from env (`examples/.env.sample`), never hard-code them.
- **Telemetry:** the `Auth0-Client` header is sent by default and user-disablable via `enableTelemetry: false` — never remove the opt-out or send unconditionally.

---

> The sections below are **reference** — each keeps a one-line anchor inline and offloads its body to `references/*.md` behind a linked pointer. Read a reference file only when your task needs it.

## Commands

```bash
npm run test # unit tests (Mocha, safe — no credentials, HTTP is nocked)
npm run lint # ESLint
```

See [references/commands.md](references/commands.md) for the full list (coverage, type tests, end-to-end, docs, example app). Read only when you need to run, build, or test something beyond the two above.

## Testing

Unit tests (`npm run test`, Mocha + Chai + Sinon) live in `test/` and are the safe default — no credentials, all HTTP stubbed with `nock`. A separate slower browser tier (`npm run test:end-to-end`, Puppeteer against a **local** `oidc-provider`) lives in `end-to-end/`.

See [references/testing.md](references/testing.md) for conventions, mocking, coverage, and the end-to-end tier. Read when writing or running tests.

## Code Style

CommonJS (`require`/`module.exports`). Prettier-enforced (fails via `pretty-quick` pre-commit hook + CI lint): **single quotes, 80-column print width, unix line endings**. ESLint flags unused vars as errors.

See [references/code-style.md](references/code-style.md) for naming, patterns, and good/bad examples. Read before writing non-trivial new code.

## Git Workflow

Branch off `master`; run `npm run test` and `npm run lint` before opening a PR against `master`. A `pretty-quick` pre-commit hook auto-formats staged files.

See [references/git-workflow.md](references/git-workflow.md) for full branch/commit/PR conventions. Read when preparing a commit or PR.

## Common Pitfalls

Top traps: HTTP in unit tests must be stubbed with `nock` (network is disabled in `test/setup.js`); `index.d.ts` is hand-written and drifts easily; `lib/context.js` is large and central.

See [references/pitfalls.md](references/pitfalls.md) for the full list with fixes. Read when debugging unexpected behavior.

## Docs Update Rules

Treat docs as a first-class deliverable: a PR that changes a config option, the public API, or an integration pattern is not complete until `README.md` / `EXAMPLES.md` (and any affected `examples/` app) are updated in the same PR.

See [references/docs-update.md](references/docs-update.md) for the tracked-docs inventory and the code-to-docs mapping. Read when changing public API, config, or examples.
48 changes: 48 additions & 0 deletions references/code-style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Code Style

## Enforced tooling

- **Prettier** (`.prettierrc`): `singleQuote: true`, `printWidth: 80`. Enforced by a `pretty-quick --staged` pre-commit hook (husky) and by CI lint.
- **ESLint** (`eslint.config.js`, flat config on `@eslint/js` recommended): `no-unused-vars` is an **error** (args checked after-used, rest siblings ignored), `linebreak-style` is unix (error), `no-useless-escape` is a warning, `no-console` is off. `CHANGELOG.md`, `docs/`, `coverage/`, `.nyc_output/`, `node_modules/` are ignored.

## Naming & module conventions

- **CommonJS everywhere:** `const x = require('...')` and `module.exports = { ... }`. No ESM `import`/`export` in the library source.
- Files are camelCase matching their primary export (`appSession.js`, `transientHandler.js`, `requiresAuth.js`).
- `camelCase` for functions/variables, `PascalCase` for classes/error types (`SessionExpiredError`).
- Middleware factories are functions that return an Express `(req, res, next)` handler.

## Patterns used here

- **Config-schema-as-contract:** all public options are declared and defaulted in a `joi` schema (`lib/config.js`). Add new options there with a validation rule and a sensible secure default — don't read raw config elsewhere.
- **Typed errors:** throw the project's error types (e.g. `SessionExpiredError` in `lib/errors.js`, which carries `code`/`status`), and `http-errors` for HTTP responses, rather than bare `Error`.
- **Custom fetch wrapper:** outbound OIDC HTTP goes through `createCustomFetch` (`lib/client.js`), which injects the `User-Agent` and (opt-in) `Auth0-Client` headers.

## Examples

**✅ Good** — CommonJS, single quotes, typed error, schema-driven option:

```javascript
const createHttpError = require('http-errors');

function requiresAuth(req, res, next) {
if (!req.oidc.isAuthenticated()) {
return next(createHttpError(401, 'Authentication required'));
}
next();
}

module.exports = requiresAuth;
```

**❌ Bad** — ESM, double quotes, bare `Error`, magic config read:

```javascript
export function requiresAuth(req, res, next) { // no ESM in this repo
if (!req.oidc.isAuthenticated()) {
throw new Error("Authentication required"); // use http-errors / typed errors; double quotes fail Prettier
}
const ttl = process.env.SESSION_TTL || 86400; // options belong in the lib/config.js Joi schema
next();
}
```
39 changes: 39 additions & 0 deletions references/commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Commands

Full command reference for express-openid-connect. All commands are `npm run <script>` unless noted, and map to scripts in `package.json` and jobs in `.github/workflows/test.yml`.

## Everyday

```bash
npm install # install dependencies (NODE_ENV=development in CI build step)
npm run test # unit tests — Mocha with --max-http-header-size=16384
npm run lint # ESLint over the repo (eslint .)
```

## Testing tiers

```bash
npm run test:ci # unit tests with coverage: nyc --reporter=lcov npm test
npm run test:types # type-definition tests: tsd . (validates index.d.ts)
npm run test:end-to-end # browser integration: mocha end-to-end --timeout 30000 (Puppeteer + local oidc-provider)
```

## Docs & examples

```bash
npm run docs # generate TypeDoc API docs into docs/ (typedoc --options typedoc.js index.d.ts)
npm run start:example # run the local example app (node ./examples/run_example.js), serves http://localhost:3000
```

## What CI runs

`.github/workflows/test.yml` runs, gated on a build job:

- **unit** — `npm run test:ci` on Node 20.x / 22.x / 24.x, then uploads coverage to Codecov
- **types** — `npm run test:types`
- **mocha** — `npm run test:end-to-end`
- **lint** — `npm run lint`

Other workflows: `codeql.yml`, `snyk.yml`, `rl-secure.yml` (security scans), `release.yml` / `npm-release.yml` (release — cut by the release process, not by an agent).

> There is no separate build/compile step — this is plain CommonJS. The "Build Package" CI job just installs dependencies and caches them.
28 changes: 28 additions & 0 deletions references/docs-update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Docs Update Rules

Treat documentation as a first-class deliverable. A PR that adds or changes public API, config options, or integration patterns is **not complete** until the relevant docs are updated in the same PR.

## Tracked docs

| Doc | Covers | Status |
|-----|--------|--------|
| `README.md` | Install, requirements, getting started, configuration, API reference links | present |
| `EXAMPLES.md` | Runnable code samples and integration patterns (proxy, PAR, backchannel logout, custom stores, etc.) | present |
| `examples/` | Standalone runnable example apps exercised by the end-to-end tests | present |

> Not tracked here: `CHANGELOG.md` (cut by the release process, not agent-edited), and the migration guides (`V2_MIGRATION_GUIDE.md`, `V3_MIGRATION_GUIDE.md`) — their filenames are version-specific and the right one is inferred from the branch when a breaking change is made. `FAQ.md` / `TROUBLESHOOTING.md` / `ARCHITECTURE.md` exist for context but aren't part of the code-to-docs contract.

## When you change code, update these docs

This is a library/SDK; the public surface is the exported functions (`index.js`) and the config options (`lib/config.js` Joi schema).

| When this changes | Update these docs |
|-------------------|-------------------|
| A config option added, removed, renamed, or its default changed (`lib/config.js`) | `README.md` (configuration), `EXAMPLES.md` (affected samples), `index.d.ts` (types) |
| Public export added/removed/renamed (`index.js`: `auth`, `requiresAuth`, `attemptSilentLogin`, `SessionExpiredError`) | `README.md` (API reference / usage), `EXAMPLES.md`, `index.d.ts` |
| Authentication / session / logout flow behavior | `README.md` (getting started), `EXAMPLES.md` (auth/logout examples) |
| Install requirements / supported Node versions (`engines`) | `README.md` (requirements / install) |
| A new integration pattern supported | `EXAMPLES.md` (add an example) + an app under `examples/` if it warrants a runnable demo |
| An `examples/` app's demonstrated API changes | the matching `examples/*.js` app and its `end-to-end/*.test.js` |

> When you touch code that maps to a doc above, update that doc **in the same PR** — do not defer.
18 changes: 18 additions & 0 deletions references/git-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Git Workflow

## Branches

- Default/base branch is `master`. Branch off `master` for changes.
- No branch-name linting is enforced; use a short descriptive name (e.g. `fix/session-cookie-samesite`).

## Before you commit

- A husky `pre-commit` hook runs `pretty-quick --staged`, auto-formatting staged files with Prettier.
- Run the checks CI enforces: `npm run test` (unit), `npm run lint`, and — for type or example changes — `npm run test:types` / `npm run test:end-to-end`.

## Commits & PRs

- No commitlint/Conventional-Commits enforcement is configured; write clear, imperative commit subjects. (The maintained `CHANGELOG.md` is produced by the release process — don't hand-edit it in feature PRs.)
- There is no PR template; see `CONTRIBUTING.md` and Auth0's [general contributing guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md).
- Open PRs against `master`. CI (`.github/workflows/test.yml`) must pass: build, unit tests on Node 20/22/24, type tests, end-to-end, and lint. Security workflows (CodeQL, Snyk, rl-secure) also run.
- `CODEOWNERS` governs required reviewers.
Loading
Loading