diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c7bed4e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +# Agent Guidelines + +See [@CLAUDE.md](./CLAUDE.md) for the full AI-agent guidelines for this repository. + +`CLAUDE.md` is the single source of truth — persona, working principles, boundaries, security, commands, and the `references/*.md` detail. This file exists so non-Claude agents (Codex CLI, Gemini CLI, and other tools that read `AGENTS.md`) use the same guidance. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5531b30 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,126 @@ +# AI Agent Guidelines for auth0-server-python + +This document provides context and guidelines for AI coding assistants working with the auth0-server-python codebase. + +## Your Role + +You are a Python SDK engineer working on auth0-server-python, Auth0's server-side authentication SDK for Python web applications. You write async-first, type-annotated code with Pydantic models and a pluggable storage abstraction, and you keep the public `ServerClient` API stable across the SDK's supported Python versions (3.9–3.12). + +## 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 + +**auth0-server-python** is Auth0's server-side Python SDK for implementing user authentication in Python web applications — interactive login, backchannel login/logout, token management, user linking, and connected accounts. + +- **Language:** Python (supports 3.9–3.12) +- **Tech Stack:** authlib, PyJWT + cryptography, httpx (async), Pydantic v2, jwcrypto +- **Package Manager:** Poetry +- **Minimum Platform Version:** Python 3.9 +- **Dependencies:** authlib, pyjwt, httpx, pydantic · test: pytest, pytest-asyncio, pytest-mock (full list in `pyproject.toml` — bumping a dep is Ask-First) + +## Project Structure + +``` +auth0-server-python/ +├── src/auth0_server_python/ +│ ├── auth_server/ # ServerClient (main API) + mfa_client, my_account_client +│ ├── auth_schemes/ # bearer auth scheme +│ ├── auth_types/ # Pydantic models / typed options +│ ├── store/ # AbstractDataStore + StateStore (pluggable session/state storage) +│ ├── encryption/ # encrypt/decrypt for stored state +│ ├── error/ # Auth0Error hierarchy +│ ├── utils/ # PKCE, State, helpers +│ ├── telemetry.py # builds the Auth0-Client header +│ └── tests/ # pytest suite (async) +├── examples/ # hand-written usage guides (.md, one per use case) +└── pyproject.toml # Poetry config, deps, pytest options +``` + +### Key Files + +| File | Purpose | +|------|---------| +| `src/auth0_server_python/auth_server/server_client.py` | `ServerClient` — the SDK's public API surface | +| `src/auth0_server_python/store/abstract.py` | `AbstractDataStore` / `StateStore` — storage contract to implement | +| `src/auth0_server_python/error/__init__.py` | `Auth0Error` exception hierarchy | +| `src/auth0_server_python/telemetry.py` | `Auth0-Client` telemetry header | +| `pyproject.toml` | Deps, `ruff`/`pytest` config, coverage settings | + +## Boundaries + +### ✅ Always Do +- Run `poetry run pytest` and `poetry run ruff check .` before committing. +- Add or update a test for every change (`src/auth0_server_python/tests/`). +- Mark new coroutine tests with `@pytest.mark.asyncio`. +- Raise typed errors from the `Auth0Error` hierarchy (`error/__init__.py`), not bare `Exception`. +- Update `README.md` and the relevant `examples/*.md` in the same PR when you change the public API, configuration options, or supported integration patterns. + +### ⚠️ Ask First +- Adding a new dependency or bumping one in `pyproject.toml` / `poetry.lock`. +- Changing the public `ServerClient` method signatures or the `AbstractDataStore` contract (breaks downstream implementers). +- Dropping or changing supported Python versions (3.9–3.12 matrix). +- Any breaking change to public behavior — confirm before proceeding. + +### 🚫 Never Do +- Commit secrets, client secrets, tokens, or the state-encryption `secret`/`salt`. +- Log access tokens, refresh tokens, ID tokens, or the encryption secret. +- Weaken token/JWT verification (signature, `iss`/`aud`/`exp` checks) to make something pass. +- Skip or delete failing tests without fixing the cause. + +## Security Considerations + +- **Token handling:** JWTs are verified and decoded via PyJWT/jwcrypto against the tenant's JWKS (fetched and cached from OIDC metadata) — never trust an unverified token. +- **State storage:** session/transaction state is encrypted before it hits the pluggable store (`encryption/encrypt.py`, AES key derived from the configured `secret` + `salt`); `ServerClient` refuses to start without a `secret`. +- **PKCE:** the authorization-code flow uses PKCE (`utils.PKCE`). +- **Secrets stay out of code and logs:** the encryption `secret`, client secret, and tokens are runtime inputs — never hardcode or log them. + +--- + +> 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 pointer only when the task needs it. + +## Commands + +```bash +poetry install # install deps (with dev group) +poetry run pytest # run all tests (async) — safe, no credentials +poetry run ruff check . # lint +``` + +See [references/commands.md](references/commands.md) for the full list (coverage, single-test, format, build). Read only when you need to run, test, or build something beyond the three above. + +## Testing + +`poetry run pytest` runs the full suite with coverage (configured in `pyproject.toml`). Tests are async (`pytest-asyncio`) and use `unittest.mock.AsyncMock`/`pytest-mock` — the default suite is unit-only and needs no credentials or live tenant. + +See [references/testing.md](references/testing.md) for the async test conventions, mocking approach, and coverage. Read when writing or running tests. + +## Code Style + +Python formatted and linted with **ruff** (line length 100, target py39). CI runs `ruff check .` and fails on violations — enabled rule sets include `E/W/F/I` (pycodestyle/pyflakes/isort), `B` (bugbear), `UP` (pyupgrade), and `S` (bandit security). + +See [references/code-style.md](references/code-style.md) for naming, the async/typed idiom, and good/bad examples. Read when writing or reshaping code. + +## Git Workflow + +Branch off `main`; run `poetry run pytest` before opening a PR against `main`, following [Auth0's contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md). Every change ships with a test. + +See [references/git-workflow.md](references/git-workflow.md) for the full contribution and PR flow. Read when preparing a PR. + +## Common Pitfalls + +The high-frequency traps: **forgetting `@pytest.mark.asyncio` on coroutine tests**, catching a bare `Exception` instead of an `Auth0Error` subclass, and bandit (`S`) lint failures on crypto/subprocess code. + +See [references/pitfalls.md](references/pitfalls.md) for the full list with fixes (JWKS caching, Pydantic v2 model changes, store encryption contract). Read when a test or lint fails unexpectedly. + +## Docs Update Rules + +Tracked docs: `README.md` (install + getting started) and `examples/*.md` (one hand-written guide per use case — InteractiveLogin, MFA, ConnectedAccounts, UserLinking, etc.). There is no generated API-doc site — the examples are the primary reference. + +See [references/docs-update.md](references/docs-update.md) for the code-to-docs mapping (which exported symbol maps to which doc/example). Read when changing the public API or configuration. diff --git a/references/code-style.md b/references/code-style.md new file mode 100644 index 0000000..57d169a --- /dev/null +++ b/references/code-style.md @@ -0,0 +1,40 @@ +# Code Style + +## Formatting & linting (CI-enforced) + +- **Tool:** ruff (`.ruff.toml`), line length 100, `target-version = py39`. CI runs `ruff check .` and fails on violations. +- **Enabled rule sets:** `E`/`W` (pycodestyle), `F` (pyflakes), `I` (isort — keep imports sorted/grouped), `B` (bugbear), `C4` (comprehensions), `UP` (pyupgrade — use modern Python idioms), `S` (bandit security), `PLC0415` (no imports inside functions). +- **Ignored:** `E501` (line length handled separately), `B904`, `S101`/`S105`/`S106` (assert + hardcoded-password heuristics that misfire in tests). + +## Naming & idiom + +- Idiomatic Python: `snake_case` for functions/variables, `PascalCase` for classes, module-level constants `UPPER_SNAKE`. +- The SDK is **async-first** and **type-annotated**: public methods are `async def` with full type hints; options are typed (Pydantic models in `auth_types/`, `Generic[TStoreOptions]`). +- Errors come from the `Auth0Error` hierarchy in `error/` — define a new subclass rather than raising a bare `Exception`. + +**✅ Good:** + +```python +async def get_user(self, store_options: Optional[dict[str, Any]] = None) -> Optional[dict[str, Any]]: + session = await self.get_session(store_options) + if session is None: + return None + return session.get("user") +``` + +**❌ Bad:** + +```python +def get_user(self, store_options=None): # not async, no type hints + session = self.get_session(store_options) # missing await on a coroutine + try: + return session["user"] + except Exception: # bare except instead of an Auth0Error + return None +``` + +## Patterns + +- **Pluggable storage:** state/session persistence goes through `AbstractDataStore` / `StateStore` — depend on the abstraction, don't hardcode a backend. +- **Encrypted state:** stored state is encrypted via `encryption/encrypt.py`; don't persist plaintext session data. +- **Cached OIDC/JWKS:** metadata and JWKS are fetched once and cached — reuse the cached helpers rather than re-fetching. diff --git a/references/commands.md b/references/commands.md new file mode 100644 index 0000000..2d98f4e --- /dev/null +++ b/references/commands.md @@ -0,0 +1,32 @@ +# Commands + +Canonical source: `pyproject.toml` (Poetry) and `.github/workflows/test.yml`. + +## Everyday + +```bash +poetry install # install deps (dev group included) +poetry run pytest # run all tests (async), with coverage per pyproject.toml +poetry run ruff check . # lint +``` + +## Coverage & single test + +```bash +# Coverage is on by default (addopts in pyproject.toml): term-missing + xml +poetry run pytest -v --cov=auth0_server_python --cov-report=term-missing --cov-report=xml + +# Run a single test file or test +poetry run pytest src/auth0_server_python/tests/test_server_client.py +poetry run pytest src/auth0_server_python/tests/test_server_client.py::test_init_no_secret_raises +``` + +## Lint autofix & build + +```bash +poetry run ruff check . --fix # apply autofixable lint fixes +poetry run ruff format . # format +poetry build # build the package (sdist + wheel) +``` + +CI (`test.yml`) runs `poetry run pytest` across Python 3.9–3.12 and `poetry run ruff check .`. diff --git a/references/docs-update.md b/references/docs-update.md new file mode 100644 index 0000000..e4df9d4 --- /dev/null +++ b/references/docs-update.md @@ -0,0 +1,27 @@ +# Docs Update Rules + +Treat docs as part of the change. This is a **library**, so the public surface is the exported API — chiefly `ServerClient` and the `AbstractDataStore` contract. There is no generated API-doc site; `README.md` and the `examples/*.md` guides are the reference. + +## Tracked docs + +| Doc | Covers | Maintained | +|-----|--------|-----------| +| `README.md` | Install + getting started (client construction, basic login) | Hand-written | +| `examples/*.md` | One guide per use case (see mapping below) | Hand-written | + +> No `EXAMPLES.md` at the repo root — usage guides live as individual files under `examples/`. + +## When you change code, update these docs + +| Code change | Update | +|-------------|--------| +| `ServerClient` construction / config options | `README.md` "Create the Auth0 SDK client" + `examples/ConfigureStore.md` | +| Interactive login (`start_/complete_interactive_login`) | `examples/InteractiveLogin.md` | +| Backchannel login/logout | `examples/ClientInitiatedBackChannelLogin.md` | +| MFA (`mfa_client`) | `examples/MFA.md` | +| Connected accounts (`*_connect_account`, `list/delete_connected_account`) | `examples/ConnectedAccounts.md` | +| User linking (`start_/complete_link_user`, unlink) | `examples/UserLinking.md` | +| Token retrieval / connection tokens | `examples/RetrievingData.md`, `examples/CustomTokenExchange.md` | +| Custom-domain handling | `examples/MultipleCustomDomains.md` | + +> When you touch a public symbol that maps to a doc above, update that doc **in the same PR** — do not defer. diff --git a/references/git-workflow.md b/references/git-workflow.md new file mode 100644 index 0000000..91f855f --- /dev/null +++ b/references/git-workflow.md @@ -0,0 +1,19 @@ +# Git Workflow + +Follows [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) (referenced from `CONTRIBUTING.md`). + +## Flow + +1. Branch off `main` with a short, descriptive name. +2. `poetry install` to set up the environment. +3. Make the change; add/update a test (every change ships with a test). +4. Run `poetry run pytest` and `poetry run ruff check .` locally. +5. Open a PR against `main` and complete the PR template. + +## CI gates + +`test.yml` must pass: `poetry run pytest` across Python 3.9–3.12 and `poetry run ruff check .`. CodeQL and SCA/Snyk scans also run. + +## Releases + +Publishing is automated via `publish.yml` (Poetry build + dynamic versioning); the version lives in `.version` / `pyproject.toml`. Don't hand-cut a release or bump the version as part of a feature PR. diff --git a/references/pitfalls.md b/references/pitfalls.md new file mode 100644 index 0000000..1e0c6a2 --- /dev/null +++ b/references/pitfalls.md @@ -0,0 +1,10 @@ +# Common Pitfalls + +- **Forgetting `@pytest.mark.asyncio`.** The SDK is async; a coroutine test without the marker is silently skipped or errors. Mark every `async def test_*`. +- **Missing `await`.** `ServerClient` methods and the store contract are coroutines — a forgotten `await` returns a coroutine object, not the result. Bugbear (`B`) catches some cases, not all. +- **Bare `except Exception`.** Catch/raise the specific `Auth0Error` subclass (`ApiError`, `MissingTransactionError`, `MissingRequiredArgumentError`, …) so callers can branch on `.code`. Bandit/bugbear will also flag broad excepts. +- **Bandit (`S`) lint failures.** The `S` rule set scans crypto and subprocess usage. When working in `encryption/` or JWT verification, expect bandit to scrutinize it — don't silence a finding by weakening the crypto. +- **Weakening JWT/JWKS verification.** Tokens are verified against the tenant's cached JWKS with issuer/audience/expiry checks. Don't disable a check to make a test pass; fix the token/fixture instead. +- **Constructing `ServerClient` without a `secret`.** It raises `MissingRequiredArgumentError("secret")` by design — the secret drives state encryption. Pass one (a test value) in tests. +- **Pydantic v2 semantics.** Models are Pydantic v2 (`auth_types/`); use v2 APIs (`model_dump`, `model_validate`) — not the removed v1 methods. +- **Reusing OIDC/JWKS fetches.** Metadata and JWKS are cached on the client; call the cached helpers rather than adding a fresh network fetch. diff --git a/references/testing.md b/references/testing.md new file mode 100644 index 0000000..ed49cdc --- /dev/null +++ b/references/testing.md @@ -0,0 +1,36 @@ +# Testing + +## Running + +```bash +poetry run pytest +``` + +Runs the full suite with coverage (configured in `pyproject.toml` `addopts`: `--cov=auth0_server_python --cov-report=term-missing:skip-covered --cov-report=xml`). No credentials or live tenant required — everything is mocked. CI runs this across Python 3.9, 3.10, 3.11, 3.12. + +## Location & framework + +- Tests live in `src/auth0_server_python/tests/` (`test_server_client.py`, `test_mfa_client.py`, `test_my_account_client.py`, `test_telemetry.py`). +- **Framework:** pytest with `pytest-asyncio` and `pytest-mock`. + +## Conventions + +- The SDK is async, so most tests are coroutines — mark them with `@pytest.mark.asyncio`. +- Mock collaborators with `unittest.mock.AsyncMock` / `MagicMock` (and the `mocker` fixture from `pytest-mock`); pass `AsyncMock()` for `state_store` / `transaction_store` when constructing a `ServerClient`. +- Patch outbound HTTP (`httpx`) and OIDC-metadata/JWKS fetches rather than hitting the network. +- Name tests for the behavior under test, e.g. `test_start_interactive_login_no_redirect_uri`, `test_init_no_secret_raises`. + +## Mocking pattern + +```python +@pytest.mark.asyncio +async def test_something(mocker): + client = ServerClient( + ..., + state_store=AsyncMock(), + transaction_store=AsyncMock(), + secret="test-secret", + ) + mocker.patch.object(client, "_fetch_oidc_metadata", return_value={...}) + ... +```