|
| 1 | +# AI Agent Guidelines for auth0-server-python |
| 2 | + |
| 3 | +This document provides context and guidelines for AI coding assistants working with the auth0-server-python codebase. |
| 4 | + |
| 5 | +## Your Role |
| 6 | + |
| 7 | +You are a Python SDK engineer on auth0-server-python — Auth0's framework-agnostic server-side authentication SDK. You work in async OIDC flows, Pydantic-typed models, and pluggable session/transaction stores that integrators supply. |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## Working Principles |
| 12 | + |
| 13 | +Apply these on every task in this repo — they keep changes correct, small, and reviewable. |
| 14 | + |
| 15 | +- **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. |
| 16 | +- **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. |
| 17 | +- **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. |
| 18 | +- **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. |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## Project Structure |
| 23 | + |
| 24 | +**auth0-server-python** is the server-side (confidential client) SDK that framework wrappers such as `auth0-fastapi` build on. |
| 25 | + |
| 26 | +``` |
| 27 | +src/auth0_server_python/ |
| 28 | +├── auth_server/ |
| 29 | +│ ├── server_client.py # ServerClient — main entry point; login, session, token, passkey, CTE flows |
| 30 | +│ ├── mfa_client.py # MfaClient — MFA API; exposed via ServerClient.mfa |
| 31 | +│ └── my_account_client.py # MyAccountClient — My Account API (stateless; takes a user token per call) |
| 32 | +├── auth_schemes/ |
| 33 | +│ ├── bearer_auth.py # BearerAuth — httpx.Auth strategy |
| 34 | +│ └── dpop_auth.py # DPoPAuth — RFC 9449 proofs, nonce retry, EC P-256 enforcement |
| 35 | +├── auth_types/__init__.py # All public Pydantic models + Literal type aliases |
| 36 | +├── error/__init__.py # Auth0Error hierarchy + *ErrorCode constant classes |
| 37 | +├── store/abstract.py # StateStore / TransactionStore ABCs integrators implement |
| 38 | +├── encryption/encrypt.py # JWE encrypt/decrypt (HKDF-SHA256 → A256CBC-HS512) |
| 39 | +├── utils/helpers.py # PKCE, State, URL helpers; org-claim + domain-resolver validation |
| 40 | +├── telemetry.py # Auth0-Client header construction |
| 41 | +└── tests/ # pytest suite, one test_<module>.py per module |
| 42 | +examples/ # per-feature Markdown guides (not runnable apps) |
| 43 | +references/ # Agent reference docs (this file's offloaded sections) |
| 44 | +``` |
| 45 | + |
| 46 | +Before working on a specific flow — interactive login, CIBA, passkeys, CTE, MFA, My Account, user linking, connected accounts, MCD — read its row in [references/flow-map.md](references/flow-map.md) for the entry points, supporting modules, and guide. |
| 47 | + |
| 48 | +--- |
| 49 | + |
| 50 | +## Boundaries |
| 51 | + |
| 52 | +### ✅ Always Do |
| 53 | + |
| 54 | +- Run `poetry run pytest` and `poetry run ruff check .` before committing — both gate CI. |
| 55 | +- Add tests for new functionality, and mark async tests `@pytest.mark.asyncio` — see Common Pitfalls for why. |
| 56 | +- Raise a typed error from `error/` with a stable `code`, and re-raise the SDK's own errors untouched inside a catch-all — never return `None` or a bare `dict` to signal failure. |
| 57 | +- Return Pydantic models from `auth_types/`, validated via `model_validate` — never leak an unvalidated response `dict` through the public API. |
| 58 | +- Resolve the domain through `await self._resolve_current_domain(store_options)` and thread `store_options` through every new public flow method — reading `self._domain` breaks MCD deployments and cookie-backed stores. |
| 59 | +- Issue HTTP through `self._get_http_client()` so the `Auth0-Client` telemetry header is attached. When adding a **new outbound request path to Auth0**, route it through the existing `src/auth0_server_python/telemetry.py` mechanism rather than constructing a client or headers by hand. |
| 60 | +- Attach credentials with `BearerAuth` / `DPoPAuth` from `auth_schemes/`, not by setting `Authorization` inline. |
| 61 | +- Keep `Optional[X]` / `Union[...]` typing and `target-version = "py39"`-compatible syntax — the 3.9 CI leg fails on 3.10+ constructs. |
| 62 | +- Update `README.md` and the matching `examples/*.md` guide in the same PR when changing the public API, configuration options, or supported integration patterns. |
| 63 | +- Put new code snippets in the matching `examples/*.md` guide and link to it from `README.md` — don't add another code block to `README.md`. |
| 64 | +- Keep `.version` and `pyproject.toml`'s `version` in sync when either is touched. |
| 65 | +- Preserve existing declaration order and the `# ==== SECTION ====` banners; insert new methods into the matching section. In a file without banners, group a new method/class/function with the related code it belongs to, or append it at the end — never insert it at an arbitrary position. |
| 66 | + |
| 67 | +### ⚠️ Ask First |
| 68 | + |
| 69 | +- **Any breaking change — always ask first.** Never make one on your own initiative: a renamed/removed public method, a changed signature or default, a tightened model field, a new required constructor argument, or a raised Python floor. |
| 70 | +- Renaming or removing an existing `*ErrorCode` constant in `error/`, or changing the `code` an existing typed error raises. Integrators branch on `code`, so this is a breaking change even though no signature moves — treat an existing code as public API. Adding a new code is not breaking. |
| 71 | +- Adding, removing, or bumping a dependency (`pyproject.toml` + `poetry.lock` + `requirements.txt` must move together — Snyk SCA installs from `requirements.txt`). |
| 72 | +- Changing security-relevant code: token handling, `encryption/encrypt.py`, DPoP proof construction, PKCE, `state`/`nonce` handling, issuer or org-claim validation, session-expiry enforcement. |
| 73 | +- Modifying the `StateStore` / `TransactionStore` contract in `store/abstract.py`. |
| 74 | +- Changing CI/CD (`.github/workflows/`), `.ruff.toml` rule selection, or `.snyk` suppressions. |
| 75 | +- Changing the session or transaction storage format, or the store identifier defaults (`_a0_session`, `_a0_tx`). |
| 76 | +- Deprecating a public name — use the PEP 562 `__getattr__` alias pattern in `auth_types/`; don't delete it. |
| 77 | + |
| 78 | +### 🚫 Never Do |
| 79 | + |
| 80 | +- Commit secrets, API keys, tokens, or a real Auth0 tenant domain. Test fixtures use `<client_id>`-style placeholders and `auth0.local`. |
| 81 | +- Log, `print`, or include in an error message any token, `code`, `code_verifier`, `client_secret`, DPoP private key, or full response body. There is no logger in the SDK — don't introduce one that emits these. |
| 82 | +- Fail open. A validation, resolver, JWKS, or token-endpoint failure must raise, never fall through to a permissive default (no default domain, no "assume valid"). |
| 83 | +- Validate `state` by comparing strings. `complete_interactive_login` looks the transaction up by `{transaction_identifier}:{state}` and raises `MissingTransactionError` on a miss — that store lookup *is* the binding. If you ever add a secret-to-secret comparison, use `hmac.compare_digest`, never `==`. |
| 84 | +- Run `ruff format .` repo-wide — it is not a CI gate and the tree is not format-clean, so a repo-wide run buries your change in unrelated reformatting. Format only lines you touched; `poetry run ruff format --check .` shows the current state. |
| 85 | +- Remove or `skip` a failing test instead of fixing it, or weaken an assertion to make it pass. |
| 86 | +- Hand-edit `poetry.lock`, `coverage.xml`, `CHANGELOG.md` (release flow owns it), or anything in `dist/`, `.venv/`, `__pycache__/`, `.pytest_cache/`, `.ruff_cache/`. |
| 87 | +- Break backward compatibility without asking first (see Ask First) and getting explicit approval. |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +## Security Considerations |
| 92 | + |
| 93 | +This SDK is a **confidential client**: it holds a real client secret server-side and the browser never sees a token. |
| 94 | + |
| 95 | +- **Tokens are server-side only.** Access, refresh, and ID tokens live in the integrator's `StateStore`; the browser holds only an opaque session reference. Never add a public accessor that hands a raw refresh token to the caller's browser layer. |
| 96 | +- **Store payloads are encrypted.** `encryption/encrypt.py` derives a key with HKDF-SHA256 from the integrator's `secret` salted with the record identifier, then produces a JWE (`alg: dir`, `enc: A256CBC-HS512`). `secret` is required at construction — `ServerClient` raises `MissingRequiredArgumentError` without it, and must keep failing closed rather than storing plaintext. |
| 97 | +- **PKCE is always on** for the authorization-code flow (`utils/helpers.py` `PKCE`, `secrets`-backed), even though this is a confidential client. `state` and `nonce` are generated per transaction and validated at callback. |
| 98 | +- **DPoP (RFC 9449)** is supported for passkey sign-in and the My Account authentication-methods/factors calls. Keys must be EC P-256/ES256 (`_validate_dpop_key` rejects anything else); resource proofs carry `ath`, token-endpoint proofs deliberately do not; a `401` + `DPoP-Nonce` triggers exactly one retry. |
| 99 | +- **Token and claim validation is mandatory.** ID-token issuer validation (`IssuerValidationError`), organization `org_id`/`org_name` claim validation (exact for `org_`-prefixed IDs, NFC-normalized case-insensitive for names), and the upstream-IdP `session_expiry` ceiling (30s skew leeway) all fail closed. Don't add a bypass flag. |
| 100 | +- **Backchannel logout** matches on `sid` **or** `sub` per the OIDC spec, and compares the token `iss` against the session's stored domain before deleting — this is what prevents cross-domain session deletion in MCD deployments. |
| 101 | +- **Secret handling.** `AUTH0_SECRET` and the client secret come from the integrator's environment/secrets manager; the SDK never reads them from disk and must never write them anywhere. `S105`/`S106` are ignored in `.ruff.toml` for kwarg names — that suppression is not permission to hardcode a value. |
| 102 | +- **Scanning:** CodeQL (`codeql.yml`) and Snyk SCA (`sca_scan.yml`) run on every PR; ruff's bandit rules (`S`) run in `ruff check .`. Report vulnerabilities via Auth0's Responsible Disclosure Program, never a public issue. |
| 103 | + |
| 104 | +--- |
| 105 | + |
| 106 | +> The sections below are **reference** — each keeps a one-line anchor inline and offloads its body to `references/*.md`. |
| 107 | +
|
| 108 | +## Commands |
| 109 | + |
| 110 | +```bash |
| 111 | +poetry install # setup |
| 112 | +poetry run pytest # full suite (coverage flags come from pyproject.toml) |
| 113 | +poetry run ruff check . # the lint gate in CI |
| 114 | +poetry build # sdist + wheel |
| 115 | +``` |
| 116 | + |
| 117 | +See [references/commands.md](references/commands.md) for the exact CI invocations, single-file/single-test runs, format and clean commands, and what deliberately doesn't exist here (no typecheck, no live-test tier). Read it when you need to run, build, or test something. |
| 118 | + |
| 119 | +## Testing |
| 120 | + |
| 121 | +pytest + pytest-asyncio in `src/auth0_server_python/tests/` (one `test_<module>.py` per module); the default `poetry run pytest` suite is mock-based and needs no credentials. Async tests require an explicit `@pytest.mark.asyncio`. |
| 122 | + |
| 123 | +See [references/testing.md](references/testing.md) for naming, class-grouping conventions, the factory-helper pattern, mocking approach, and coverage setup. Read it before writing or restructuring tests. |
| 124 | + |
| 125 | +## Code Style |
| 126 | + |
| 127 | +Config is `.ruff.toml` at the repo root. CI-enforced: ruff's `E,W,F,I,B,C4,UP,S,PLC0415` rule sets — so **isort-ordered imports** (stdlib → third-party → local), **no imports inside functions** (`PLC0415`), and **bandit security rules** are hard gates. `E501` is ignored, so the `line-length = 100` is not enforced. `snake_case` methods, `PascalCase` classes, `_`-prefixed internals. |
| 128 | + |
| 129 | +See [references/code-style.md](references/code-style.md) for the good/bad method examples and the dominant patterns (generic store client, `httpx.Auth` strategies, Pydantic wire contracts, PEP 562 deprecation aliases). Read it before adding a public method or a new module. |
| 130 | + |
| 131 | +## Git Workflow |
| 132 | + |
| 133 | +Conventional Commits preferred (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`) — nothing enforces it, history is mixed. `.github/PULL_REQUEST_TEMPLATE.md` requires a method-level inventory of what changed. |
| 134 | + |
| 135 | +See [references/git-workflow.md](references/git-workflow.md) for branch naming, the full PR template requirements, and required checks. Read it when opening a PR or naming a branch. |
| 136 | + |
| 137 | +## Common Pitfalls |
| 138 | + |
| 139 | +The three that bite most often: the **Python 3.9 floor** (no `X | None`), **`ruff format` is not a CI gate** so don't reformat the tree, and a **missing `@pytest.mark.asyncio`** skips an async test with a warning instead of running it. |
| 140 | + |
| 141 | +See [references/pitfalls.md](references/pitfalls.md) for all eight, including the MCD domain-resolver trap, the `store_options` threading requirement, and the `Literal`-vs-`str` typing rule. Read it when a change touches HTTP, domains, or type annotations. |
| 142 | + |
| 143 | +## Docs Update Rules |
| 144 | + |
| 145 | +> A PR that adds or changes public API, configuration, or integration patterns is **not complete** until the docs move with it. |
| 146 | +
|
| 147 | +Note: there is **no `EXAMPLES.md`** in this repo — per-feature guides live in `examples/*.md` instead. `CHANGELOG.md` is owned by the release flow, not by feature work. |
| 148 | + |
| 149 | +See [references/docs-update.md](references/docs-update.md) for the tracked-docs inventory, the code-to-docs mapping table, and the feature → guide map. Read it whenever you change a public method, signature, config option, or error type. |
0 commit comments