pyxle-auth 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyxle_auth-0.2.0/.gitignore +37 -0
- pyxle_auth-0.2.0/CHANGELOG.md +96 -0
- pyxle_auth-0.2.0/LICENSE +21 -0
- pyxle_auth-0.2.0/PKG-INFO +298 -0
- pyxle_auth-0.2.0/README.md +268 -0
- pyxle_auth-0.2.0/pyproject.toml +52 -0
- pyxle_auth-0.2.0/pyxle_auth/__init__.py +134 -0
- pyxle_auth-0.2.0/pyxle_auth/_ddl.py +49 -0
- pyxle_auth-0.2.0/pyxle_auth/api_tokens.py +301 -0
- pyxle_auth-0.2.0/pyxle_auth/errors.py +67 -0
- pyxle_auth-0.2.0/pyxle_auth/guards.py +199 -0
- pyxle_auth-0.2.0/pyxle_auth/migrations/0001-pyxle-auth-core.mysql.sql +82 -0
- pyxle_auth-0.2.0/pyxle_auth/migrations/0001-pyxle-auth-core.sql +86 -0
- pyxle_auth-0.2.0/pyxle_auth/models.py +122 -0
- pyxle_auth-0.2.0/pyxle_auth/plugin.py +234 -0
- pyxle_auth-0.2.0/pyxle_auth/py.typed +0 -0
- pyxle_auth-0.2.0/pyxle_auth/ratelimit.py +179 -0
- pyxle_auth-0.2.0/pyxle_auth/rbac.py +317 -0
- pyxle_auth-0.2.0/pyxle_auth/service.py +797 -0
- pyxle_auth-0.2.0/pyxle_auth/settings.py +218 -0
- pyxle_auth-0.2.0/pyxle_auth/tokens.py +189 -0
- pyxle_auth-0.2.0/tests/__init__.py +0 -0
- pyxle_auth-0.2.0/tests/conftest.py +31 -0
- pyxle_auth-0.2.0/tests/test_api_tokens.py +356 -0
- pyxle_auth-0.2.0/tests/test_database_contract.py +181 -0
- pyxle_auth-0.2.0/tests/test_guards.py +418 -0
- pyxle_auth-0.2.0/tests/test_live_backends.py +149 -0
- pyxle_auth-0.2.0/tests/test_plugin.py +268 -0
- pyxle_auth-0.2.0/tests/test_ratelimit.py +99 -0
- pyxle_auth-0.2.0/tests/test_rbac.py +325 -0
- pyxle_auth-0.2.0/tests/test_security_fixes.py +131 -0
- pyxle_auth-0.2.0/tests/test_service.py +565 -0
- pyxle_auth-0.2.0/tests/test_settings.py +114 -0
- pyxle_auth-0.2.0/tests/test_tokens.py +323 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Python bytecode
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
.eggs/
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
venv/
|
|
15
|
+
.venv/
|
|
16
|
+
|
|
17
|
+
# Test / coverage
|
|
18
|
+
htmlcov/
|
|
19
|
+
.coverage
|
|
20
|
+
.coverage.*
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
coverage.xml
|
|
23
|
+
|
|
24
|
+
# Node
|
|
25
|
+
node_modules/
|
|
26
|
+
|
|
27
|
+
# IDE
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
|
30
|
+
*.swp
|
|
31
|
+
|
|
32
|
+
# OS
|
|
33
|
+
.DS_Store
|
|
34
|
+
Thumbs.db
|
|
35
|
+
|
|
36
|
+
# Tooling caches
|
|
37
|
+
.ruff_cache/
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `pyxle-auth` are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.0] - 2026-06-11
|
|
9
|
+
|
|
10
|
+
### Changed (BREAKING)
|
|
11
|
+
|
|
12
|
+
- Requires `pyxle-db>=0.2.0`. Its transaction methods became
|
|
13
|
+
coroutines (`await tx.execute(...)`), and all pyxle-auth SQL is now
|
|
14
|
+
written in portable qmark style — the plugin works unchanged on
|
|
15
|
+
SQLite, PostgreSQL, and MySQL (DML fully portable; shipped DDL targets
|
|
16
|
+
SQLite/PostgreSQL — MySQL schema needs a dialect override, see README).
|
|
17
|
+
Upgraders from 0.1: the `ratelimit_buckets.key` column is now
|
|
18
|
+
`bucket_key` (KEY is reserved in MySQL); drop the old table — bucket
|
|
19
|
+
data is ephemeral hourly counters and recreates itself.
|
|
20
|
+
- The plugin now hard-requires the `pyxle-db` plugin to have run
|
|
21
|
+
first. List `"pyxle-db"` before `"pyxle-auth"` in
|
|
22
|
+
`pyxle.config.json::plugins`; startup aborts with an actionable
|
|
23
|
+
error otherwise.
|
|
24
|
+
- The `ensureSchema` plugin setting is removed. The plugin always
|
|
25
|
+
applies its bundled migrations and then runs each service's
|
|
26
|
+
idempotent `ensure_schema()` — both are no-ops on an up-to-date
|
|
27
|
+
database, so there is nothing left to opt out of.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- Password reset and email verification flows, powered by
|
|
32
|
+
`TokenService`: single-use, purpose-scoped, expiring tokens with
|
|
33
|
+
only the SHA-256 stored at rest. The library never sends email —
|
|
34
|
+
your app delivers the token through its own mailer.
|
|
35
|
+
- `RoleService` (RBAC): roles, permissions, and per-user grants,
|
|
36
|
+
registered as `auth.rbac`.
|
|
37
|
+
- `ApiTokenService`: long-lived `pyxle_pat_` personal access tokens
|
|
38
|
+
with scopes, per-user caps enforced atomically, and revocation.
|
|
39
|
+
Registered as `auth.api_tokens`.
|
|
40
|
+
- Request guards: `current_user`, `require_user_page`,
|
|
41
|
+
`require_user_action`, `require_permission_page`,
|
|
42
|
+
`require_permission_action`, and `bearer_token`, re-exported from
|
|
43
|
+
the package root.
|
|
44
|
+
- New settings: `password_reset_ttl_seconds` (default 1800),
|
|
45
|
+
`email_verify_ttl_seconds` (default 86400), and
|
|
46
|
+
`rate_limit_password_reset_per_hour` (default 3), each with a
|
|
47
|
+
`PYXLE_AUTH_*` environment variable and a camelCase plugin key.
|
|
48
|
+
- Settings precedence: plugin `settings` in `pyxle.config.json`
|
|
49
|
+
override `PYXLE_AUTH_*` environment variables, which override the
|
|
50
|
+
built-in defaults. `AuthSettings.from_env` grew an `overrides`
|
|
51
|
+
parameter to express this.
|
|
52
|
+
- Bundled migrations (`pyxle_auth/migrations`) applied through
|
|
53
|
+
`pyxle_db.Migrator` at startup, with `ensure_schema()` as
|
|
54
|
+
belt-and-braces after.
|
|
55
|
+
- New exports: `SessionInfo`, `InvalidToken`, `TokenClaim`,
|
|
56
|
+
`TokenService`, `ApiToken`, `ApiTokenService`, `TokenLimitReached`,
|
|
57
|
+
`TOKEN_PREFIX`, and `RoleService`.
|
|
58
|
+
- Live-backend test suite (`tests/test_live_backends.py`) running the
|
|
59
|
+
real plugin schema path and a full account lifecycle against
|
|
60
|
+
PostgreSQL and MySQL (gated on `PYXLE_DB_TEST_POSTGRES_URL` /
|
|
61
|
+
`PYXLE_DB_TEST_MYSQL_URL`, shared with pyxle-db's suites).
|
|
62
|
+
- `PYXLE_AUTH_STRICT` environment variable: `strict` now resolves
|
|
63
|
+
config > env > secure-default(True), so a committed config can stay
|
|
64
|
+
production-safe (strict + Secure cookies) while local HTTP dev
|
|
65
|
+
relaxes via the environment.
|
|
66
|
+
- **Bring your own database.** Services and the plugin now bind to the
|
|
67
|
+
`pyxle_db.DatabaseLike` protocol instead of the concrete `Database`
|
|
68
|
+
class, and the plugin's requirement is the `db.database` service
|
|
69
|
+
*name* — any plugin registering a protocol-satisfying object can back
|
|
70
|
+
pyxle-auth (pyxle-db remains the reference provider and a hard
|
|
71
|
+
dependency for the error types and migrator). The contract (surface,
|
|
72
|
+
`IntegrityError` translation, dialect, datetimes) is documented in the
|
|
73
|
+
README and enforced by `tests/test_database_contract.py`, which runs
|
|
74
|
+
the full lifecycle against a deliberately foreign database object.
|
|
75
|
+
|
|
76
|
+
### Fixed
|
|
77
|
+
|
|
78
|
+
- **Schema is now genuinely portable** (found by the live-server
|
|
79
|
+
suites). Key and indexed columns are `VARCHAR(n)` instead of `TEXT`
|
|
80
|
+
(MySQL cannot index bare `TEXT`), a `0001-pyxle-auth-core.mysql.sql`
|
|
81
|
+
override uses `DATETIME(6)` (MySQL `TIMESTAMP` is 2038-capped,
|
|
82
|
+
second-rounded, and session-time-zone converted), and
|
|
83
|
+
`ensure_schema()` creates indexes through an `information_schema`
|
|
84
|
+
probe on MySQL, which has no `CREATE INDEX IF NOT EXISTS`.
|
|
85
|
+
- Dependency floor corrected to `pyxle-framework>=0.4.0` — the
|
|
86
|
+
`pyxle.plugins` API first shipped in 0.4.0.
|
|
87
|
+
|
|
88
|
+
## [0.1.0] - 2026-04-23
|
|
89
|
+
|
|
90
|
+
### Added
|
|
91
|
+
|
|
92
|
+
- Initial release: email+password `AuthService` (argon2id hashing,
|
|
93
|
+
sliding sessions with an absolute cap, SHA-256 token storage,
|
|
94
|
+
enumeration-resistant errors, fixed-window rate limits), the
|
|
95
|
+
`pyxle-auth` plugin registering `auth.service`/`auth.settings`, and
|
|
96
|
+
`AuthSettings` loadable from the environment.
|
pyxle_auth-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pyxle
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyxle-auth
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Authentication plugin for Pyxle: argon2id sessions, password reset and email verification flows, RBAC, scoped API tokens, and request guards.
|
|
5
|
+
Project-URL: Homepage, https://pyxle.dev
|
|
6
|
+
Project-URL: Source, https://github.com/pyxle-dev/pyxle-plugins
|
|
7
|
+
Project-URL: Changelog, https://github.com/pyxle-dev/pyxle-plugins/blob/main/packages/pyxle-auth/CHANGELOG.md
|
|
8
|
+
Author-email: Pyxle <dev@pyxle.dev>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: argon2,auth,pyxle,rbac,sessions,tokens
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: argon2-cffi>=23.1
|
|
24
|
+
Requires-Dist: pyxle-db>=0.2.0
|
|
25
|
+
Requires-Dist: pyxle-framework>=0.4.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# pyxle-auth
|
|
32
|
+
|
|
33
|
+
Django-grade authentication for [Pyxle](https://pyxle.dev) apps:
|
|
34
|
+
sessions, password reset and email verification flows, roles and
|
|
35
|
+
permissions, API tokens, and one-line request guards. Built on
|
|
36
|
+
[pyxle-db](https://github.com/pyxle-dev/pyxle-plugins/tree/main/packages/pyxle-db),
|
|
37
|
+
so the same code runs on SQLite, PostgreSQL, and MySQL. (Caveat: every query is portable across all three, but the *shipped schema files* target SQLite and PostgreSQL; MySQL needs a dialect-override migration — `0001-pyxle-auth-core.mysql.sql` — because MySQL requires key lengths on TEXT keys. On the roadmap; contributions welcome.)
|
|
38
|
+
|
|
39
|
+
- **Sessions** — argon2id-hashed passwords, server-side sessions with
|
|
40
|
+
sliding expiry and an absolute cap, `HttpOnly; Secure; SameSite=Lax`
|
|
41
|
+
cookies.
|
|
42
|
+
- **Password reset & email verification** — single-use, purpose-scoped,
|
|
43
|
+
expiring tokens. The library never sends email; your app delivers the
|
|
44
|
+
link through its own mailer.
|
|
45
|
+
- **RBAC** — roles, permissions, per-user grants, and
|
|
46
|
+
`require_permission_*` guards.
|
|
47
|
+
- **API tokens** — long-lived `pyxle_pat_` personal access tokens with
|
|
48
|
+
scopes, per-user caps, and revocation, for CLIs and CI.
|
|
49
|
+
- **Guards** — `require_user_page(request)` and friends protect a
|
|
50
|
+
loader or action in one line.
|
|
51
|
+
- **Rate limits** — database-backed fixed-window buckets on sign-in,
|
|
52
|
+
sign-up, and reset requests; they survive process restarts.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install pyxle-auth
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quickstart
|
|
61
|
+
|
|
62
|
+
List `pyxle-db` **before** `pyxle-auth` in `pyxle.config.json` — the
|
|
63
|
+
auth services run on the database that plugin opens:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"plugins": [
|
|
68
|
+
"pyxle-db",
|
|
69
|
+
"pyxle-auth"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
That's the whole wire-up. At startup the plugin applies its bundled
|
|
75
|
+
migrations (idempotent, checksum-tracked) and registers the services
|
|
76
|
+
listed [below](#plugin-services).
|
|
77
|
+
|
|
78
|
+
Protect a page with a guard in its `@server` loader:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# pages/dashboard.pyxl — Python section
|
|
82
|
+
from pyxle.runtime import server
|
|
83
|
+
from pyxle_auth import require_user_page
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@server
|
|
87
|
+
async def load(request):
|
|
88
|
+
user = await require_user_page(request) # 401 → error boundary when signed out
|
|
89
|
+
return {"email": user.email}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Sign-in needs to put a `Set-Cookie` header on the response, so it lives
|
|
93
|
+
in an [API route](https://pyxle.dev/docs) (actions return plain JSON
|
|
94
|
+
payloads and can't attach cookies):
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# pages/api/sign_in.py
|
|
98
|
+
from starlette.requests import Request
|
|
99
|
+
from starlette.responses import JSONResponse
|
|
100
|
+
|
|
101
|
+
from pyxle_auth import AuthError, RateLimited, get_auth_service
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def endpoint(request: Request) -> JSONResponse:
|
|
105
|
+
body = await request.json()
|
|
106
|
+
auth = get_auth_service()
|
|
107
|
+
try:
|
|
108
|
+
user, cookie = await auth.sign_in(
|
|
109
|
+
email=body["email"],
|
|
110
|
+
password=body["password"],
|
|
111
|
+
ip=request.client.host,
|
|
112
|
+
user_agent=request.headers.get("user-agent", ""),
|
|
113
|
+
)
|
|
114
|
+
except RateLimited as exc:
|
|
115
|
+
return JSONResponse(
|
|
116
|
+
{"ok": False, "error": str(exc)},
|
|
117
|
+
status_code=429,
|
|
118
|
+
headers={"Retry-After": str(exc.retry_after_seconds)},
|
|
119
|
+
)
|
|
120
|
+
except AuthError as exc:
|
|
121
|
+
# InvalidCredentials and friends share one deliberately vague
|
|
122
|
+
# message — don't replace it with something more "helpful".
|
|
123
|
+
return JSONResponse({"ok": False, "error": str(exc)}, status_code=401)
|
|
124
|
+
|
|
125
|
+
response = JSONResponse({"ok": True, "userId": user.id})
|
|
126
|
+
response.set_cookie(**cookie.kwargs())
|
|
127
|
+
return response
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`sign_up` has the same shape. `sign_out(cookie_value=...)` returns a
|
|
131
|
+
cookie that clears the browser's copy — set it the same way.
|
|
132
|
+
|
|
133
|
+
## Bring your own mailer
|
|
134
|
+
|
|
135
|
+
pyxle-auth never sends email. Flows that need delivery return a raw,
|
|
136
|
+
single-use token exactly once; your app puts it in a link and hands it
|
|
137
|
+
to whatever mailer it already uses:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
# pages/api/forgot_password.py
|
|
141
|
+
async def endpoint(request: Request) -> JSONResponse:
|
|
142
|
+
body = await request.json()
|
|
143
|
+
auth = get_auth_service()
|
|
144
|
+
result = await auth.request_password_reset(
|
|
145
|
+
email=body["email"], ip=request.client.host
|
|
146
|
+
)
|
|
147
|
+
if result is not None:
|
|
148
|
+
user, token = result
|
|
149
|
+
await my_mailer.send(
|
|
150
|
+
to=user.email,
|
|
151
|
+
subject="Reset your password",
|
|
152
|
+
body=f"https://example.com/reset?token={token}",
|
|
153
|
+
)
|
|
154
|
+
# Same response whether the account exists or not — this endpoint
|
|
155
|
+
# must not be usable to probe for accounts.
|
|
156
|
+
return JSONResponse({"ok": True, "message": "Check your inbox."})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The user completes the flow with
|
|
160
|
+
`await auth.reset_password(raw_token=token, new_password=...)`, which
|
|
161
|
+
burns the token and revokes every session. Email verification mirrors
|
|
162
|
+
the pattern: `request_email_verification(user_id=...)` returns a token,
|
|
163
|
+
`confirm_email(raw_token=...)` redeems it. Both raise `InvalidToken`
|
|
164
|
+
for anything stale, used, unknown, or wrong-purpose —
|
|
165
|
+
indistinguishably.
|
|
166
|
+
|
|
167
|
+
For your own flows (invite links, magic links), the same machinery is
|
|
168
|
+
registered as `auth.tokens`: issue with a custom `purpose`, consume it
|
|
169
|
+
once, never store the raw value.
|
|
170
|
+
|
|
171
|
+
## Bring your own database
|
|
172
|
+
|
|
173
|
+
pyxle-auth binds to the **`db.database` plugin service**, not to the
|
|
174
|
+
pyxle-db package. The reference provider is pyxle-db, but any plugin (or
|
|
175
|
+
test fixture) that registers an object satisfying
|
|
176
|
+
`pyxle_db.DatabaseLike` works — an adapter over SQLAlchemy's async
|
|
177
|
+
engine, a bespoke driver wrapper, an in-memory fake.
|
|
178
|
+
|
|
179
|
+
The full contract a replacement must honour:
|
|
180
|
+
|
|
181
|
+
1. **Surface** — the five members of `pyxle_db.DatabaseLike`:
|
|
182
|
+
`execute`, `fetchone`, `fetchall`, an async-context-manager
|
|
183
|
+
`transaction()` (yielding the same query surface), and a `dialect`
|
|
184
|
+
property returning a `pyxle_db.Dialect`. SQL arrives in canonical
|
|
185
|
+
qmark style (`?` placeholders); rows go back as `pyxle_db.Row`.
|
|
186
|
+
2. **Errors** — unique-constraint violations must raise
|
|
187
|
+
`pyxle_db.IntegrityError`. pyxle-auth converts it into domain
|
|
188
|
+
behaviour (`AccountExists` on duplicate sign-up, idempotent role
|
|
189
|
+
grants); raise your driver's own error type and those paths break.
|
|
190
|
+
3. **Dialect** — `dialect.name` drives portable DDL. `sqlite`,
|
|
191
|
+
`postgresql`, and `mysql` have live-tested paths; any other name
|
|
192
|
+
falls back to the SQLite/PostgreSQL-flavoured DDL (right for
|
|
193
|
+
PostgreSQL-compatible engines, wrong for e.g. MSSQL).
|
|
194
|
+
4. **Datetimes** — reads return timezone-aware UTC; binds accept naive
|
|
195
|
+
(assumed UTC) or aware (converted) datetimes.
|
|
196
|
+
|
|
197
|
+
`tests/test_database_contract.py` runs the entire auth lifecycle against
|
|
198
|
+
a wrapper that exposes *only* this surface — it is both the executable
|
|
199
|
+
specification and a template for writing your own adapter.
|
|
200
|
+
|
|
201
|
+
## Security properties
|
|
202
|
+
|
|
203
|
+
- **Password hashing** — argon2id, `t=3, m=64 MiB, p=2` by default
|
|
204
|
+
(~300 ms on a 2020-era laptop), tunable via settings. Hashes are
|
|
205
|
+
transparently upgraded on sign-in when parameters change.
|
|
206
|
+
- **Nothing secret at rest** — session cookies, reset/verification
|
|
207
|
+
tokens, and API tokens all store only the SHA-256 of the secret. A
|
|
208
|
+
leaked database cannot resurrect a session or replay a reset link.
|
|
209
|
+
- **Enumeration resistance** — sign-in failures share one message and
|
|
210
|
+
run a dummy argon2 verify on unknown emails so timing stays flat;
|
|
211
|
+
password-reset requests do token-shaped work and return the same
|
|
212
|
+
shape whether the account exists or not; token redemption never says
|
|
213
|
+
*why* it failed.
|
|
214
|
+
- **Single-use tokens** — redemption burns the token atomically, so two
|
|
215
|
+
racing requests can't both succeed, and requesting a new reset link
|
|
216
|
+
invalidates earlier unused ones.
|
|
217
|
+
- **Rate limits** — sign-in is capped per IP *and* per email (10/hour
|
|
218
|
+
each), sign-up per IP (5/hour), reset requests per email and per IP
|
|
219
|
+
(3/hour). Buckets live in the database and survive restarts.
|
|
220
|
+
- **Session lifecycle** — sliding expiry (30 days) under an absolute
|
|
221
|
+
cap (90 days); password change and password reset revoke every
|
|
222
|
+
session; `list_sessions`/`revoke_session` power a "your devices"
|
|
223
|
+
screen.
|
|
224
|
+
- **Cookie posture** — `HttpOnly`, `Secure`, `SameSite=Lax` by default.
|
|
225
|
+
Strict mode (the default) refuses to start with `cookie_secure=False`.
|
|
226
|
+
|
|
227
|
+
## Plugin services
|
|
228
|
+
|
|
229
|
+
| Service | Type | Use it for |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `auth.service` | `AuthService` | Sign-up/in/out, sessions, password change/reset, email verification |
|
|
232
|
+
| `auth.rbac` | `RoleService` | Define roles, grant them, check permissions |
|
|
233
|
+
| `auth.tokens` | `TokenService` | Custom single-use token flows (invites, magic links) |
|
|
234
|
+
| `auth.api_tokens` | `ApiTokenService` | `pyxle_pat_` personal access tokens |
|
|
235
|
+
| `auth.settings` | `AuthSettings` | The resolved configuration (cookie name, TTLs, …) |
|
|
236
|
+
|
|
237
|
+
Reach them with `ctx.require(...)`, `pyxle.plugins.plugin(...)`, or the
|
|
238
|
+
typed helpers `get_auth_service()` / `get_auth_settings()`.
|
|
239
|
+
|
|
240
|
+
Guards resolve `auth.service` / `auth.rbac` automatically; pass
|
|
241
|
+
`service=` / `rbac=` explicitly in tests. For API routes authenticating
|
|
242
|
+
with personal access tokens, pair `bearer_token(request)` with
|
|
243
|
+
`ApiTokenService.resolve(raw_token=..., required_scope=...)`.
|
|
244
|
+
|
|
245
|
+
## Settings
|
|
246
|
+
|
|
247
|
+
Precedence: plugin `settings` in `pyxle.config.json` **>**
|
|
248
|
+
`PYXLE_AUTH_*` environment variables **>** defaults.
|
|
249
|
+
|
|
250
|
+
| Config key | Environment variable | Default | Meaning |
|
|
251
|
+
|---|---|---|---|
|
|
252
|
+
| `argonTimeCost` | `PYXLE_AUTH_ARGON_T` | `3` | Argon2 time cost |
|
|
253
|
+
| `argonMemoryKib` | `PYXLE_AUTH_ARGON_M` | `65536` | Argon2 memory (KiB) |
|
|
254
|
+
| `argonParallelism` | `PYXLE_AUTH_ARGON_P` | `2` | Argon2 parallelism |
|
|
255
|
+
| `passwordMinLength` | `PYXLE_AUTH_PW_MIN` | `8` | Reject shorter passwords |
|
|
256
|
+
| `passwordMaxLength` | — | `1024` | Reject pathological inputs |
|
|
257
|
+
| `sessionTtlSeconds` | `PYXLE_AUTH_SESSION_TTL` | `2592000` (30 d) | Sliding session lifetime |
|
|
258
|
+
| `sessionAbsoluteMaxSeconds` | `PYXLE_AUTH_SESSION_ABS_MAX` | `7776000` (90 d) | Hard cap from creation |
|
|
259
|
+
| `cookieName` | `PYXLE_AUTH_COOKIE_NAME` | `pyxle_session` | Session cookie name |
|
|
260
|
+
| `cookieSecure` | `PYXLE_AUTH_COOKIE_SECURE` | `true` | `Secure` cookie flag |
|
|
261
|
+
| `cookieSameSite` | `PYXLE_AUTH_COOKIE_SAMESITE` | `Lax` | `Lax` / `Strict` / `None` |
|
|
262
|
+
| `cookieDomain` | `PYXLE_AUTH_COOKIE_DOMAIN` | unset | Share across subdomains |
|
|
263
|
+
| `cookiePath` | — | `/` | Cookie path |
|
|
264
|
+
| `passwordResetTtlSeconds` | `PYXLE_AUTH_PASSWORD_RESET_TTL_SECONDS` | `1800` (30 min) | Reset-token lifetime |
|
|
265
|
+
| `emailVerifyTtlSeconds` | `PYXLE_AUTH_EMAIL_VERIFY_TTL_SECONDS` | `86400` (24 h) | Verify-token lifetime |
|
|
266
|
+
| `rateLimitSignInPerHour` | `PYXLE_AUTH_RL_SIGN_IN_PER_HOUR` | `10` | Per IP and per email |
|
|
267
|
+
| `rateLimitSignUpPerHour` | `PYXLE_AUTH_RL_SIGN_UP_PER_HOUR` | `5` | Per IP |
|
|
268
|
+
| `rateLimitPasswordResetPerHour` | `PYXLE_AUTH_RATE_LIMIT_PASSWORD_RESET_PER_HOUR` | `3` | Per email and per IP |
|
|
269
|
+
| `requireEmailVerified` | `PYXLE_AUTH_REQUIRE_VERIFIED` | `false` | Gate sign-in on verification |
|
|
270
|
+
| `strict` | — | `true` | Enforce `cookieSecure=true`; set `false` for HTTP dev servers |
|
|
271
|
+
|
|
272
|
+
Outside the plugin, load the same configuration with
|
|
273
|
+
`AuthSettings.from_env()`, and use `AuthSettings(...).for_tests()` in
|
|
274
|
+
test suites — it drops argon costs and TTLs so suites stay fast.
|
|
275
|
+
|
|
276
|
+
## Schema
|
|
277
|
+
|
|
278
|
+
The plugin owns its tables (`users`, `sessions`, `auth_tokens`,
|
|
279
|
+
`api_tokens`, `roles`, `user_roles`, `ratelimit_buckets`): bundled
|
|
280
|
+
migrations are applied through `pyxle_db.Migrator` at startup, followed
|
|
281
|
+
by each service's idempotent `ensure_schema()`. Repeated startups are
|
|
282
|
+
no-ops. The SQL is portable qmark style throughout, so the plugin works
|
|
283
|
+
on every pyxle-db backend without per-database configuration.
|
|
284
|
+
|
|
285
|
+
## Roadmap
|
|
286
|
+
|
|
287
|
+
Honest status — these are **not implemented yet**:
|
|
288
|
+
|
|
289
|
+
- OAuth / OIDC sign-in (Google, GitHub, generic OIDC)
|
|
290
|
+
- Multi-factor authentication (TOTP, WebAuthn)
|
|
291
|
+
|
|
292
|
+
If you need them today, the building blocks (sessions, `TokenService`,
|
|
293
|
+
guards) compose underneath whatever you bring; contributions are
|
|
294
|
+
welcome.
|
|
295
|
+
|
|
296
|
+
## License
|
|
297
|
+
|
|
298
|
+
MIT.
|