Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:

- name: Install packages with all extras
run: |
pip install -e "packages/pyxle-db[postgres,mysql,dev]"
pip install -e "packages/pyxle-db[sqlalchemy,postgres,mysql,dev]"
pip install -e "packages/pyxle-auth[dev]"
pip install -e "packages/pyxle-mail[resend,dev]"

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ packages/

```bash
python -m venv .venv && source .venv/bin/activate
pip install -e "packages/pyxle-db[postgres,mysql,dev]" -e "packages/pyxle-auth[dev]"
pip install -e "packages/pyxle-db[sqlalchemy,postgres,mysql,dev]" -e "packages/pyxle-auth[dev]"

# unit + SQLite suites
(cd packages/pyxle-db && pytest)
Expand Down
78 changes: 78 additions & 0 deletions packages/pyxle-auth/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,84 @@ All notable changes to `pyxle-auth` are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.0] - 2026-06-17

### Added

- **`AuthSessionMiddleware`** — contributed automatically through the
plugin's middleware seam. It populates `request.user` (a `User` or
`None`) on every request and serves two endpoints the client `useAuth`
hook talks to:
- `GET {authPathPrefix}/me` — the current user as JSON (safe method;
CSRF-exempt).
- `POST {authPathPrefix}/logout` — revoke the session and clear the
cookie (state-changing; covered by core CSRF, which runs first).

A request without the session cookie does zero database work; one that
carries it performs a single indexed lookup, and the guards reuse that
resolved value (cached on the request) so a guarded loader never
resolves the session twice. Both this middleware and the OAuth
middleware are **pure-ASGI** (not `BaseHTTPMiddleware`): the pass-through
path forwards the response untouched, so they work with streaming-SSR
(`<Suspense>`) pages and never raise `No response returned` on a
mid-stream client disconnect.
- **Credentials API** — `POST {prefix}/login` and `POST {prefix}/signup`
(email + password), gated by `enableCredentialsApi` (default on). They
reuse the hardened `AuthService.sign_in` / `sign_up` (per-IP and
per-email rate limiting, enumeration-safe errors) and map failures to
conventional status codes: `401` invalid credentials, `409` account
exists, `422` weak password, `403` unverified email, `429` rate limited
(with `Retry-After`). The framework `useAuth()` hook calls them; apps
rolling custom flows turn them off and keep `/me` + `/logout`.
- **SSR seed** — the middleware publishes the signed-in user plus the
endpoint map on `request.scope["pyxle.auth"]`, which Pyxle core seeds
into `window.__PYXLE_AUTH__`. The client `useAuth()` hook reads it to
render the user on the first frame with no round-trip.
- `AuthSettings.auth_path_prefix` (config key `authPathPrefix`, env
`PYXLE_AUTH_PATH_PREFIX`; default `/auth`) — move the middleware's
endpoints if your app already owns `/auth`.
- `AuthSettings.enable_credentials_api` (config key `enableCredentialsApi`,
env `PYXLE_AUTH_ENABLE_CREDENTIALS_API`; default `True`).
- **OAuth 2.0 / OIDC sign-in** (`pyxle_auth.oauth`, `[oauth]` extra) —
built-in Google, GitHub, and Discord providers, served at
`GET {prefix}/oauth/{provider}/{start,callback}`. Security model:
- **PKCE `S256` mandatory** — the verifier never leaves the server.
- A **signed, single-use, HttpOnly `state` cookie** binds the flow to the
browser (provider + PKCE verifier + `next` + nonce, HMAC-verified with
`hmac.compare_digest`); the echoed `state` must match the cookie nonce.
This is the login-CSRF defense for the `GET` callback.
- **Account linking only on a provider-verified email** — the
takeover guard; a returning identity signs in directly.
- The post-login `next` is **same-origin path only** (open-redirect
guard), re-checked on use.
- Client **secrets come from the environment only**
(`PYXLE_AUTH_OAUTH_<PROVIDER>_CLIENT_ID` / `…_CLIENT_SECRET`) and are
redacted in `repr`; the state cookie is signed with `PYXLE_AUTH_SECRET`
(or `PYXLE_SECRET_KEY`), required in strict mode.

Configure with the `oauth` plugin setting:
`{ "oauth": { "providers": ["google", "github"] } }`. New `oauth_identities`
table (migration `0002`). `AuthService` gained `start_session()` and
`create_external_user()` for passwordless, externally-authenticated sign-in.
- **JWT for API / mobile clients** (`pyxle_auth.jwt_tokens.JWTService`,
`[jwt]` extra). Short-lived signed **access tokens** (HS256) plus long-lived
**opaque refresh tokens** stored hashed-at-rest. Refresh does
**rotation with reuse detection**: every refresh issues a new token and
marks the old one used; replaying a rotated token **revokes the whole token
family** (theft detection). Served at `POST {prefix}/token` (email+password
→ pair) and `POST {prefix}/token/refresh` (rotate) when the `jwt` setting is
configured. New guards: `bearer_user()` (JWT → PAT) and `authenticate()`
(session → JWT → PAT). New `jwt_refresh_tokens` table (migration `0003`).
`AuthService.verify_credentials()` extracted from `sign_in` so token
issuance reuses the same rate-limited, enumeration-safe check without
minting a session. Add `{prefix}/token*` to `csrf.exempt_paths` for
non-browser clients (the endpoints authenticate from the body, not cookies).
- Roadmap-named guard aliases: `login_required` / `login_required_action`
and `permission_required` / `permission_required_action` — thin,
explicit re-exports of the existing `require_user_*` / `require_permission_*`
guards (a wrapping decorator would violate the framework's
"decorators add metadata, not behaviour" rule).

## [0.2.0] - 2026-06-11

### Changed (BREAKING)
Expand Down
6 changes: 6 additions & 0 deletions packages/pyxle-auth/examples/auth-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Generated locally — never commit the dev database or build artifacts.
data/
.pyxle-build/
dist/
node_modules/
.env
43 changes: 43 additions & 0 deletions packages/pyxle-auth/examples/auth-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# pyxle-auth example: sign up / sign in / dashboard

A minimal Pyxle app that wires up `pyxle-auth` end to end:

- **`request.user`** populated automatically by the session middleware
(read in `pages/index.pyxl`'s loader).
- **`useAuth()`** client hook for live user state + `login` / `signup` /
`logout` (`pages/login.pyxl`, `pages/register.pyxl`, `pages/index.pyxl`).
- **`require_user_page`** guarding a loader (`pages/dashboard.pyxl`), with an
`error.pyxl` that turns the 401 into a friendly "please sign in".
- The session user is **seeded into the server render**, so the navbar shows
the right state on the first paint — no flash of "logged out".

## Run it

From this directory, with `pyxle`, `pyxle-db`, and `pyxle-auth` installed:

```bash
pyxle dev
```

Then open http://127.0.0.1:8000:

1. **Create account** → you're signed in and redirected to the dashboard.
2. **Sign out**, then **Sign in** again with the same credentials.
3. Visit **/dashboard** while signed out → the `require_user_page` guard
raises a 401 and `error.pyxl` offers a sign-in link.

> This example sets `"strict": false` so it runs over plain HTTP. **In
> production**, leave strict mode on (the default) and serve over HTTPS so the
> session cookie gets the `Secure` flag. Relax strict per-environment via
> `PYXLE_AUTH_STRICT=false`, never in committed config.

## What to read next

- The plugin guide: `pyxle/docs/plugins/pyxle-auth.md`
- The client hook: `pyxle/docs/reference/client-api.md` → `useAuth()`

To add **OAuth** (Google/GitHub/Discord), install `pyxle-auth[oauth]`, set
`PYXLE_AUTH_OAUTH_<PROVIDER>_CLIENT_ID/SECRET` + `PYXLE_AUTH_SECRET`, add
`"oauth": { "providers": ["google"] }` to the plugin settings, and link to
`/auth/oauth/google/start`. For **JWT** API tokens, install `pyxle-auth[jwt]`,
add `"jwt": { "accessTtlSeconds": 900 }`, and exempt `/auth/token*` from CSRF.
Loading
Loading