# Architecture (/docs/architecture) Layers [#layers] | Layer | Role | Code | | ----------------- | --------------------------------------------------------------------------------------------- | ------------------------------------ | | **Transport** | Reads/writes the **session token** on HTTP (Bearer header or cookie) | `aura_auth.transport` | | **Strategy** | Validates credentials and returns a **user** (e.g. password on a credential account) | `aura_auth.strategies.password` | | **Backend** | Implements **`BackendProtocol`** — CRUD for users, accounts, sessions, verifications | `aura_auth.backend.sqlalchemy` | | **Orchestration** | **`AuraAuth`**, **`UserManager`** — register, login, sessions, verification helpers | `aura_auth.app`, `aura_auth.manager` | Contracts (protocols, schemas, config, exceptions, security helpers) live in **`aura_auth._core`**. Higher layers depend on protocols, not on each other’s concrete types. Four tables [#four-tables] 1. **User** — identity: `name`, `email`, `email_verified`, `image`, timestamps. 2. **Account** — one row per sign-in method: `provider_id` (e.g. `"credential"`), `account_id`, optional OAuth fields, **`password`** (hashed) for credential accounts. 3. **Session** — opaque **`token`**, **`user_id`**, **`expires_at`**, optional IP / user agent. 4. **Verification** — short-lived rows for email verification, resets, etc. Login creates a **session** row; **`current_user`** resolves the token against the session table, then loads the user by id. This is **not** a stateless JWT login flow (though **`TokenHelper`** remains available for other signing needs). Request flow (login) [#request-flow-login] 1. Client posts email/password to **`POST /auth/login`**. 2. **Password strategy** finds the credential **account**, verifies bcrypt, returns the **user**. 3. **UserManager** creates a **session** with a new opaque token. 4. Response returns **`AuthResponse`**; transport may set **Bearer** in the body only, or also **`Set-Cookie`** when cookie transport is enabled. # Configuration (/docs/configuration) `AuraAuth(database_url=..., secret=..., **kwargs)` merges supported keys onto **`AuraAuthConfig`** (`aura_auth._core.config`). Routing [#routing] | Field | Default | Purpose | | ------------------ | --------- | ------------------------------------------------------------- | | **`route_prefix`** | `"/auth"` | Prefix for routes returned by **`auth.router`**. | | — | — | Use **`auth.raw_router`** to mount with your own prefix. | Sessions [#sessions] | Field | Default | Purpose | | ----------------------------------- | ----------------- | ----------------------------------------------------------------- | | **`session_lifetime_seconds`** | `604800` (7 days) | Session row expiry; cookie **`max-age`** when using cookies. | | **`verify_token_lifetime_seconds`** | `86400` | Verification rows (e.g. email verify). | Cookies [#cookies] When **`cookie_transport`** is `True`, register/login call **`set_token`** on the response (HTTP-only cookie by default). | Field | Default | Purpose | | ---------------------- | -------------- | ---------------------------------------------------------------------------------- | | **`cookie_transport`** | `False` | Use cookie instead of Bearer for **`get_token`** / **`set_token`**. | | **`cookie_name`** | `"aura_token"` | Cookie name. | | **`cookie_secure`** | `True` | **`Secure`** flag. Use **`False`** for `http://` local dev. | | **`cookie_httponly`** | `True` | HTTP-only. | | **`cookie_samesite`** | `"lax"` | `lax`, `strict`, or `none`. | With **`cookie_transport=True`**, the client must send the cookie; the **`Authorization`** header is **not** read by the default transport. Passwords [#passwords] | Field | Default | | ------------------------- | ------- | | **`password_min_length`** | `8` | Models (SQLAlchemy) [#models-sqlalchemy] Optional overrides passed to the backend: * **`user_model`** * **`account_model`** * **`session_model`** * **`verification_model`** If omitted, the built-in **`Default*`** models from **`aura_auth.models.sqlalchemy`** are used. Advanced [#advanced] | Field | Purpose | | ------------ | ------------------------------------------------------------- | | **`engine`** | Inject a pre-built async SQLAlchemy engine (common in tests). | The **`secret`** field is required on the public constructor today (reserved for signing and future JWT-oriented features). # Contributing (/docs/contributing) Clone and Python env [#clone-and-python-env] From the repository root (parent of this `docs/` app): ```bash uv sync uv run ruff check . uv run ruff format . uv run ty check uv run pytest ``` Or use **`make check`** if the Makefile is present. Agent / contributor notes [#agent--contributor-notes] See **`AGENTS.md`** at the repo root for the phased development plan, import boundaries (`_core` stays free of router/backend imports), docstring style, and test layout. Some phase descriptions predate the **account + session** architecture; treat the **source tree** and **`README.md`** as canonical for current behavior. Documentation site (this folder) [#documentation-site-this-folder] ```bash cd docs pnpm install pnpm dev ``` Build: ```bash pnpm build ``` Set **`OPENROUTER_API_KEY`** (or your chosen provider) if you use the bundled **Ask AI** panel — the model is not provided by Fumadocs. # For LLMs and agents (/docs/for-llms-and-agents) This documentation site is built with **Fumadocs**. The following endpoints help **LLMs**, **IDE agents**, and **automation** ingest the same content as the HTML docs. Endpoints [#endpoints] | URL | Description | | -------------------------- | --------------------------------------------------------------------- | | **`/llms.txt`** | Compact **index** of pages (titles and URLs), suitable for discovery. | | **`/llms-full.txt`** | **Full** processed markdown for **all** pages, concatenated. | | **`/docs/...path....mdx`** | Single page as markdown (rewritten to the internal MDX route). | Use **`Accept`** headers that prefer markdown where supported (see [Fumadocs negotiation](https://fumadocs.dev/docs/ui/llms)); this project’s **`proxy`** can rewrite doc paths for markdown-friendly clients. On each docs page [#on-each-docs-page] Use **“Copy Markdown”** and **view options** in the page header to copy or open the **`.mdx`** URL for that page. Source of truth [#source-of-truth] Library behavior is defined by the **Python package** in this repository (`src/aura_auth`) and tests under **`tests/`**. If this site and **`README.md`** disagree with **`AGENTS.md`** phase text, prefer the **implementation** and the **README**. Repository: [github.com/snapwre/aura-auth](https://github.com/snapwre/aura-auth) # Introduction (/docs) **Aura Auth** is a small, explicit library for **FastAPI** and **async SQLAlchemy**. The default install gives you **email/password** registration and login with: * **Identity vs credentials** — a `User` row for who someone is, and `Account` rows for how they sign in (including `provider_id="credential"` with a bcrypt hash). * **Server-side sessions** — opaque tokens stored in the database so you can **revoke** them, list devices, and log out cleanly. * **Optional cookie transport** — HTTP-only session cookies, or **Bearer** tokens in the `Authorization` header. The project is **early beta**: pin versions in production and follow release notes when upgrading. Install [#install] Requires **Python 3.11+**. ```bash uv add aura-auth # or: pip install aura-auth ``` Install from GitHub during beta: ```bash uv add "aura-auth @ git+https://github.com/snapwre/aura-auth.git" ``` Next steps [#next-steps] # Models and backend (/docs/models-and-backend) Mixins [#mixins] Under **`aura_auth.models.base`**, mixins define the columns shared by the default implementation: * **`UserMixin`** — identity fields (no password on the user row). * **`AccountMixin`** — links to `user_id`, `provider_id`, `account_id`, tokens, **`password`** for credential provider, timestamps. * **`SessionMixin`** — `token`, `expires_at`, optional IP / user agent. * **`VerificationMixin`** — `identifier`, `value`, `expires_at`, timestamps. Subclass a mixin with your declarative **`Base`**, add columns, then pass **`user_model=...`** (and optionally account/session/verification models) into **`AuraAuth`**. Default models [#default-models] **`aura_auth.models.sqlalchemy`** exposes **`DefaultUser`**, **`DefaultAccount`**, **`DefaultSession`**, **`DefaultVerification`** and a shared **`Base`** for **`create_tables`**. BackendProtocol [#backendprotocol] The SQLAlchemy backend implements async methods for: * **Users** — `get_user_by_id`, `get_user_by_email`, `create_user`, `update_user`, `delete_user` * **Accounts** — `get_account`, `get_accounts_by_user`, `create_account`, `update_account`, `delete_account` * **Sessions** — `create_session`, `get_session_by_token`, `list_sessions_by_user`, deletes by id/token/user * **Verifications** — `create_verification`, `get_verification`, `delete_verification`, `delete_expired_verifications` Strategies and **`UserManager`** depend on this protocol so another database layer could be swapped in later. # Quickstart (/docs/quickstart) Minimal app [#minimal-app] ```python from fastapi import Depends, FastAPI from aura_auth import AuraAuth app = FastAPI() auth = AuraAuth( database_url="sqlite+aiosqlite:///./app.db", secret="change-me-to-a-long-random-secret-at-least-32-chars", ) @app.on_event("startup") async def startup() -> None: await auth.create_tables() auth.init_app(app) @app.get("/protected") async def protected(user=Depends(auth.current_user())): return {"message": f"Hello, {user.name}"} ``` `init_app` registers the bundled router (by default under **`/auth`**) and maps library exceptions to HTTP responses. Register and login [#register-and-login] | Method | Path | Body / notes | | ------ | ---------------- | ---------------------------------------------------- | | `POST` | `/auth/register` | `{"name": "...", "email": "...", "password": "..."}` | | `POST` | `/auth/login` | `{"email": "...", "password": "..."}` | Both return **`AuthResponse`**: a **`user`** object and a **`session`** with **`token`**, **`expires_at`**, and **`id`**. Authenticated requests [#authenticated-requests] **Bearer (default)** — send the session token from the JSON body: ```http Authorization: Bearer ``` **Cookie** — set `cookie_transport=True` (and usually `cookie_secure=False` on plain HTTP during local dev). Register/login set **`Set-Cookie`**; `Depends(auth.current_user())` reads the same cookie. See [Sessions and cookies](/docs/sessions-and-cookies). Dependencies [#dependencies] * **`Depends(auth.current_user())`** — 401 if not logged in. * **`Depends(auth.current_verified_user())`** — same, plus 403 if `email_verified` is false. Custom route prefix [#custom-route-prefix] ```python auth = AuraAuth( database_url="sqlite+aiosqlite:///./app.db", secret="...", route_prefix="/api/v1/auth", ) ``` To mount paths yourself, use **`auth.raw_router`** with your own `prefix`. # Sessions and cookies (/docs/sessions-and-cookies) Session token [#session-token] After **register** or **login**, the client receives **`session.token`** — an **opaque** string stored in the **`sessions`** table. The API checks it with **`get_session_by_token`**; expired or deleted sessions yield **401**. Benefits over stateless JWT-only login: * **Revocation** — delete the row (logout, compromised token). * **Device lists** — **`GET /auth/sessions`** (non-expired sessions for the current user). * **Per-session logout** — **`DELETE /auth/sessions/{session_id}`**. Bearer transport (default) [#bearer-transport-default] `cookie_transport=False` (**default**) * Clients send **`Authorization: Bearer `**. * **`set_token`** on the response is a no-op for Bearer (token is only in JSON). Cookie transport [#cookie-transport] `cookie_transport=True` * **Register** / **login** responses include **`Set-Cookie`** (name from **`cookie_name`**, default `aura_token`). * **`Depends(auth.current_user())`** reads the token via **`request.cookies`**. Local HTTP [#local-http] Browsers **do not** send cookies with the **`Secure`** attribute over plain **`http://`**. For local development set: ```python AuraAuth( database_url="...", secret="...", cookie_transport=True, cookie_secure=False, ) ``` Use **`cookie_secure=True`** (default) behind **HTTPS** in production. Logout [#logout] **`POST /auth/logout`** revokes the current session (by token from header or cookie) and clears the cookie when cookie transport is enabled.