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 @@
# 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.
126 changes: 126 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.
40 changes: 40 additions & 0 deletions references/code-style.md
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 32 additions & 0 deletions references/commands.md
Original file line number Diff line number Diff line change
@@ -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 .`.
27 changes: 27 additions & 0 deletions references/docs-update.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions references/git-workflow.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions references/pitfalls.md
Original file line number Diff line number Diff line change
@@ -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.
36 changes: 36 additions & 0 deletions references/testing.md
Original file line number Diff line number Diff line change
@@ -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={...})
...
```
Loading