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
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# AI Agent Guidelines for auth0-fastapi

@./CLAUDE.md for all coding guidelines, commands, project structure, code style, testing conventions, and boundaries.
108 changes: 108 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# AI Agent Guidelines for auth0-fastapi

## Your Role

You are a Python SDK engineer maintaining auth0-fastapi, the FastAPI adapter over auth0-server-python for server-side web apps. You write thin wrappers plus the routes, cookie stores, and tests around them.

---

## Project Structure

```
auth0-fastapi/
├── src/auth0_fastapi/ # The package (no top-level __init__.py, import from submodules)
│ ├── config.py # Auth0Config pydantic model (integrator settings)
│ ├── auth/auth_client.py # AuthClient: public API, each method delegates to ServerClient
│ ├── server/routes.py # router + register_auth_routes (/auth/login, /callback, /logout, ...)
│ ├── errors/__init__.py # Re-exported auth0-server-python errors, ConfigurationError, exception handler
│ ├── stores/ # CookieTransactionStore, StatelessStateStore, StatefulStateStore
│ ├── util/__init__.py # URL helpers incl. to_safe_redirect and build_request_base_url
│ └── test/ # pytest suite (tests live inside the package)
├── examples/ # Feature guides: Passkeys, CustomTokenExchange, MultipleCustomDomains, Sessions
├── references/ # Agent reference docs offloaded from this file
├── .github/workflows/ # tests.yml (lint + tests), sca_scan.yml (Snyk), codeql.yml, publish.yml
├── pyproject.toml # Poetry manifest, pytest addopts (coverage)
├── requirements.txt # Pip copy of runtime and test deps, installed by the SCA workflow
├── .ruff.toml # Lint config (line length 120, py39 target, bandit rules)
├── .version # Release version read by publish.yml (release commits also bump pyproject.toml)
└── CHANGELOG.md # Release notes, written during the release flow
```

---

## Boundaries

### Always Do

- Run `poetry run ruff check .` and `poetry run pytest` before committing. CI runs both on Python 3.9 to 3.12.
- Make surgical changes. Touch only what the request requires, and don't refactor or reformat adjacent code that isn't broken.
- Keep `AuthClient` methods as thin pass-throughs to `self.client` (`ServerClient`). Protocol, token, and crypto logic belongs in auth0-server-python.
- Add a new feature's public methods at the end of `AuthClient`, and keep private helpers grouped together, never between public methods.
- Write docstrings as a one-line summary, then `Args`, `Returns`, `Raises`, matching the passkey and custom token exchange methods.
- Add unit tests in `src/auth0_fastapi/test/` for every change, and mark async tests with `@pytest.mark.asyncio`.
- Raise or re-export typed errors from `auth0_fastapi.errors`. Map new `Auth0Error` subclasses in `auth0_exception_handler` when they need a non-400 status.
- Pass every user-supplied redirect target (`returnTo` and similar) through `to_safe_redirect` before redirecting.
- Update `README.md` and the matching `examples/*.md` guide in the same PR when changing the public API, `Auth0Config` options, mounted routes, or supported integration patterns.
- When a runtime or test dependency changes, update both `pyproject.toml` and `requirements.txt` (the SCA workflow installs `requirements.txt` before the Snyk scan).
- Use Python 3.9 compatible syntax: `Optional[X]` and `Union[X, Y]`, not `X | Y`, in annotations that FastAPI or pydantic evaluate.

### Ask First

- Any breaking change. Never make one on your own initiative. Stop and ask the maintainer before writing it.
- Adding, removing, or bumping a dependency, including the `auth0-server-python` floor.
- Changing a public signature in `AuthClient`, `Auth0Config`, `register_auth_routes`, the stores, or `auth0_fastapi.errors`.
- Changing a mounted route path, its query or body contract, or its status codes.
- Changing cookie names, cookie attributes, chunking, or session expiry. Renaming a cookie makes existing sessions unreadable.
- Changing the status-code mapping in `auth0_exception_handler`.
- Changing CI workflows, `.github/actions/`, `.ruff.toml`, or `.snyk`.
- Running `ruff format` on any file. CI doesn't enforce it, and it can rewrite lines the task didn't touch.

