Metadata-Version: 2.5
Name: pyfastauth
Version: 0.1.0
Summary: Batteries-included authentication and authorization for FastAPI: JWT, rotating refresh tokens, cookie and bearer transports, roles and scopes, HTML UI, migrations, and a CLI, on SQLAlchemy or MongoDB.
Project-URL: Homepage, https://github.com/Aadik1ng/fastauth
Project-URL: Documentation, https://github.com/Aadik1ng/fastauth#readme
Project-URL: Changelog, https://github.com/Aadik1ng/fastauth/blob/main/CHANGELOG.md
Project-URL: Source, https://github.com/Aadik1ng/fastauth
Project-URL: Issues, https://github.com/Aadik1ng/fastauth/issues
Project-URL: Security, https://github.com/Aadik1ng/fastauth/blob/main/SECURITY.md
License-Expression: MIT
License-File: LICENSE
Keywords: argon2,authentication,authorization,csrf,fastapi,jwt,mongodb,oauth2,rbac,security,sessions,sqlalchemy
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: AsyncIO
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: Session
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: alembic>=1.13
Requires-Dist: fastapi>=0.115
Requires-Dist: greenlet>=3.0
Requires-Dist: jinja2>=3.1
Requires-Dist: pwdlib[argon2]>=0.2.1
Requires-Dist: pydantic>=2.7
Requires-Dist: pyjwt>=2.9
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: pyyaml>=6.0
Requires-Dist: sqlalchemy[asyncio]>=2.0.30
Requires-Dist: starlette>=0.40
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: anyio>=4.4; extra == 'dev'
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: import-linter>=2.0; extra == 'dev'
Requires-Dist: motor<4,>=3.5; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pip-audit>=2.7; extra == 'dev'
Requires-Dist: pre-commit>=3.8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest-xdist>=3.6; extra == 'dev'
Requires-Dist: pytest>=8.2; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: twine>=5.1; extra == 'dev'
Requires-Dist: types-pyyaml; extra == 'dev'
Provides-Extra: mongo
Requires-Dist: motor<4,>=3.5; extra == 'mongo'
Provides-Extra: postgres
Requires-Dist: asyncpg>=0.29; extra == 'postgres'
Provides-Extra: sqlite
Requires-Dist: aiosqlite>=0.20; extra == 'sqlite'
Provides-Extra: testing
Requires-Dist: pytest-asyncio>=0.24; extra == 'testing'
Requires-Dist: pytest>=8.2; extra == 'testing'
Description-Content-Type: text/markdown

# fastauth

