From 7c38dc12f6334d7c3957796dbfcd0382a4391a2b Mon Sep 17 00:00:00 2001 From: Sourav Basu Date: Mon, 12 Oct 2026 20:45:14 +0530 Subject: [PATCH] docs: Added CLAUDE.md with AGENTS.md and references --- .gitignore | 1 + AGENTS.md | 3 + CLAUDE.md | 129 +++++++++++++++++++++++++++++++++++++ references/code-style.md | 73 +++++++++++++++++++++ references/commands.md | 41 ++++++++++++ references/docs-update.md | 25 +++++++ references/flow-map.md | 20 ++++++ references/git-workflow.md | 37 +++++++++++ references/pitfalls.md | 9 +++ references/testing.md | 33 ++++++++++ 10 files changed, 371 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 references/code-style.md create mode 100644 references/commands.md create mode 100644 references/docs-update.md create mode 100644 references/flow-map.md create mode 100644 references/git-workflow.md create mode 100644 references/pitfalls.md create mode 100644 references/testing.md diff --git a/.gitignore b/.gitignore index 59de4e0..42f6d7c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__/ #Environments .env .venv +.venv-* env/ #Session Cache diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e7c9c08 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +# AI Agent Guidelines for auth0-api-python + +@./CLAUDE.md for all coding guidelines, commands, project structure, code style, testing conventions, and boundaries. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2179d79 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,129 @@ +# AI Agent Guidelines for auth0-api-python + +## Your Role + +You are a Python SDK engineer on auth0-api-python, the async library APIs use to verify Auth0 access tokens (Bearer and DPoP) and perform server-side token exchange. + +--- + +## Working Principles + +- **Think before coding.** State assumptions. When a request is ambiguous, surface the interpretations and ask before building. +- **Simplicity first.** Write the minimum code that solves the stated problem. No speculative options, single-use abstractions, or error handling for impossible states. +- **Surgical changes.** Every changed line traces to the request. Clean up imports your change orphaned, leave pre-existing dead code alone. +- **Verify before done.** Turn the request into a checkable criterion (e.g. "reject X" becomes a failing test for X, then a fix) and run it before claiming success. + +--- + +## Project Structure + +``` +src/auth0_api_python/ + __init__.py # Public API: everything in __all__ is the supported surface + api_client.py # ApiClient: verify_request, verify_access_token, verify_dpop_proof, + # get_access_token_for_connection, get_token_by_exchange_profile, + # get_token_on_behalf_of; private helpers (_discover, _fetch_jwks, ...) at the end, + # except _resolve_allowed_domains, which sits above verify_request (leave it there) + config.py # ApiClientOptions (all constructor options and defaults) + errors.py # BaseAuthError hierarchy (status code, OAuth error code, WWW-Authenticate headers) + cache.py # CacheAdapter ABC + InMemoryCache (LRU + TTL) for discovery/JWKS + utils.py # normalize_domain, OIDC/JWKS fetch, unverified header/payload decode, htu normalization + act.py # get_current_actor / get_delegation_chain (RFC 8693 act claim) + types.py # TypedDicts: DomainsResolverContext, OnBehalfOfTokenResult, ActClaim + token_utils.py # Test helpers (token/DPoP proof generators) with a synthetic test key. Not in __all__, but packaged and importable +tests/ + conftest.py # Shared fixtures (api_client_confidential, mock_discovery) and httpx mock helpers + test_api_client.py, test_cache.py, test_utils.py, test_act.py +docs/ # Caching.md, MultipleCustomDomain.md (hand-written guides, linked from README) +README.md, EXAMPLES.md +pyproject.toml # Poetry manifest; source of truth for dependencies +requirements.txt # Hand-maintained runtime + dev dep list for the Snyk SCA workflow (pins can differ from pyproject.toml) +.version, .shiprc # Release version source (.shiprc keeps .version and pyproject.toml in sync) +.ruff.toml # Lint config (CI-enforced) +``` + +Before changing a flow (Bearer/DPoP verification, token exchange, MCD, caching), read its row in [references/flow-map.md](references/flow-map.md) for entry points, supporting helpers, tests, and docs. + +--- + +## Boundaries + +### Always Do + +- Run `poetry run pytest` and `poetry run ruff check .` before committing (both run in CI on Python 3.9 to 3.12). +- Make surgical changes: touch only what the request requires. Do not refactor or reformat adjacent code. +- Follow existing style and naming. Add new public `ApiClient` methods after the existing public methods, above the `# ===== Private Methods =====` banner in `api_client.py`. Add new private helpers below it. Never reorder existing declarations. +- Add unit tests for new functionality, marked `@pytest.mark.asyncio` when async, mocking HTTP with `pytest-httpx` (no real network calls). +- Raise typed errors from `errors.py` (subclasses of `BaseAuthError` with `get_status_code()` / `get_error_code()`). Wrap third-party failures in a typed error with `raise ... from e`. Inside a catch-all, re-raise this SDK's own errors first (`except VerifyAccessTokenError: raise`, as `verify_access_token` does) so they are not double-wrapped. Never return `None` or a bare `dict` to signal failure, and never leak raw `httpx`/`authlib` errors from public methods. +- Route discovery and JWKS fetches through `fetch_oidc_metadata` / `fetch_jwks` so `custom_fetch` and caching apply. Token endpoint calls use `httpx.AsyncClient(timeout=httpx.Timeout(self.options.timeout))` and map `httpx.TimeoutException` to `ApiError` 504 and `httpx.HTTPError` to 502, as `get_access_token_for_connection` does. +- Export any new public symbol through `__init__.py` `__all__`. +- Update `README.md` and `EXAMPLES.md` (and `docs/*.md` when caching or MCD behaviour changes) in the same PR when changing the public API, `ApiClientOptions`, or supported integration patterns. +- When adding or bumping a runtime dependency in `pyproject.toml`, update `requirements.txt` to match. The SCA scan installs from it, not from Poetry. +- Keep code compatible with Python 3.9 (`target-version = "py39"`, CI matrix includes 3.9). + +### Ask First + +- Any breaking change. Never make one on your own initiative, stop and ask the maintainer before writing it. This includes a renamed or removed public method, a changed signature or default, a new required option, or a raised Python floor. +- Changing an existing error's `get_error_code()` string, `ApiError.code`, or `get_status_code()`. Integrators branch on these and they surface in `WWW-Authenticate`, so treat them as public API. Adding a new error code is not breaking. +- Adding or bumping dependencies (`pyproject.toml`, `poetry.lock`, `requirements.txt`). +- Modifying public API signatures or `ApiClientOptions` defaults (e.g. `dpop_enabled`, `dpop_required`, cache TTL/size, `timeout`). +- Changes to token, DPoP, issuer, or domain validation logic, the allowed algorithm lists, `RESERVED_PARAMS`, or `normalize_domain`. +- Changes to CI/CD configuration (`.github/workflows/`, `.github/actions/`), `.ruff.toml` rule selection, or `.snyk` suppressions. +- Changing the cache key format or what is stored in the cache (affects custom `CacheAdapter` implementations). + +### Never Do + +- Commit secrets, client secrets, real tokens, or private keys. Test fixtures use placeholder values (`cid`, `csecret`, `auth0.local`). +- Hand-edit `poetry.lock` or `CHANGELOG.md`, or edit `.version` / the `pyproject.toml` version (the release flow owns them via `.shiprc`). +- Remove or skip a failing test instead of fixing it, or weaken an assertion to make it pass. +- Fail open. A validation, resolver, discovery, JWKS, or token endpoint failure must raise a typed error, never fall through to a permissive default (no default domain, no "assume valid"). +- Compare secrets or token-derived values with `==` in new code. Use `hmac.compare_digest`. +- Run `ruff format .` repo-wide. It is not a CI gate and the tree is not format-clean (see Common Pitfalls). +- Modify `.venv*/`, `dist/`, `__pycache__/`, `.coverage`, `coverage.xml`. +- Import `token_utils` from library code, or put a real key in it. +- Log, print, or include access tokens, DPoP proofs, client secrets, exchanged tokens, or full token endpoint response bodies in error messages. The SDK has no logger. Do not introduce one that emits these. +- Break backward compatibility without asking first and getting explicit approval. + +--- + +## Security Considerations + +- **Algorithm pinning:** access tokens verify with `RS256` only (`self._jwt = JsonWebToken(["RS256"])`), DPoP proofs with `ES256` only. Never add `none`, `HS*`, or derive the algorithm from the token header. +- **Never fetch from a token-derived host:** in MCD mode (`domains`), the unverified `iss` is normalized and must pass `_resolve_allowed_domains` before `_discover(issuer=...)` runs. In single-domain mode, discovery always targets the configured `domain`, and the token `iss` is matched against the discovery issuer afterwards. Never pass a token-derived issuer or URL to a fetch without the allowlist check. That turns token input into SSRF. +- **Domain normalization:** `normalize_domain` rejects `http://`, credentials, paths, queries, and fragments. Route every domain or issuer comparison through it. +- **MCD resolver input is untrusted:** `request_url`, `request_headers`, and `unverified_iss` in `DomainsResolverContext` are attacker-influenced. Resolver failures raise `DomainsResolverError`, never fall back to accepting the token (see `docs/MultipleCustomDomain.md` "Security Requirements"). +- **Claims:** `iss`, `aud`, `exp`, `iat` are always validated. `required_claims` adds to them, never replaces them. +- **DPoP:** proofs must carry `iat`, `ath`, `htm`, `htu`, `jti`. `htu` is compared via `normalize_url_for_htu`, `ath` must match the access token hash, and `cnf.jkt` must match the proof key thumbprint. `dpop_required=True` rejects Bearer. `jti` is checked for presence and type only. There is no replay store, so replay is bounded only by the `dpop_iat_offset` / `dpop_iat_leeway` window. +- **Token exchange:** `RESERVED_PARAMS` blocks overriding OAuth parameters via `extra`, and `MAX_ARRAY_VALUES_PER_KEY` caps array values. Do not weaken either. +- **Errors:** `_prepare_error` builds `WWW-Authenticate` headers. Error messages reach HTTP responses, so keep them generic and free of token contents. +- **Anonymous callers:** tokens with `sub` starting `anon@` verify like any token. Authorization is the integrator's call, the SDK must not silently accept or reject them. +- **Dependency scanning:** Snyk (`sca_scan.yml`, from `requirements.txt`, on push to `main` only), CodeQL (`codeql.yml`), and ruff `S` (bandit) rules in CI. +- **Vulnerability reports:** go through Auth0's Responsible Disclosure Program (README "Vulnerability Reporting"), never a public issue or PR. + +--- + +> The sections below are reference. Each keeps a short anchor inline and offloads detail to `references/`. + +## Commands + +`poetry install` to set up, `poetry run pytest` to test, `poetry run ruff check .` to lint. See [references/commands.md](references/commands.md) for the full CI-matching command list. Read it when you need to build, test, or lint. + +## Testing + +pytest + pytest-asyncio + pytest-httpx, in `tests/`. The default `poetry run pytest` suite needs no credentials (HTTP is mocked). See [references/testing.md](references/testing.md) for fixtures, async test patterns, and coverage. Read it when writing or running tests. + +## Code Style + +CI-enforced via `.ruff.toml`: rule sets `E, W, F, I, B, C4, UP, S`, isort import order, py39 target, `S101`/`S105`/`S106` allowed only under `tests/`. New non-secret string constants in `src/` use `# noqa: S105` with a reason (existing sites predate this and omit it). See [references/code-style.md](references/code-style.md) for naming, error patterns, and examples. Read it before adding a class, method, or error type. + +## Git Workflow + +Branches `feat/`, `fix/`, `docs/`, `chore/`, `release/`. Commits use Conventional Commits prefixes. See [references/git-workflow.md](references/git-workflow.md) for PR template sections. Read it when committing or opening a PR. + +## Common Pitfalls + +The ones that bite most: the Python 3.9 floor (no `X | None`), `ruff format` is not a CI gate, a missing `@pytest.mark.asyncio`, and `custom_fetch` covering only discovery/JWKS. See [references/pitfalls.md](references/pitfalls.md). Read it when a command, test, or CI job behaves unexpectedly. + +## Docs Update Rules + +Public API, options, or integration pattern changes update `README.md`, `EXAMPLES.md`, and the matching `docs/` guide in the same PR. See [references/docs-update.md](references/docs-update.md) for the tracked-docs inventory and code-to-docs mapping. Read it before finishing any PR that touches `src/`. diff --git a/references/code-style.md b/references/code-style.md new file mode 100644 index 0000000..4815564 --- /dev/null +++ b/references/code-style.md @@ -0,0 +1,73 @@ +# Code Style + +## Naming + +- `snake_case` functions, methods, and options. `PascalCase` classes. `UPPER_SNAKE` module constants (`RESERVED_PARAMS`, `TOKEN_EXCHANGE_GRANT_TYPE`). +- Private helpers carry a leading underscore (`_discover`, `_fetch_jwks`, `_prepare_error`) and sit together after the public methods of `ApiClient`. The one pre-existing exception is `_resolve_allowed_domains`, above `verify_request`. Leave it there. +- Error classes end in `Error` and live in `errors.py`. +- Type hints use builtin generics (`dict[str, Any]`, `list[str]`) with `Optional` / `Union` from `typing`. ruff `UP` targets py39, so do not use `X | Y` syntax. + +## Errors + +Every error subclasses `BaseAuthError` and defines its HTTP mapping: + +```python +class DomainsResolverError(BaseAuthError): + """Error raised when domains resolver function fails.""" + + def get_status_code(self) -> int: + return 500 + + def get_error_code(self) -> str: + return "domains_resolver_error" +``` + +Wrap third-party failures in a typed error and chain the cause: + +```python +# Good (api_client.py, _resolve_allowed_domains) +try: + result = self._allowed_domains(context) + if asyncio.iscoroutine(result) or asyncio.isfuture(result): + result = await result +except Exception as e: + raise DomainsResolverError( + f"Domains resolver function failed: {str(e)}" + ) from e +``` + +Interpolating `str(e)` is acceptable here because the resolver is integrator code. Do not interpolate exception text that can carry token contents or a response body. + +```python +# Bad +def get_token(self, options): # sync, untyped + client = httpx.Client() # no options.timeout + resp = client.post(url, data=options) # raw extras, no RESERVED_PARAMS check + if resp.status_code != 200: + print(resp.text) # may print token material + return None # fails open, untyped + return resp.json() # unvalidated dict escapes +``` + +Each line is a separate violation: sync in an async SDK, no timeout, unfiltered OAuth parameters, a print of a possibly token-bearing body, a `None` a caller can mistake for success, and an unvalidated response in the public API. + +Public errors meant for integrators are exported from `__init__.py` (`ApiError`, `ConfigurationError`, `DomainsResolverError`, `GetTokenByExchangeProfileError`). Others are imported from `auth0_api_python.errors`. + +## Patterns + +- **Options object:** all configuration goes through `ApiClientOptions` and is validated in `ApiClient.__init__`, raising `ConfigurationError` or `MissingRequiredArgumentError` at construction rather than at first use. +- **Async I/O:** network calls use `httpx.AsyncClient`, or the integrator's `custom_fetch` when provided. Public verification and token methods are `async def`. +- **Pluggable cache:** `CacheAdapter` ABC with `get` / `set` / `delete` / `clear`. `InMemoryCache` is the default. Entry TTL is `min(Cache-Control max-age, cache_ttl_seconds)` when the discovery/JWKS response sends `max-age`. +- **Docstrings:** Google style with `Args:` / `Returns:` / `Raises:` sections on public methods. +- **Placement:** `api_client.py` has one banner, `# ===== Private Methods =====`. New public methods go above it after the existing ones, new helpers below it. In files without banners, put new code next to related code or at the end, never at an arbitrary position. + +## Comments and docstrings + +Enforced on every change (code, tests, docs, commit messages): + +1. **Comments earn their place.** Only a non-obvious why: a protocol citation (`# RFC 9449`), a security decision, an intentional omission. Never restate the line below. Default to one sharp line or none. +2. **Docstring format.** One-line description, then `Args`, then `Returns` / `Raises`. No design rationale, that belongs in the PR description. +3. **Plain hyphens only.** No em or en dashes in comments, docstrings, commit messages, or Markdown. +4. **No semicolon clause-splicing.** Split independent clauses into separate sentences. +5. **No internal references.** No ticket numbers, planning-doc paths, internal links, or cross-SDK comparisons ("same as the Node SDK"). +6. **Helpers stay grouped.** Never interleave a private helper between public methods. diff --git a/references/commands.md b/references/commands.md new file mode 100644 index 0000000..f7c811a --- /dev/null +++ b/references/commands.md @@ -0,0 +1,41 @@ +# Commands + +These match `.github/workflows/test.yml` (Poetry 2.2.1, Python 3.9 to 3.12) and `CONTRIBUTING.md`. + +```bash +# Install dependencies and the package (CI runs both steps) +poetry install --no-interaction --no-root +poetry install --no-interaction + +# Run all unit tests (CI command) +poetry run pytest -v --cov=src --cov-report=term-missing --cov-report=xml + +# Run all unit tests (short form; pyproject addopts already adds --cov=src) +poetry run pytest + +# Fast loop without coverage +poetry run pytest --no-cov -q + +# Lint (CI-enforced) +poetry run ruff check . + +# Auto-fix lint issues (review the diff, keep it scoped to your change) +poetry run ruff check . --fix + +# Show formatting state only (not a CI gate, never apply repo-wide) +poetry run ruff format --check . + +# Build sdist and wheel into dist/ +poetry build + +# Clean generated artifacts +rm -rf dist/ .coverage coverage.xml .pytest_cache .ruff_cache +find . -path ./.venv\* -prune -o -name __pycache__ -type d -prune -exec rm -rf {} + +``` + +## Not available here + +- **No typecheck command.** mypy/pyright are not in `pyproject.toml` and no CI step runs them. +- **No integration/e2e tier.** The detected suite runs on `pytest-httpx` mocks, and nothing in `src/`, `tests/`, or CI reads Auth0 tenant credentials. + +`publish.yml` (manual `workflow_dispatch`) builds, runs the RL scanner, and publishes to PyPI. Agents do not run releases. diff --git a/references/docs-update.md b/references/docs-update.md new file mode 100644 index 0000000..a01c6e9 --- /dev/null +++ b/references/docs-update.md @@ -0,0 +1,25 @@ +# Docs Update Rules + +A PR that changes public API, configuration, or integration patterns is not complete until the matching docs are updated in the same PR. + +## Tracked docs + +| Doc | Covers | Exists | +|-----|--------|--------| +| `README.md` | Install, `ApiClient` setup, verification, connection tokens, custom token exchange, OBO, DPoP modes and options, MCD summary, anonymous callers | present | +| `EXAMPLES.md` | OBO exchange, delegation inspection, Bearer and DPoP verification, DPoP modes, anonymous caller patterns | present | +| `docs/Caching.md` | Cache defaults, TTL/max entries, custom `CacheAdapter`, tuning | present | +| `docs/MultipleCustomDomain.md` | `domains` list and resolver modes, hybrid mode, resolver security requirements, MCD with DPoP, errors, migration | present | + +## When you change code, update these docs + +| When this changes | Update | +|-------------------|--------| +| A public `ApiClient` method added, removed, renamed, or its arguments change | `README.md` (relevant numbered section), `EXAMPLES.md` (affected samples) | +| `ApiClientOptions` parameter added, removed, or default changed | `README.md` (setup and the DPoP "Configuration Options" example if DPoP-related), plus `docs/Caching.md` for `cache_*` or `docs/MultipleCustomDomain.md` for `domain` / `domains` | +| `CacheAdapter` interface or `InMemoryCache` behaviour | `docs/Caching.md` ("CacheAdapter API", "Default Behavior") | +| Domain resolver context, MCD validation, or resolver errors | `docs/MultipleCustomDomain.md`, `README.md` section 7 | +| Bearer/DPoP verification behaviour or error codes | `README.md` section 6, `EXAMPLES.md` (Bearer, DPoP sections) | +| Token exchange (connection, exchange profile, OBO) behaviour or errors | `README.md` sections 4 and 5, `EXAMPLES.md` "On Behalf Of Token Exchange" | +| New exported symbol in `__init__.py` | `EXAMPLES.md` (add a usage sample) | +| Supported Python version or install method | `README.md` "Install the SDK" | diff --git a/references/flow-map.md b/references/flow-map.md new file mode 100644 index 0000000..f857eed --- /dev/null +++ b/references/flow-map.md @@ -0,0 +1,20 @@ +# Flow Map + +Before changing a flow, read its entry points and supporting helpers. Public methods are in `ApiClient` (`api_client.py`) above the `# ===== Private Methods =====` banner, and helpers are below it, except `_resolve_allowed_domains`, which sits above `verify_request`. + +| Flow | Entry points | Also read | Docs | +|------|--------------|-----------|------| +| Request verification (Bearer + DPoP) | `verify_request` | `verify_access_token`, `verify_dpop_proof`, `_prepare_error`, `_build_www_authenticate`, `errors.py` | `README.md` sections 3 and 6, `EXAMPLES.md` "Bearer Authentication", "DPoP Authentication" | +| Access token verification | `verify_access_token` | `_resolve_allowed_domains`, `_discover`, `_fetch_jwks`, `_validate_claims_presence`, `utils.normalize_domain`, `get_unverified_header` / `get_unverified_payload` | `README.md` section 3 | +| DPoP proof verification | `verify_dpop_proof` | `utils.normalize_url_for_htu`, `sha256_base64url`, `calculate_jwk_thumbprint`, options `dpop_enabled` / `dpop_required` / `dpop_iat_leeway` / `dpop_iat_offset` | `README.md` section 6, `EXAMPLES.md` "Direct DPoP Proof Verification" | +| Connection access token (Token Vault) | `get_access_token_for_connection` | `GetAccessTokenForConnectionError`, `ApiError`, `_discover` (token endpoint) | `README.md` section 4 | +| Custom token exchange | `get_token_by_exchange_profile` | `_apply_extra`, `RESERVED_PARAMS`, `MAX_ARRAY_VALUES_PER_KEY`, `GetTokenByExchangeProfileError` | `README.md` section 5 | +| On Behalf Of exchange | `get_token_on_behalf_of` | delegates to `get_token_by_exchange_profile`, returns `types.OnBehalfOfTokenResult` | `README.md` "On Behalf Of Token Exchange", `EXAMPLES.md` "On Behalf Of Token Exchange" | +| Delegation inspection | `get_current_actor`, `get_delegation_chain` (`act.py`) | `types.ActClaim` | `README.md` / `EXAMPLES.md` "Inspecting Delegation After Token Verification" | +| Multiple Custom Domains | `ApiClientOptions(domains=...)` | `_resolve_allowed_domains`, `utils.normalize_domain`, `types.DomainsResolverContext`, `DomainsResolverError` | `docs/MultipleCustomDomain.md`, `README.md` section 7 | +| Discovery / JWKS caching | `CacheAdapter`, `InMemoryCache` (`cache.py`) | `_discover`, `_fetch_jwks`, `utils.parse_cache_control_max_age` | `docs/Caching.md` | +| Anonymous callers | no dedicated code, verified like any token | `act.py` is unrelated, `sub` starts with `anon@` | `README.md` section 8, `EXAMPLES.md` "Anonymous Callers" | + +Tests: `tests/test_api_client.py` covers every `ApiClient` flow, `tests/test_act.py` the delegation helpers, `tests/test_cache.py` the cache, `tests/test_utils.py` the utils. + +Two rules cut across every flow: never fetch from a token-derived host unless it passed the allowlist (single-domain mode only fetches the configured `domain`), and fail closed with a typed error on every failure path. diff --git a/references/git-workflow.md b/references/git-workflow.md new file mode 100644 index 0000000..31ffa74 --- /dev/null +++ b/references/git-workflow.md @@ -0,0 +1,37 @@ +# Git Workflow + +## Branch naming + +From merged PR history: `feat/`, `fix/`, `docs/`, `chore/`, `release/` (e.g. `release/1.0.0b10`). Dependabot opens `dependabot/...` branches. Base branch is `main`. + +## Commit messages + +Conventional Commits prefixes (`feat:`, `fix:`, `docs:`, `chore:`, `test:`, `refactor:`), lowercase subject, no trailing period. Example: `docs: add 403 guidance for anonymous caller blocking in EXAMPLES.md`. No commit linter is configured. Older commits do not all follow this, so match the prefix style for new work. + +## Pull requests + +`.github/PULL_REQUEST_TEMPLATE.md` sections: + +- **Changes:** what and why, classes/methods added, changed, or deprecated, and a usage summary for public API changes +- **References:** supporting links (tickets, community posts) +- **Testing:** how to test, with checkboxes for unit coverage, integration coverage, and latest-platform testing +- **Checklist:** Auth0 contribution guidelines, Code of Conduct, all tests passing + +The template asks for a method-level inventory, so write the PR body from the diff of the public surface (`__init__.py` `__all__`, `ApiClient` public methods, `ApiClientOptions`, `errors.py`), not from a summary of intent. + +`CODEOWNERS` assigns every path to `@auth0/project-dx-sdks-engineer-codeowner`. + +## CI workflows + +From `.github/workflows/`. Which of these are required status checks is set in branch protection, not in the repo. + +- **Build and Test** (`test.yml`): `pytest` + `ruff check .` across the Python version matrix in that workflow, on PR, merge group, and push to `main` +- **CodeQL** (`codeql.yml`): on PR, merge group, push to `main`, and a weekly schedule +- **SCA** (`sca_scan.yml`): Snyk against `requirements.txt`, with suppressions in `.snyk`. Runs on push to `main` only, so it does not gate a PR +- **Claude Code Review** (`claude-code-review.yml`) + +`ruff format` is not a check. Reformatting untouched files creates noise CI won't flag but reviewers will. + +## Releases + +`.version` is the version source. `.shiprc` keeps `pyproject.toml` in step with it. Releases are cut by `publish.yml` (manual dispatch, gated on the RL scan), not by an agent editing version files. `CHANGELOG.md` is written as part of that flow. diff --git a/references/pitfalls.md b/references/pitfalls.md new file mode 100644 index 0000000..8ac3730 --- /dev/null +++ b/references/pitfalls.md @@ -0,0 +1,9 @@ +# Common Pitfalls + +1. **Do not run `ruff format .`.** CI runs only `ruff check .`. The codebase is not ruff-formatted (`ruff format --check .` reports 13 files that would be reformatted), so a repo-wide format produces a large unrelated diff. Match surrounding formatting by hand. +2. **Python 3.9 compatibility.** CI tests 3.9 and `.ruff.toml` targets `py39`. Avoid `X | Y` type unions at runtime, `match` statements, and other 3.10+ syntax. `ada-url` has separate version constraints above and below 3.10 in `pyproject.toml`. +3. **Async tests need `@pytest.mark.asyncio`.** No `asyncio_mode` is set in `pyproject.toml`, so pytest-asyncio runs in strict mode and ignores an unmarked async test. Depending on the pytest version it then fails with an "async def functions are not natively supported" message or is skipped with a warning. If a new async test fails that way or shows as skipped, check the marker first. +4. **Two dependency lists.** `pyproject.toml` + `poetry.lock` drive installs. `requirements.txt` is a hand-maintained copy that only the Snyk SCA workflow reads. Changing one without the other leaves the scan out of date. +5. **`custom_fetch` covers only discovery and JWKS.** `fetch_oidc_metadata` and `fetch_jwks` (`utils.py`) use the integrator's `custom_fetch` when set. The token endpoint calls in `get_access_token_for_connection` and `get_token_by_exchange_profile` always build their own `httpx.AsyncClient` with `options.timeout`. Do not assume a new HTTP path inherits either behaviour, pick the matching pattern deliberately. +6. **`.ruff.toml` uses deprecated top-level keys.** ruff warns that `select` / `ignore` / `per-file-ignores` belong under `[lint]`. The warning is harmless. Migrating the file is a separate change, do not fold it into unrelated work. +7. **Stale local venvs.** A hand-made venv in the working tree (anything other than Poetry's) may not have the package or dev dependencies installed. Use `poetry install` then `poetry run ...` so you test the same environment CI does. diff --git a/references/testing.md b/references/testing.md new file mode 100644 index 0000000..8d634ca --- /dev/null +++ b/references/testing.md @@ -0,0 +1,33 @@ +# Testing + +- **Framework:** pytest, with pytest-asyncio, pytest-httpx, pytest-mock, freezegun (versions in `pyproject.toml` dev group) +- **Location:** `tests/`, one `test_.py` per source module +- **Coverage:** pytest-cov. `pyproject.toml` `addopts` always applies `--cov=src --cov-report=term-missing:skip-covered --cov-report=xml`. No minimum threshold is configured. + +The default `poetry run pytest` suite needs no credentials. All Auth0 HTTP calls go through `httpx_mock` against `https://auth0.local`. + +## Running tests + +```bash +poetry run pytest # all unit tests +poetry run pytest tests/test_cache.py # one file +poetry run pytest tests/test_api_client.py -k dpop # filter by name +``` + +## Conventions + +- Module-level `test__` functions (e.g. `test_verify_access_token_fail_no_iss`), often with a one-line docstring, grouped under `# ===== Section =====` comment banners. No test classes. +- Async tests are marked explicitly with `@pytest.mark.asyncio`. No `asyncio_mode` is configured, so an unmarked `async def test_...` will not run as intended. +- Assert error types with `pytest.raises(VerifyAccessTokenError)` and similar. Where both exist, assert on `get_error_code()` / `.code` and `get_status_code()` rather than message text. Codes are the contract, messages are not. +- Test placeholders stay obviously synthetic (`cid`, `csecret`, `auth0.local`). Never paste a real tenant value. +- Time-dependent tests use `@freeze_time(...)` from freezegun. Cache TTL tests in `test_cache.py` use short real sleeps. + +## Mocking and test utilities + +- `tests/conftest.py` provides: + - `api_client_confidential`: an `ApiClient` with `domain="auth0.local"` and placeholder `client_id` / `client_secret` + - `mock_discovery`: registers the discovery response + - `mock_token_response(...)`, `token_success(...)`, `last_form(...)`, `last_auth_header(...)`: token endpoint mocking and request inspection + - `assert_api_error(...)`, `assert_http_basic_auth(...)`, `assert_form_post(...)`, `assert_no_requests(...)`: shared assertions. Use `assert_api_error` rather than hand-rolling `.code` / status checks +- `src/auth0_api_python/token_utils.py` provides `generate_token`, `generate_dpop_proof`, `generate_token_with_cnf`, signed with a synthetic test key. Use these instead of hand-building JWTs. +- Use the constants `DISCOVERY_URL`, `JWKS_URL`, `TOKEN_ENDPOINT` from `conftest.py` rather than repeating URLs.