### Never Do

- Commit secrets, API keys, tokens, or a real `.env`. Test fixtures use synthetic values only.
- Log or echo tokens, session data, `client_secret`, or `secret`, including in error messages and exception `detail`.
- Weaken cookie flags (`secure`, `httponly`, `samesite`) or the issuer check in `StatefulStateStore.delete_by_logout_token`.
- Redirect to a URL built from request input without `to_safe_redirect`.
- Hand-edit `poetry.lock`, or bump the version in `.version` or `pyproject.toml` outside the release flow.
- Modify `.venv/`, `build/`, `dist/`, `htmlcov/`, `.pytest_cache/`, or `__pycache__/`.
- Remove or skip failing tests without fixing them.

---

## Security Considerations

- **App type:** confidential client (server-side web app). `client_secret` and `secret` stay server-side and come from the environment, never source.
- **Delegated protocol logic:** authorize, token exchange, logout token, and ID token handling all run in `ServerClient` from auth0-server-python. The two cookie stores encrypt with that package's store `encrypt`/`decrypt`, keyed by `Auth0Config.secret`. Don't add crypto here.
- **Cookies:** the transaction cookie `_a0_tx` is `secure`, `httponly`, `samesite=Lax`, `max_age=60` (`stores/cookie_transaction_store.py`). The stateless session is chunked into `_a0_session_<n>` cookies that are `secure`, `httponly`, `samesite=Lax` (`stores/stateless_state_store.py`).
- **Stateful store:** `StatefulStateStore` sets only `httponly` on its session-ID cookie (`stores/stateful_state_store.py`).
- **Open redirect:** `to_safe_redirect` (`util/__init__.py`) only allows the same origin as `app_base_url` (or the request base URL under MCD). The callback and connect routes depend on it. `/auth/logout` passes `returnTo` straight to Auth0's logout URL without it.
- **Multiple Custom Domains:** `build_request_base_url` trusts `x-forwarded-host` and `x-forwarded-proto`. Domain resolvers must map hosts through an allowlist (see `examples/MultipleCustomDomains.md`, Security Best Practices).
- **Backchannel logout:** `StatefulStateStore.delete_by_logout_token` skips sessions whose stored domain doesn't match the token `iss` when both are present, which prevents cross-domain deletion under MCD.
- **Error responses:** `auth0_exception_handler` returns only `error` (code) and `message`. Some routes wrap failures in `HTTPException(detail=str(e))`. Don't copy that pattern into new code.
- **Lint:** ruff's bandit rules (`S`) run in CI. Of those, tests ignore only `S101`, `S105`, `S106`.

---

## Commands

Install with `poetry install`, lint with `poetry run ruff check .`, and test with `poetry run pytest`. See [references/commands.md](references/commands.md) for the full command list. Read it only when you need to build, lint, or test.

## Testing

The default `poetry run pytest` suite needs no credentials. HTTP is mocked with `unittest.mock` or `httpx.MockTransport`. See [references/testing.md](references/testing.md) for layout, fixtures, mocking patterns, and coverage. Read it when you write or debug tests.

## Code Style

`ruff check` enforces 120-character lines and isort import order. Naming isn't linted, but the code uses snake_case functions and fields and PascalCase classes. See [references/code-style.md](references/code-style.md) for examples and patterns. Read it when you add or change public API.

## Git Workflow

Conventional commit subjects (`feat:`, `fix:`, `docs:`, `test:`, `refactor:`, `build:`, `chore:`, `revert:`) on a typed branch such as `feat/<topic>`. See [references/git-workflow.md](references/git-workflow.md) for branch and PR conventions. Read it before you commit or open a PR.