[![CI](https://github.com/Aadik1ng/fastauth/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Aadik1ng/fastauth/actions/workflows/ci.yml)

**Batteries-included authentication and authorization for FastAPI.**

Password login, JWT access tokens, rotating refresh tokens with server-side
revocation, cookie *and* bearer transports, CSRF protection, YAML-defined roles
and scopes, a per-user ownership layer, login/signup/account pages that need no
JavaScript, bundled migrations, and a CLI — installed with one command and
wired in with three lines.

Runs on PostgreSQL, SQLite, or **MongoDB** — and on MongoDB it needs no replica
set and no second database for auth. Writing your own storage backend is a
supported path, with a shipped conformance test suite to prove it correct.

> **Status: alpha (0.1.0).** The public API is marked and documented, but this
> is a 0.x release: minor versions may break it. See
> [CHANGELOG.md](CHANGELOG.md) and the release policy below.

---

## Five minutes

> Installed as **`pyfastauth`**, imported as **`fastauth`** -- the same split as
> `pyjwt`/`jwt`. The PyPI name `fastauth` is blocked by an unrelated project
> abandoned in 2022; the import path, the CLI and the repo are all `fastauth`.

```bash
pip install "pyfastauth[sqlite]"

fastauth init                                   # writes auth.yaml, .env.example
fastauth secret generate --env-line >> .env     # AUTH_SECRET=...
export $(grep AUTH_SECRET .env)                 # or use your process manager
fastauth db upgrade                             # create the schema
```

```python
# main.py
from fastapi import FastAPI
from fastauth import FastAuth

app = FastAPI()
auth = FastAuth(config="auth.yaml")
auth.install(app)
```

```bash
uvicorn main:app --reload
```

That is the whole integration. `http://localhost:8000/auth/signup` is a working
signup page; `http://localhost:8000/auth/login` is a working login page.

### Generated routes

`fastauth routes` prints the authoritative table for *your* configuration. With
the default cookie configuration, 15 of the 16 declared routes are mounted:

| Method | Path | Auth | Media type |
| --- | --- | --- | --- |
| GET, POST | `/auth/login` | public | `text/html` |
| GET, POST | `/auth/signup` | public | `text/html` |
| GET | `/auth/account` | required | `text/html` |
| POST | `/auth/logout` | public | `text/html` |
| POST | `/auth/logout-all` | required | `text/html` |
| POST | `/auth/api/signup` | public | `application/json` |
| POST | `/auth/api/login` | public | `application/json` |
| POST | `/auth/token` | public | `application/json` |
| POST | `/auth/refresh` | public | `application/json` |
| POST | `/auth/api/logout` | public | `application/json` |
| POST | `/auth/api/logout-all` | required | `application/json` |
| GET | `/auth/me` | required | `application/json` |
| GET | `/auth/sessions` | required | `application/json` |
| DELETE | `/auth/sessions/{session_id}` | required | `application/json` |

The stylesheet is served from `/auth/static/`. `POST /auth/token` is the OAuth2
password grant that drives Swagger's **Authorize** button; it is mounted by
default only in bearer mode ([D-08](docs/DECISIONS.md)). The mount point moves
with `server.auth_mount_path`.

### One protected endpoint

```python
from typing import Annotated
from fastapi import Depends
from fastauth import AuthUser

@app.get("/whoami")
async def whoami(user: Annotated[AuthUser, Depends(auth.current_user)]) -> dict[str, str]:
    return {"email": user.email}
```

No credential → **401**.

### One scope-protected endpoint

```python
@app.get("/contracts")
async def list_contracts(
    user: Annotated[AuthUser, Depends(auth.require_scopes("contracts:read"))],
) -> list[str]:
    return []
```

Authenticated but unscoped → **403**. Scopes come from roles declared in
`auth.yaml`:

```yaml
roles:
  user:
    scopes: ["contracts:read"]
  admin:
    inherits: ["user"]
    scopes: ["admin:*"]
```

An entire router can carry the requirement, so a new endpoint cannot be added
unprotected by forgetting a decorator:

```python
router = auth.protected_router(prefix="/contracts", scopes=["contracts:read"])
```

### One ownership example

Logging a user in does not segregate their data. `SELECT * FROM contracts`
still returns everybody's contracts. The ownership layer is what fixes that:

```python
from fastauth.ownership import OwnedModelMixin, OwnedRepository

class Contract(OwnedModelMixin, Base):        # adds owner_id: UUID, not null, indexed
    __tablename__ = "contracts"
    id: Mapped[UUID] = mapped_column(GUID(), primary_key=True, default=uuid4)
    title: Mapped[str] = mapped_column(String(200))

contracts: OwnedRepository[Contract] = OwnedRepository(Contract)

@router.get("/{contract_id}")
async def read_contract(
    contract_id: UUID,
    user: Annotated[AuthUser, Depends(auth.current_user)],
    db: Annotated[AsyncSession, Depends(auth.session_dependency)],
) -> ContractOut:
    row = await contracts.get_for_owner(db, owner_id=user.id, object_id=contract_id)
    if row is None:                            # not yours and does not exist
        raise HTTPException(404)               # are the same answer, deliberately
    return ContractOut.model_validate(row)
```

There is deliberately **no** unscoped `get()`, `list()`, `update()` or
`delete()`. The one way across tenants is `SystemRepository.list_all_as_system`,
which demands a written reason and logs a `WARNING`.

---

## What you get

| | |
| --- | --- |
| **Login and signup** | JSON endpoints and server-rendered HTML pages. No Node.js, no build step, zero JavaScript. |
| **Access tokens** | HS256 JWTs, 15-minute default, with issuer, audience, type and token-version validation. |
| **Refresh tokens** | Opaque, backed by a server-side session row, rotated on every use, with reuse detection ([D-02](docs/DECISIONS.md)). |
| **Transports** | Cookie (`HttpOnly`, `Secure`, `SameSite`) or bearer header. Both are accepted on a request; the header wins ([D-05](docs/DECISIONS.md)). |
| **CSRF** | Signed, session-bound double-submit tokens, rotated at login to defeat fixation. Enforced on cookie-authenticated requests only. |
| **Passwords** | Argon2 via `pwdlib`, per-password salts, transparent rehash on policy change. |
| **Roles and scopes** | Declared in YAML, with inheritance and wildcards. Route- and router-level dependencies. |
| **Ownership** | `OwnedModelMixin` + `OwnedRepository`: every query carries the owner predicate, and foreign rows are indistinguishable from absent ones. |
| **Rate limiting** | Sliding-window throttling on login by IP and by identifier, signup by IP, refresh by session. No permanent lockout. |
| **Security headers** | CSP (`script-src 'none'`), `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy` — never overriding headers the host application already set. |
| **Databases** | SQLite (development) and PostgreSQL (production) via SQLAlchemy, with bundled Alembic migrations; MongoDB via `pyfastauth[mongo]`, needing no replica set. Async throughout. |
| **CLI** | `init`, `config validate`, `secret generate`, `doctor`, `db`, `users`, `sessions`, `routes`, `version`. |
| **Events** | Twelve auditable events with an `@auth.on(...)` hook. A failing handler can never break authentication. |
| **Typing** | `py.typed`, `mypy --strict` clean. |

## What this is *not*

The MVP deliberately omits the following. The architecture allows them later;
none is present today, and no shim pretends otherwise.

- **No social login** — no Google, GitHub, Microsoft.
- **No OAuth/OIDC provider, no SAML, no enterprise SSO.**
- **No multi-factor authentication.**
- **No magic links or passwordless login.**
- **No email verification, no password-reset email** — there is no mail
  transport in the package at all.
- **No organisations, teams, workspaces, invitations or memberships.** Tenancy
  means *user-level* ownership: one user, one tenant.
- **No attribute-based access control** and no policy language. Roles and
  scopes only.
- **No admin dashboard.** Administration is the CLI.
- **No API-key management** and no service-to-service authentication.
- **No MySQL.** Nothing prevents it -- SQLAlchemy supports it and the extra is a
  driver line -- but it is untested here, so it is not claimed.
- **No automatic query interception.** The framework will not claim it can
  safely scope arbitrary ORM queries; you identify which models are user-owned.
- **No ownership layer outside SQLAlchemy.** `OwnedRepository` is SQLAlchemy-only.
  On MongoDB you get identity and roles, and write the `owner_id` filter yourself.
- **No key rotation, JWKS publishing or KMS integration.** One `HS256` secret.
- **No frontend SDK** and no plugin marketplace.
- **No distributed rate limiting.** The bundled limiter is in-process, so its
  limit multiplies by the number of workers — `fastauth doctor` warns about
  exactly this.

## Supported environments

- Python 3.11, 3.12, 3.13
- FastAPI ≥ 0.115, Starlette ≥ 0.40, SQLAlchemy 2.x, Pydantic 2.x
- SQLite (`pip install "pyfastauth[sqlite]"`) and PostgreSQL
  (`pip install "pyfastauth[postgres]"`)

## How it compares

[`fastapi-users`](https://github.com/fastapi-users/fastapi-users) is the
established option and remains a reasonable choice — particularly if you need
OAuth social login, which it has and this does not. As of this writing it is in
maintenance mode and has been for some time.

Relative to it, `fastauth` ships four things you would otherwise assemble
yourself:

1. **Server-rendered HTML pages.** Login, signup and account, styled, with CSRF
   and no JavaScript. `fastapi-users` is API-only; the UI is your problem.
2. **Roles and scopes in YAML.** Declared once, with inheritance and wildcards,
   rather than written as bespoke dependencies per project.
3. **A CLI.** `fastauth doctor` alone catches the production misconfigurations
   that are otherwise found by an incident.
4. **Bundled migrations.** `fastauth db upgrade` creates the schema. You do not
   copy model definitions into your own Alembic history.

And one thing it does *not* ship: a starter template to copy. The framework is
a dependency, so upgrades reach you through `pip`.

## Documentation

| Guide | |
| --- | --- |
| [Quickstart](docs/quickstart.md) | Five minutes, install to first protected route. |
| [Configuration reference](docs/configuration.md) | Every `auth.yaml` key, its type and its default. |
| [Cookie vs bearer](docs/cookie-vs-bearer.md) | Choosing a transport, and the full token/session lifecycle. |
| [Roles and scopes](docs/roles-and-scopes.md) | Inheritance, wildcard matching, 401 vs 403. |
| [Ownership](docs/ownership.md) | Per-user data isolation with `OwnedRepository`. |
| [Databases and migrations](docs/databases-and-migrations.md) | SQLite → PostgreSQL, and every `fastauth db` operation. |
| [Production checklist](docs/production-checklist.md) | What to do before you deploy, plus troubleshooting. |
| [Threat model](docs/threat-model.md) | Threat, control, and residual risk — including what is *not* mitigated. |
| [Extending](docs/extending.md) | The protocols, and how to substitute an implementation. |
| [Architecture](docs/architecture.md) | Layering, and where to find things. |
| [Design decisions](docs/DECISIONS.md) | Every deviation from the specification, with reasons. |
| [Security policy](SECURITY.md) | How to report a vulnerability. |

## Examples

| | |
| --- | --- |
| [`examples/minimal_sqlite`](examples/minimal_sqlite) | The smallest working app. |
| [`examples/postgres_api`](examples/postgres_api) | Bearer-mode API on PostgreSQL, with a Dockerfile that installs from the built wheel. |
| [`examples/gpu_sla_reviewer`](examples/gpu_sla_reviewer) | The full use case: projects, contracts, uploads, review jobs, findings, conversations and messages, all per-user isolated. |

## Release policy

Semantic versioning. While the version is `0.x`:

- The public API is exactly `AuthConfig`, `AuthContext`, `AuthUser`, `FastAuth`
  from `fastauth`, and `OwnedModelMixin`, `OwnedRepository`,
  `SystemRepository` from `fastauth.ownership`. Everything else is an
  implementation detail. `FastAuth.components` is public but explicitly outside
  the stability promise.
- Breaking changes are documented in [CHANGELOG.md](CHANGELOG.md) under a
  **Breaking** heading, and only in a minor version.
- The meaning of a YAML key never changes silently. `schema_version` exists so
  that a format migration is explicit, and an unknown version refuses to boot
  rather than being interpreted optimistically.
- Minimum dependency versions are pinned; upper bounds are added only for a
  known incompatibility.

## Development

```bash
./scripts/setup-env.sh    # conda interpreter, pip libraries (D-23, D-24)
conda activate fastauth

make test                 # SQLite suite
make db-up                # PostgreSQL via Docker
make test-pg              # the same suite against PostgreSQL
make check                # ruff, mypy --strict, import-linter
make build                # wheel and sdist
```

## License

MIT. See [LICENSE](LICENSE).
