wred-auth 0.1.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.
- wred_auth-0.1.0/.gitignore +8 -0
- wred_auth-0.1.0/LICENSE +29 -0
- wred_auth-0.1.0/PKG-INFO +359 -0
- wred_auth-0.1.0/README.md +329 -0
- wred_auth-0.1.0/benchmarks/auth_load.py +180 -0
- wred_auth-0.1.0/pyproject.toml +25 -0
- wred_auth-0.1.0/tests/conftest.py +60 -0
- wred_auth-0.1.0/tests/test_accounts.py +392 -0
- wred_auth-0.1.0/tests/test_integrations.py +140 -0
- wred_auth-0.1.0/tests/test_providers.py +125 -0
- wred_auth-0.1.0/tests/test_storage.py +57 -0
- wred_auth-0.1.0/wred_auth/__init__.py +1102 -0
- wred_auth-0.1.0/wred_auth/migrations/env.py +33 -0
- wred_auth-0.1.0/wred_auth/migrations/versions/0001_accounts.py +76 -0
- wred_auth-0.1.0/wred_auth/migrations/versions/0002_retention_indexes.py +33 -0
- wred_auth-0.1.0/wred_auth/mongodb.py +240 -0
- wred_auth-0.1.0/wred_auth/providers.py +116 -0
- wred_auth-0.1.0/wred_auth/sqlalchemy.py +271 -0
- wred_auth-0.1.0/wred_auth/storage.py +46 -0
wred_auth-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Harrison Erd
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
17
|
+
may be used to endorse or promote products derived from this software
|
|
18
|
+
without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
wred_auth-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wred-auth
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Optional auth integration for Wred
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: wred>=0.13,<0.14
|
|
10
|
+
Requires-Dist: argon2-cffi>=23
|
|
11
|
+
Requires-Dist: PyJWT[crypto]>=2.9,<3
|
|
12
|
+
Requires-Dist: cryptography>=43
|
|
13
|
+
Requires-Dist: authlib>=1.3,<2
|
|
14
|
+
Requires-Dist: httpx>=0.27
|
|
15
|
+
Requires-Dist: pyotp>=2.9,<3
|
|
16
|
+
Requires-Dist: wred-ratelimit>=0.1,<0.2
|
|
17
|
+
Requires-Dist: pymongo>=4.13,<5 ; extra == "mongodb"
|
|
18
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0,<3 ; extra == "sqlalchemy"
|
|
19
|
+
Requires-Dist: alembic>=1.13 ; extra == "sqlalchemy"
|
|
20
|
+
Requires-Dist: aiosqlite>=0.20 ; extra == "sqlalchemy"
|
|
21
|
+
Requires-Dist: asyncpg>=0.29 ; extra == "sqlalchemy"
|
|
22
|
+
Requires-Dist: pytest>=8 ; extra == "test"
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.24 ; extra == "test"
|
|
24
|
+
Requires-Dist: httpx>=0.27 ; extra == "test"
|
|
25
|
+
Requires-Dist: wred-pydantic>=0.1,<0.2 ; extra == "test"
|
|
26
|
+
Provides-Extra: mongodb
|
|
27
|
+
Provides-Extra: sqlalchemy
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
|
|
30
|
+
# wred-auth
|
|
31
|
+
|
|
32
|
+
Bearer authorization and an opt-in account service for Wred 0.13. Python 3.11+.
|
|
33
|
+
This package owns no UI, email service, or application account data beyond its
|
|
34
|
+
explicit identity tables. Version 0.1.0 APIs are not yet stable.
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install './extensions/wred-auth[sqlalchemy]' './extensions/wred-ratelimit[redis]'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Authorization without account storage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from wred import App
|
|
44
|
+
from wred_auth import Auth, AuthError, Principal, principal
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def verify(token):
|
|
48
|
+
# Replace with your token verifier/account lookup.
|
|
49
|
+
if token != "development-only-token":
|
|
50
|
+
raise AuthError()
|
|
51
|
+
return Principal("alice", ("admin",))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
auth = Auth(verifier=verify)
|
|
55
|
+
app = App()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.route("/orders/{order_id}")
|
|
59
|
+
@auth.require_auth()
|
|
60
|
+
@auth.require_role("admin")
|
|
61
|
+
async def order(request, order_id):
|
|
62
|
+
return {"id": order_id, "user": principal(request).id}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`Principal(id, roles=(), session_id=None, auth_time=0)` is the verifier result.
|
|
66
|
+
Sync and async verifiers are supported. Raise `AuthError` for expected rejection;
|
|
67
|
+
unexpected failures propagate. The principal is stored at `request.scope['wred.auth']`.
|
|
68
|
+
Missing/invalid credentials return 401 with a Bearer challenge; missing roles return
|
|
69
|
+
403. `require_role` authenticates itself if no principal is present.
|
|
70
|
+
|
|
71
|
+
`JWTVerifier(key, algorithms=[...], issuer=..., audience=..., lookup=None)` verifies
|
|
72
|
+
external JWTs. Algorithms are application configuration, never derived from tokens.
|
|
73
|
+
`lookup(claims)` optionally returns a Principal and can be async; without it, the
|
|
74
|
+
verified subject gets no roles. This adapter does not issue tokens or infer accounts.
|
|
75
|
+
|
|
76
|
+
## Account setup
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import os
|
|
80
|
+
from wred import App
|
|
81
|
+
from wred_auth import Auth
|
|
82
|
+
from wred_auth.sqlalchemy import SQLAlchemyStore
|
|
83
|
+
from wred_auth.providers import Google, GitHub
|
|
84
|
+
from wred_ratelimit import Limiter
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def send_email(kind, email, url):
|
|
88
|
+
await application_mailer(kind, email, url) # Your implementation.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
app = App()
|
|
92
|
+
limiter = Limiter.redis(os.environ["REDIS_URL"], namespace="my-app-auth")
|
|
93
|
+
auth = Auth(
|
|
94
|
+
store=SQLAlchemyStore(os.environ["DATABASE_URL"]),
|
|
95
|
+
signing_key=os.environ["AUTH_SIGNING_KEY"],
|
|
96
|
+
encryption_key=os.environ["AUTH_ENCRYPTION_KEY"],
|
|
97
|
+
issuer="my-app",
|
|
98
|
+
audience="my-api",
|
|
99
|
+
public_url="https://api.example.com",
|
|
100
|
+
send_email=send_email,
|
|
101
|
+
limiter=limiter,
|
|
102
|
+
providers={
|
|
103
|
+
"google": Google(
|
|
104
|
+
os.environ["GOOGLE_CLIENT_ID"], os.environ["GOOGLE_CLIENT_SECRET"]
|
|
105
|
+
),
|
|
106
|
+
"github": GitHub(
|
|
107
|
+
os.environ["GITHUB_CLIENT_ID"], os.environ["GITHUB_CLIENT_SECRET"]
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
auth.mount(app)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Use a randomly generated signing key of at least 32 bytes and a separate Fernet
|
|
115
|
+
key (`Fernet.generate_key()`). Keep them stable across workers and restarts.
|
|
116
|
+
Encryption protects persisted TOTP secrets. Key rotation currently requires an
|
|
117
|
+
application migration/re-encryption process; no automatic key ring is provided.
|
|
118
|
+
|
|
119
|
+
Account access tokens use HS256 with explicit issuer/audience and a 15-minute
|
|
120
|
+
lifetime. Refresh sessions expire after 30 days by default, without sliding renewal.
|
|
121
|
+
Every access-token authentication checks account/session state. Refresh rotates a
|
|
122
|
+
hashed opaque token transactionally; reuse revokes that session's entire token family.
|
|
123
|
+
Passwords use Argon2id and must contain 15–128 characters. Email identifiers are
|
|
124
|
+
case-folded; whitespace and malformed basic address structure are rejected.
|
|
125
|
+
|
|
126
|
+
SQLAlchemy supports PostgreSQL and SQLite. Use `postgresql+asyncpg://...` or
|
|
127
|
+
`sqlite+aiosqlite:///...`. PostgreSQL uses row locks and serializable write
|
|
128
|
+
transactions with bounded retries; access-token checks use non-locking repeatable
|
|
129
|
+
read snapshots. There is no store-wide advisory lock. SQLite uses `BEGIN IMMEDIATE`
|
|
130
|
+
for writes and remains intended for local/small deployments.
|
|
131
|
+
|
|
132
|
+
Password hashing and verification run outside database transactions. Password and
|
|
133
|
+
session state are rechecked before committing credentials. `max_password_jobs=2`
|
|
134
|
+
limits concurrent Argon2 jobs per `Auth` instance, with no waiting queue; excess
|
|
135
|
+
work returns 503. Cancellation retains the slot until its thread finishes. Size
|
|
136
|
+
this limit for your process memory/CPU budget (the current Argon2 defaults use
|
|
137
|
+
64 MiB per operation), and normally share one `Auth` instance per worker. Redis
|
|
138
|
+
quotas limit requests across workers; they do not replace this process-local
|
|
139
|
+
resource limit. Invalid reset tokens are rejected before password hashing.
|
|
140
|
+
|
|
141
|
+
Both stores accept `transaction_timeout=5` seconds and `retries=5` by default.
|
|
142
|
+
Retries cover database conflicts, not password hashing, email, or audit callbacks.
|
|
143
|
+
Deadline/retry exhaustion returns 503 through account HTTP endpoints. SQLAlchemy
|
|
144
|
+
engine options and PyMongo client options can be supplied to their store constructors.
|
|
145
|
+
|
|
146
|
+
## MongoDB
|
|
147
|
+
|
|
148
|
+
Install `pip install 'wred-auth[mongodb]'`. Use MongoDB 6.0+ as a replica set or
|
|
149
|
+
sharded cluster, including Atlas; standalone servers cannot provide the required
|
|
150
|
+
multi-document transactions. Use PyMongo's async driver, not Motor.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from wred_auth.mongodb import MongoDBStore
|
|
154
|
+
|
|
155
|
+
store = MongoDBStore(os.environ["MONGODB_URL"], database="accounts")
|
|
156
|
+
await store.initialize() # Explicit deployment step: creates collections/indexes.
|
|
157
|
+
# Pass store=store to Auth, with the same remaining configuration as above.
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`initialize()` is idempotent and must complete before serving traffic. It checks
|
|
161
|
+
the topology and creates unique email and provider/subject indexes. Transactions
|
|
162
|
+
use primary reads, snapshot read concern, and majority writes. Conflicting writes
|
|
163
|
+
are retried; read-only access checks do not touch documents. MongoDB uses the same
|
|
164
|
+
account fields as SQL and separate `wred_auth_*` collections. Keep those collections
|
|
165
|
+
unsharded unless you design shard keys and uniqueness constraints for your deployment.
|
|
166
|
+
|
|
167
|
+
## Explicit migrations
|
|
168
|
+
|
|
169
|
+
Create an application `alembic.ini`:
|
|
170
|
+
|
|
171
|
+
```ini
|
|
172
|
+
[alembic]
|
|
173
|
+
script_location = wred_auth:migrations
|
|
174
|
+
sqlalchemy.url = sqlite+aiosqlite:///accounts.db
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Then run `alembic -c alembic.ini upgrade head`. No schema is created during requests.
|
|
178
|
+
Revision `0001` creates users, sessions, tokens, and identities; `0002` adds live-record
|
|
179
|
+
and expiration indexes. Upgrade existing SQL installations before deploying this version.
|
|
180
|
+
Account tables are separate from application domain tables. Custom storage adapters
|
|
181
|
+
must now implement `run(callback, read_only=False)`, `cleanup`, filtered/paginated
|
|
182
|
+
reads, and bulk updates in addition to the transaction API. Callbacks can execute
|
|
183
|
+
more than once and must contain only transactional database work. See the contract
|
|
184
|
+
in `wred_auth.storage` and the bundled implementations. A context-managed
|
|
185
|
+
`store.transaction()` remains available for application code, but does not retry
|
|
186
|
+
its body automatically; use `run()` for conflict retries.
|
|
187
|
+
|
|
188
|
+
## Retention
|
|
189
|
+
|
|
190
|
+
Schedule cleanup in an application maintenance job; it is never run during requests:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
counts = await auth.cleanup(batch_size=500)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Each call removes at most 500 expired tokens and 500 expired sessions using expiration
|
|
197
|
+
indexes. Run additional batches with a job time budget when a backlog exists.
|
|
198
|
+
Used refresh tokens and revoked sessions are retained until expiry, so cleanup does
|
|
199
|
+
not erase replay detection for live refresh families. Revocation uses bulk database
|
|
200
|
+
updates. Account/user and external-identity records are not deleted by cleanup.
|
|
201
|
+
|
|
202
|
+
## HTTP endpoints
|
|
203
|
+
|
|
204
|
+
`auth.mount(app, prefix='/auth', rule='10/minute', account_rule='5/minute', key=None)` is opt-in.
|
|
205
|
+
It rejects existing routes. A configured limiter is required. All operations return
|
|
206
|
+
JSON with `Cache-Control: no-store`; requests use JSON bodies except GET operations.
|
|
207
|
+
Service methods also exist, but callers of those methods must apply their own rate
|
|
208
|
+
limits and authorization. `Principal` arguments are trusted application inputs.
|
|
209
|
+
|
|
210
|
+
With `key=None`, mount honors the configured `Limiter(key=...)`. Supply `key=...`
|
|
211
|
+
to override it for account routes; both synchronous and asynchronous functions work.
|
|
212
|
+
The default limiter uses the ASGI peer address. Behind a reverse proxy, configure
|
|
213
|
+
your server's trusted proxy handling to populate the real client address, or provide
|
|
214
|
+
a key from trusted middleware. Never trust arbitrary forwarded headers from clients.
|
|
215
|
+
Use Redis and a shared namespace across workers; the in-memory limiter is process-local.
|
|
216
|
+
|
|
217
|
+
| Method/path under `/auth` | Input |
|
|
218
|
+
|---|---|
|
|
219
|
+
| POST `/register` | `email`, `password` |
|
|
220
|
+
| POST `/verify` | `token` |
|
|
221
|
+
| POST `/request-verification` | `email` |
|
|
222
|
+
| POST `/login` | `email`, `password` |
|
|
223
|
+
| POST `/forgot-password` | `email` |
|
|
224
|
+
| POST `/reset-password` | `token`, `password` |
|
|
225
|
+
| POST `/refresh` | `refresh_token` |
|
|
226
|
+
| POST `/logout` | Bearer credentials, empty object |
|
|
227
|
+
| GET `/sessions` | Bearer credentials; optional `limit` (1–1000, default 100), `after` session ID |
|
|
228
|
+
| POST `/revoke` | Bearer credentials, `session_id` |
|
|
229
|
+
| POST `/change-password` | Bearer credentials, `old_password`, `password` |
|
|
230
|
+
| POST `/mfa-setup` | Recent Bearer credentials, empty object |
|
|
231
|
+
| POST `/mfa-confirm` | Recent Bearer credentials, `code` |
|
|
232
|
+
| POST `/mfa-login` | `challenge`, `code` |
|
|
233
|
+
| POST `/mfa-disable` | Recent Bearer credentials, `code` |
|
|
234
|
+
| GET `/oauth/{provider}/start` | Browser navigation |
|
|
235
|
+
| POST `/oauth/{provider}/link` | Recent Bearer credentials |
|
|
236
|
+
| GET `/oauth/{provider}/callback` | Provider callback query and browser cookie |
|
|
237
|
+
|
|
238
|
+
Registration/recovery return generic responses. Mail delivery is an async callback
|
|
239
|
+
`send_email(kind, email, url)`, where kind is `verify` or `reset`. The URL points to
|
|
240
|
+
`public_url/{kind}?token=...`; your frontend handles it and POSTs the token to the API.
|
|
241
|
+
Delivery failures propagate; users can request new verification/reset instructions.
|
|
242
|
+
Verification/reset tokens expire in 30 minutes. Password changes and resets revoke
|
|
243
|
+
existing sessions, pending MFA challenges, and outstanding password-reset tokens.
|
|
244
|
+
Session pages are ordered by ID; pass the last returned ID as `after` for the next page.
|
|
245
|
+
|
|
246
|
+
Successful password login returns bearer and refresh tokens, or an MFA challenge
|
|
247
|
+
that expires in five minutes. MFA setup returns a provisioning URI; confirmation
|
|
248
|
+
returns ten one-use recovery codes and revokes existing sessions. Used TOTP time
|
|
249
|
+
steps cannot be replayed, with one adjacent step of clock tolerance. MFA changes
|
|
250
|
+
and identity linking require authentication within five minutes; sign in again when
|
|
251
|
+
prompted. Password recovery does not disable MFA.
|
|
252
|
+
|
|
253
|
+
Google uses OIDC issuer/audience/signature/nonce checks. GitHub requires a verified
|
|
254
|
+
primary email. OAuth uses state, PKCE, and a Secure/HttpOnly/SameSite browser-binding
|
|
255
|
+
cookie. Register exact callbacks at `public_url/auth/oauth/{provider}/callback`.
|
|
256
|
+
Callback results return JSON tokens, never credentials in redirect URLs. No provider
|
|
257
|
+
refresh tokens are retained. Existing accounts are never merged by email: users must
|
|
258
|
+
sign in and explicitly link. One browser login flow at a time is supported.
|
|
259
|
+
|
|
260
|
+
Administrative `set_roles(user_id, roles)` and `set_active(user_id, active)` methods
|
|
261
|
+
are intentionally not mounted as public endpoints. Applications enforce administrator
|
|
262
|
+
permissions before invoking them. Optional `audit(event)` receives event, user ID,
|
|
263
|
+
and timestamp; it must be async and must not fail during ordinary operation.
|
|
264
|
+
|
|
265
|
+
Use `async with auth` or `await auth.aclose()` to close its store and limiter.
|
|
266
|
+
Handle lifespan in the hosting application. Configure request body limits at the
|
|
267
|
+
server/proxy; Wred buffers bodies. Credentials, mail, DNS, TLS, and live provider
|
|
268
|
+
configuration belong to the application.
|
|
269
|
+
|
|
270
|
+
## With wred-pydantic and wred-ratelimit
|
|
271
|
+
|
|
272
|
+
`wred-ratelimit` supplies `Limiter` for request quotas; install its `redis` extra
|
|
273
|
+
to share quotas across workers. `wred-pydantic` supplies the optional `body` and
|
|
274
|
+
`response` decorators for input validation and response conversion.
|
|
275
|
+
|
|
276
|
+
```sh
|
|
277
|
+
pip install wred-auth 'wred-ratelimit[redis]' wred-pydantic
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
This example defines its own app, limiter, and auth instance using external RS256
|
|
281
|
+
JWTs. Configure `REDIS_URL`, `AUTH_PUBLIC_KEY` (a PEM public key), `AUTH_ISSUER`, and
|
|
282
|
+
`AUTH_AUDIENCE` in the environment. For the built-in account service, reuse the
|
|
283
|
+
`app`, `auth`, and `limiter` from Account setup instead of creating them again.
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
import os
|
|
287
|
+
|
|
288
|
+
from pydantic import BaseModel, Field
|
|
289
|
+
from wred import App
|
|
290
|
+
from wred_auth import Auth, JWTVerifier, principal
|
|
291
|
+
from wred_pydantic import body, response
|
|
292
|
+
from wred_ratelimit import Limiter
|
|
293
|
+
|
|
294
|
+
app = App()
|
|
295
|
+
limiter = Limiter.redis(os.environ["REDIS_URL"], namespace="my-app-auth")
|
|
296
|
+
auth = Auth(
|
|
297
|
+
verifier=JWTVerifier(
|
|
298
|
+
os.environ["AUTH_PUBLIC_KEY"],
|
|
299
|
+
algorithms=["RS256"],
|
|
300
|
+
issuer=os.environ["AUTH_ISSUER"],
|
|
301
|
+
audience=os.environ["AUTH_AUDIENCE"],
|
|
302
|
+
),
|
|
303
|
+
limiter=limiter,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
class OrderInput(BaseModel):
|
|
307
|
+
quantity: int = Field(gt=0)
|
|
308
|
+
|
|
309
|
+
class OrderOutput(BaseModel):
|
|
310
|
+
user: str
|
|
311
|
+
quantity: int
|
|
312
|
+
|
|
313
|
+
@app.route("/orders", methods=["POST"])
|
|
314
|
+
@limiter.limit("30/minute")
|
|
315
|
+
@auth.require_auth()
|
|
316
|
+
@body(OrderInput, max_bytes=4096)
|
|
317
|
+
@response(OrderOutput)
|
|
318
|
+
async def create_order(request, body):
|
|
319
|
+
return {"user": principal(request).id, "quantity": body.quantity}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Auth decorators preserve injected keyword arguments and OpenAPI metadata.
|
|
323
|
+
Close `auth` during application shutdown with `await auth.aclose()`; it also closes
|
|
324
|
+
the supplied limiter. Authentication, authorization, validation, and quota failures
|
|
325
|
+
retain their HTTP statuses; the response adapter does not validate non-success payloads. All three extensions
|
|
326
|
+
support Wred 0.13 together with their current dependency constraints.
|
|
327
|
+
|
|
328
|
+
## Tests
|
|
329
|
+
|
|
330
|
+
```sh
|
|
331
|
+
pip install -e '.[test,sqlalchemy,mongodb]' '../wred-ratelimit[redis]' '../wred-pydantic'
|
|
332
|
+
pytest
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
SQLite tests always run. Set `WRED_TEST_POSTGRES` to an isolated PostgreSQL test
|
|
336
|
+
DB to exercise migrations and concurrent refresh. Tests downgrade their account
|
|
337
|
+
tables after each scenario; never point them at application data. Provider tests
|
|
338
|
+
use signed OIDC responses and simulated HTTP exchanges, not live provider accounts.
|
|
339
|
+
Set `WRED_TEST_MONGODB` to a test replica-set URI to run the same account contract
|
|
340
|
+
against MongoDB (each test creates and drops a uniquely named database).
|
|
341
|
+
Set `WRED_TEST_REDIS` to a test Redis URI for shared-quota integration tests.
|
|
342
|
+
Coverage includes transaction rollback/deadlines, independent account progress,
|
|
343
|
+
hash admission/cancellation, password races, retention, and decorator composition.
|
|
344
|
+
|
|
345
|
+
For a repeatable multi-process service benchmark against a disposable database:
|
|
346
|
+
|
|
347
|
+
```sh
|
|
348
|
+
WRED_BENCH_URL='postgresql+asyncpg://...' python benchmarks/auth_load.py sql
|
|
349
|
+
WRED_BENCH_URL='mongodb://...?replicaSet=rs0' python benchmarks/auth_load.py mongodb
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The default workload runs 1,000 operations across two processes: 90% access-token
|
|
353
|
+
checks and 10% password logins. It reports latency percentiles and every error/503,
|
|
354
|
+
and excludes HTTP/proxy, Redis, email, and provider latency. SQL runs leave seeded
|
|
355
|
+
accounts in the supplied disposable database; MongoDB runs remove their uniquely
|
|
356
|
+
named benchmark database. Tune `--workers`, `--concurrency`, `--password-jobs`,
|
|
357
|
+
`--users`, and `--requests` for your workload. This is a local capacity probe, not
|
|
358
|
+
a production SLO or failover certification.
|
|
359
|
+
|