## Common Pitfalls

Known gotchas include a broken local `.venv`, the ignored `cookie_name` option, and import paths. See [references/pitfalls.md](references/pitfalls.md). Read it when something fails unexpectedly or before you touch config, stores, or imports.

## Docs Update Rules

Docs ship in the same PR as the code they describe. See [references/docs-update.md](references/docs-update.md) for the tracked docs and the code-to-docs mapping. Read it before you finish any change to public API, config, or routes.
72 changes: 72 additions & 0 deletions references/code-style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Code Style

## Enforced by `ruff check` (`.ruff.toml`)

- Line length 120, target `py39`.
- Rule sets: `E`, `W`, `F`, `I` (isort), `C4`, `UP`, `S` (bandit), `DTZ`, `G`, `A`, `C`.
- Per-file ignores exist for `__init__.py` re-exports, `config.py` long lines, `routes.py` complexity, and tests. Don't add new suppressions without a reason.

## Naming

- snake_case for functions, methods, variables, and `Auth0Config` fields. camelCase appears only where an external contract needs it: pydantic aliases (`clientId`, `appBaseUrl`) and query parameters or `app_state` keys (`returnTo`, `connectionScope`).
- PascalCase for classes. Errors end in `Error`. `ConfigurationError`, the only error defined in this package, sets a `code` attribute.

## Wrapper pattern

Public `AuthClient` methods forward their arguments to `ServerClient` with minimal glue, such as building an options object (`StartInteractiveLoginOptions`, `LogoutOptions`). Methods that accept `store_options` take it last and document it.

Good (`auth/auth_client.py`):

```python
async def custom_token_exchange(
self,
options: CustomTokenExchangeOptions,
store_options: dict = None,
) -> TokenExchangeResponse:
"""
Performs an RFC 8693 token exchange for the given subject token.
Does not create or modify the current session.

Args:
options: Subject token details and exchange parameters
(subject_token, subject_token_type, audience, scope,
organization, authorization_params).
store_options: Optional options passed to the Transaction and State
Store. Only required when using Multiple Custom Domains, where
the domain resolver needs the incoming request.

Returns:
The raw TokenExchangeResponse (access_token, expires_in).

Raises:
CustomTokenExchangeError: If the exchange fails or the subject
token parameters are invalid (see CustomTokenExchangeErrorCode).
"""
return await self.client.custom_token_exchange(options, store_options=store_options)
```

Bad:

```python
async def custom_token_exchange(self, options, store_options: dict | None = None):
# Call the token endpoint directly
async with httpx.AsyncClient() as http:
resp = await http.post(f"https://{self.config.domain}/oauth/token", data=options)
logger.info("token exchange result %s", resp.json())
return resp.json()
```

The bad version reimplements protocol logic owned by auth0-server-python, logs tokens, uses `X | None` (breaks on Python 3.9 where evaluated), drops the return type, and has a narrating comment instead of a docstring.

## Patterns

- **Adapter:** `AuthClient` wraps `ServerClient`. The stores subclass `TransactionStore`/`StateStore` from `auth0_server_python.store.abstract` and read `request`/`response` from `store_options`.
- **Config-driven routes:** `register_auth_routes(router, config)` mounts route groups by `mount_routes`, `mount_connect_routes`, and `mount_connected_account_routes`, and raises `ConfigurationError` on invalid combinations.
- **Dependency injection:** routes get the client through `Depends(get_auth_client)`, which reads `request.app.state.auth_client`. Apps protect routes with `Depends(auth_client.require_session)`.
- **Error mapping:** `register_exception_handlers(app)` installs `auth0_exception_handler`, which maps `Auth0Error` subclasses to HTTP statuses with a `{"error", "message"}` body.

## Comments and docs

- No inline comments that narrate a self-explanatory line.
- Plain hyphens only in code, docstrings, and docs. No em or en dashes.
- Public docs and docstrings state only behavior that Auth0's public docs or the auth0-server-python public API confirm. Don't describe Auth0 backend internals.
40 changes: 40 additions & 0 deletions references/commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Commands

Run everything through Poetry. CI pins Poetry 2.2.1 and creates the virtualenv in-project (`.github/workflows/tests.yml`).

```bash
# Install runtime and dev dependencies
poetry install

# Lint (the exact CI step)
poetry run ruff check .

# Auto-fix lint findings in the files you changed
poetry run ruff check --fix <path>

# Run all unit tests (pyproject.toml addopts add coverage and an htmlcov/ report)
poetry run pytest

# Run tests the way CI does
poetry run pytest --cov=auth0_fastapi --cov-report=xml --cov-report=term-missing

# Run one test file, one class, or one test
poetry run pytest src/auth0_fastapi/test/test_routes.py
poetry run pytest src/auth0_fastapi/test/test_auth_client.py::TestPasskey
poetry run pytest -k "passkey"

# Build the sdist and wheel into dist/
poetry build
```

## Not available

- **Formatter:** CI does not run `ruff format`, and running it reformats files the task didn't touch. Format by hand to match the surrounding code.
- **Type checker:** no mypy or pyright configuration exists.
- **Clean target:** none. Generated output (`dist/`, `htmlcov/`, `.coverage`, `.pytest_cache/`) is not tracked in git.

## CI-only jobs

- `sca_scan.yml` installs `requirements.txt` into a venv and runs the Snyk scan from `auth0/devsecops-tooling`.
- `codeql.yml` runs CodeQL analysis.
- `publish.yml` is a manual `workflow_dispatch` release that reads the version from `.version`, runs the RL scanner, creates the GitHub release, and publishes to PyPI. Agents don't run it.
32 changes: 32 additions & 0 deletions references/docs-update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Docs Update Rules

A PR that adds or changes public API, configuration, routes, or integration patterns is not complete until the matching docs are updated in the same PR.

## Tracked docs

| Doc | Covers | Exists |
|-----|--------|--------|
| `README.md` | Features, installation, minimal and advanced setup, mounted routes, protecting routes, audience, MCD, session expiry, custom token exchange and passkey summaries | present |
| `EXAMPLES.md` | Not used. This repo keeps examples as per-feature guides under `examples/` | missing |
| `examples/Passkeys.md` | Passkey signup, login, identifier verification, security considerations, errors, MFA, organizations, DPoP | present |
| `examples/CustomTokenExchange.md` | Token exchange with and without a session, session transfer (STT), organization scoping, error codes | present |
| `examples/MultipleCustomDomains.md` | Domain resolvers, redirect URIs, proxy headers, session behavior, security best practices | present |
| `examples/Sessions.md` | Upstream IdP `session_expiry` handling | present |

Ask before creating `EXAMPLES.md`. New guides go in `examples/<Feature>.md` and get a link from `README.md`.

## When you change code, update these docs

| When this changes | Update |
|-------------------|--------|
| A public `AuthClient` method added, removed, renamed, or its arguments changed | `README.md` (Usage) and the feature guide in `examples/` that calls it |
| `Auth0Config` fields, defaults, or aliases | `README.md` (Setup, Routes, Usage snippets that construct `Auth0Config`) |
| Mounted routes or `mount_*` flags in `server/routes.py` | `README.md` (4. Routes) |
| Store classes or their constructor options | `README.md` (Customizing the Cookie Stores) |
| Login, callback, logout, or backchannel logout behavior | `README.md` (Features, Routes) |
| Passkey wrappers or `PasskeyError` handling | `examples/Passkeys.md` and the README Passkeys section |
| Custom token exchange or session transfer wrappers | `examples/CustomTokenExchange.md` |
| Callable `domain`, `build_request_base_url`, or MCD redirect logic | `examples/MultipleCustomDomains.md` and the README MCD section |
| Session expiry or session-read behavior | `examples/Sessions.md` |
| Errors re-exported from `auth0_fastapi.errors` or their HTTP status | The error handling section of the affected guide |
| Install requirements or minimum Python version | `README.md` (2. Installation) |
24 changes: 24 additions & 0 deletions references/git-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Git Workflow

## Branches

- Branch from `main`. Use a typed prefix that matches the change: `feat/<topic>`, `docs/<topic>`, `chore/<topic>`. Some branches use a Jira key instead (`SDK-8920-cte`).
- `release/<version>` branches (e.g. `release/1.0.0b12`) carry the release PR that bumps `.version`, `pyproject.toml`, and `CHANGELOG.md`. Don't create them as part of a feature change.
- Dependabot opens `dependabot/pip/*` and `dependabot/github_actions/*` branches daily. For pip it ignores semver-major bumps (`.github/dependabot.yml`).

## Commits

- Conventional commit subject: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`, `build:` (dependency floors), `chore:`, `revert:`. One line, lowercase after the prefix, no trailing period, and say what changed (e.g. `fix: relay the status Auth0 returned for passkey errors`).
- One logical change per commit. Docs, tests, and code for the same change can land as separate commits in one PR.
- The repo has no commitlint or pre-commit config. Reviewers enforce the format.

## Pull requests

Follow `.github/PULL_REQUEST_TEMPLATE.md`:

- **Changes:** what and why, including endpoints and classes or methods added, changed, or deprecated, plus a usage summary for public API changes.
- **References:** supporting links (ticket, community post).
- **Testing:** how reviewers test it, plus the unit coverage, integration coverage, and latest-platform checkboxes.
- **Checklist:** contribution guidelines, code of conduct, all tests pass.

CODEOWNERS assigns every path to `@auth0/project-dx-sdks-engineer-codeowner`. A Claude review workflow (`claude-code-review.yml`) responds to PR comments and reviews.
13 changes: 13 additions & 0 deletions references/pitfalls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Common Pitfalls

1. **`ModuleNotFoundError: auth0_server_python` during test collection.** If auth0-server-python was linked into `.venv` as an editable install from a local checkout that has since moved or been deleted, every test module fails to import. Run `poetry install` to restore the locked version, after confirming with the developer that they don't need the local link.

2. **No top-level package import.** `src/auth0_fastapi/` has no `__init__.py`, so `from auth0_fastapi import AuthClient` raises `ImportError`. Import from submodules: `auth0_fastapi.auth.auth_client`, `auth0_fastapi.config`, `auth0_fastapi.server.routes`, `auth0_fastapi.errors`, `auth0_fastapi.stores`.

3. **`Auth0Config.cookie_name` is not applied.** `AuthClient.__init__` builds the default `StatelessStateStore` with a hardcoded `cookie_name="_a0_session"`. To change the session cookie name today, pass a store explicitly. Fixing the default changes cookie names, which is an Ask First change.

4. **Unmarked async tests are skipped.** pytest-asyncio runs in strict mode (no `asyncio_mode` in `pyproject.toml`), so pytest skips an `async def test_...` without `@pytest.mark.asyncio` and only emits a warning. Copy the marker from neighbouring tests.

5. **Route registration mutates the router you pass.** `register_auth_routes(router, config)` adds route handlers to the given `APIRouter`. Calling it twice on the shared module-level `router` registers the routes twice. In tests, build a fresh `APIRouter()` per case. Enabling both `mount_connect_routes` and `mount_connected_account_routes` raises `ConfigurationError`.

6. **Dependency lists live in two files.** `pyproject.toml` drives Poetry and `poetry.lock`. `requirements.txt` is what the SCA workflow installs before its Snyk scan. Update both so the scanned dependencies match what Poetry resolves.
Loading
Loading