py-auth-core 0.0.1__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2026] [Olatunji Jamaldeen]
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,463 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-auth-core
3
+ Version: 0.0.1
4
+ Summary: Core auth primitives and provider framework for py-auth-core.
5
+ Author-email: Olatunji Jamaldeen Omotoyosi <jamaldeen.o@yahoo.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jamaldeen09/py-auth
8
+ Project-URL: Repository, https://github.com/jamaldeen09/py-auth
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: pydantic<3.0.0,>=2.0.0
21
+ Provides-Extra: postgres
22
+ Requires-Dist: asyncpg>=0.28.0; extra == "postgres"
23
+ Provides-Extra: mysql
24
+ Requires-Dist: aiomysql>=0.2.0; extra == "mysql"
25
+ Provides-Extra: sqlite
26
+ Requires-Dist: aiosqlite>=0.19.0; extra == "sqlite"
27
+ Dynamic: license-file
28
+
29
+ # py-auth-core
30
+
31
+ **Modular, framework-agnostic authentication primitives for Python backends.**
32
+
33
+ `py-auth-core` gives you secure session management, credential-based sign-in, CSRF protection, and a clean provider/adapter architecture — without forcing a specific ORM, web framework, or database on you.
34
+
35
+ [![PyPI version](https://img.shields.io/pypi/v/py-auth-core.svg)](https://pypi.org/project/py-auth-core/)
36
+ [![Python versions](https://img.shields.io/pypi/pyversions/py-auth-core.svg)](https://pypi.org/project/py-auth-core/)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
38
+
39
+ ---
40
+
41
+ ## Table of Contents
42
+
43
+ - [Features](#features)
44
+ - [Installation](#installation)
45
+ - [Quick Start](#quick-start)
46
+ - [Core Concepts](#core-concepts)
47
+ - [PyAuth](#pyauth-1)
48
+ - [Providers](#providers)
49
+ - [Adapters](#adapters)
50
+ - [Cookies](#cookies)
51
+ - [API Reference](#api-reference)
52
+ - [PyAuth class](#pyauth-class)
53
+ - [CredentialsProvider](#credentialsprovider)
54
+ - [BaseProvider](#baseprovider)
55
+ - [Schemas & TypedDicts](#schemas--typeddicts)
56
+ - [Exceptions](#exceptions)
57
+ - [Integrations](#integrations)
58
+ - [Available Adapters](#available-adapters)
59
+ - [Roadmap](#roadmap)
60
+ - [Security Notes](#security-notes)
61
+ - [Contributing](#contributing)
62
+ - [License](#license)
63
+
64
+ ---
65
+
66
+ ## Features
67
+
68
+ - ✅ **Async-first** — every auth operation is a coroutine
69
+ - ✅ **Provider pattern** — plug in `CredentialsProvider` or use an upcoming provider
70
+ - ✅ **Adapter pattern** — swap the database layer without touching auth logic
71
+ - ✅ **Secure by default** — SHA-256 session-token hashing, `httpOnly` + `Secure` cookies, CSRF protection via `hmac.compare_digest`
72
+ - ✅ **Pydantic v2** request validation built-in
73
+ - ✅ **Framework-agnostic** — works with FastAPI, Starlette, Django, Flask, or any async Python backend
74
+ - ✅ **Typed throughout** — ships a `py.typed` marker; full TypedDict / Protocol coverage
75
+
76
+ ---
77
+
78
+ ## Installation
79
+
80
+ ```bash
81
+ pip install py-auth-core
82
+ ```
83
+
84
+ `py-auth-core` requires **Python ≥ 3.9** and **Pydantic ≥ 2.0**.
85
+
86
+ ---
87
+
88
+ ## Quick Start
89
+
90
+ Below is a minimal example using `py-auth-core` directly. If you're on **FastAPI**, see [Integrations](#integrations) — the official integration reduces this to a single line.
91
+
92
+ ```python
93
+ from pydantic import BaseModel, EmailStr
94
+ from py_auth import PyAuth, CredentialsProvider
95
+
96
+
97
+ # 1. Define your credentials schema (Pydantic v2)
98
+ class LoginSchema(BaseModel):
99
+ email: EmailStr
100
+ password: str
101
+
102
+
103
+ # 2. Implement your authorization callback
104
+ async def authorize(credentials: dict) -> dict | None:
105
+ """Check if the user exists — create them if not. Return None to reject."""
106
+ user = await db.find_user_by_email(credentials["email"])
107
+
108
+ if user:
109
+ # Existing user — verify their password
110
+ if not verify_password(credentials["password"], user.hashed_password):
111
+ return None
112
+ return {"id": str(user.id), "email": user.email, "name": user.name}
113
+
114
+ # New user — create them and return their details
115
+ new_user = await db.create_user(
116
+ email=credentials["email"],
117
+ hashed_password=hash_password(credentials["password"]),
118
+ )
119
+ return {"id": str(new_user.id), "email": new_user.email, "name": new_user.name}
120
+
121
+
122
+ # 3. Wire everything together
123
+ credentials_provider = CredentialsProvider(model=LoginSchema, authorize=authorize)
124
+
125
+ auth = PyAuth(
126
+ adapter=my_adapter, # any PyAuthAdapterProtocol-compliant adapter
127
+ providers=[credentials_provider],
128
+ )
129
+ ```
130
+
131
+ Once `auth` is set up, use it in your route handlers:
132
+
133
+ ```python
134
+ # Sign in
135
+ result = await auth.signin_with_credentials(request_body)
136
+
137
+ # Verify an active session
138
+ result = await auth.verify_session(session_token, csrf_token)
139
+
140
+ # Sign out
141
+ result = await auth.signout(session_id)
142
+ ```
143
+
144
+ Every method returns an `AuthResult` — a plain dict with `data` and `error` keys. Check `result["error"]` first; if it's `None` the operation succeeded.
145
+
146
+ ---
147
+
148
+ ## Core Concepts
149
+
150
+ ### PyAuth
151
+
152
+ `PyAuth` is the central manager. It holds your adapter and providers and exposes async methods for every auth flow.
153
+
154
+ ```
155
+ PyAuth
156
+ ├── adapter ← talks to your database
157
+ ├── providers ← one or more auth strategies
158
+ └── cookies ← merged cookie configuration
159
+ ```
160
+
161
+ ### Providers
162
+
163
+ A **provider** encapsulates a single authentication strategy. `py-auth-core` ships with one built-in provider today, with more on the way:
164
+
165
+ | Provider | Status | Description |
166
+ |---|---|---|
167
+ | `CredentialsProvider` | ✅ Available | Field-based sign-in (email/password, etc.) via a Pydantic model + async callback |
168
+ | `GoogleProvider` | 🔜 Coming soon | Google OAuth 2.0 |
169
+ | `GithubProvider` | 🔜 Coming soon | GitHub OAuth |
170
+ | `EmailProvider` | 🔜 Coming soon | Passwordless magic-link sign-in |
171
+
172
+ ### Adapters
173
+
174
+ An **adapter** is any object that satisfies `PyAuthAdapterProtocol`. It handles all database I/O: creating sessions, updating sessions and looking up / deleting sessions.
175
+
176
+ `py-auth-core` validates your adapter at startup using a structural `Protocol` check — you'll get a clear `ConfigurationError` immediately if a required method is missing, rather than a cryptic failure later.
177
+
178
+ See [Available Adapters](#available-adapters) for ready-made options.
179
+
180
+ ### Cookies
181
+
182
+ `py-auth-core` manages two cookies:
183
+
184
+ | Cookie | Default name | Purpose |
185
+ |---|---|---|
186
+ | Session token | `__Host-py_auth_session` | Authenticates the session — `httpOnly`, `Secure`, `SameSite=lax` |
187
+ | CSRF token | `py_auth_csrf` | Double-submit CSRF protection — JavaScript-readable (no `httpOnly`) |
188
+
189
+ Defaults are environment-aware: `secure=True` is always enforced when `ENVIRONMENT=production`. Override any value via `PyAuthCookiesInput`:
190
+
191
+ ```python
192
+ from py_auth import PyAuth, PyAuthCookiesInput, CookieConfig, CookieOptions
193
+
194
+ auth = PyAuth(
195
+ adapter=my_adapter,
196
+ providers=[credentials_provider],
197
+ cookies=PyAuthCookiesInput(
198
+ session_token=CookieConfig(
199
+ name="my_session",
200
+ options=CookieOptions(max_age=7 * 24 * 60 * 60), # 7 days
201
+ )
202
+ ),
203
+ )
204
+ ```
205
+
206
+ ---
207
+
208
+ ## API Reference
209
+
210
+ ### `PyAuth` class
211
+
212
+ ```python
213
+ PyAuth(
214
+ adapter: PyAuthAdapterProtocol,
215
+ providers: list[BaseProvider] | None = None,
216
+ cookies: PyAuthCookiesInput | None = None,
217
+ )
218
+ ```
219
+
220
+ **Attributes**
221
+
222
+ | Attribute | Type | Description |
223
+ |---|---|---|
224
+ | `adapter` | `PyAuthAdapterProtocol` | The validated adapter instance |
225
+ | `cookies` | `dict[str, dict]` | Merged cookie config (name + options per token) |
226
+
227
+ ---
228
+
229
+ #### `await auth.signin_with_credentials(request_body: dict) -> AuthResult`
230
+
231
+ Validates `request_body` with the `CredentialsProvider`'s Pydantic model, calls your `authorize` callback, creates a session, and returns tokens.
232
+
233
+ ```python
234
+ result = await auth.signin_with_credentials({"email": "...", "password": "..."})
235
+ # Success:
236
+ # result["data"] = {"session_token": "...", "csrf_token": "...", "user": {...}}
237
+ # result["error"] = None
238
+ #
239
+ # Failure:
240
+ # result["data"] = None
241
+ # result["error"] = {"code": "CredentialsSignIn", "status_code": 401, "message": "..."}
242
+ ```
243
+
244
+ ---
245
+
246
+ #### `await auth.verify_session(session_token: str, csrf_token: str) -> AuthResult`
247
+
248
+ Hashes the session token, fetches the session from the adapter, checks expiry, and validates the CSRF token with `hmac.compare_digest`.
249
+
250
+ ```python
251
+ result = await auth.verify_session(session_token, csrf_token)
252
+ # Success: result["data"] = {"session": {...}}
253
+ # Failure: result["error"] = {"code": "SessionExpired" | "InvalidCsrfToken" | ..., ...}
254
+ ```
255
+
256
+ ---
257
+
258
+ #### `await auth.signout(session_id: str) -> AuthResult`
259
+
260
+ Deletes the session identified by `session_id`.
261
+
262
+ ```python
263
+ result = await auth.signout(session_id)
264
+ # result["data"] = {"signed_out": True}
265
+ ```
266
+
267
+ ---
268
+
269
+ #### `auth.get_auth_result(data=None, error=None) -> AuthResult`
270
+
271
+ Utility to build a standardised `AuthResult`. Useful in custom middleware or route guards.
272
+
273
+ ---
274
+
275
+ ### `CredentialsProvider`
276
+
277
+ ```python
278
+ CredentialsProvider(
279
+ model: Type[BaseModel],
280
+ authorize: Callable[[dict], Any] | Callable[[dict], Awaitable[Any]],
281
+ )
282
+ ```
283
+
284
+ | Parameter | Type | Description |
285
+ |---|---|---|
286
+ | `model` | `Type[BaseModel]` | Pydantic v2 model — the request body is validated against this before `authorize` is called |
287
+ | `authorize` | sync or async callable | Receives the validated payload as a plain `dict`. Return a truthy user dict on success, or `None` / falsy to trigger a `401` |
288
+
289
+ **Validation errors** are automatically serialised into a structured `422` response:
290
+
291
+ ```json
292
+ {
293
+ "error": {
294
+ "code": "ValidationError",
295
+ "status_code": 422,
296
+ "message": "Validation failed.",
297
+ "details": {
298
+ "validation_errors": [
299
+ {"field": "email", "errors": ["value is not a valid email address"]}
300
+ ]
301
+ }
302
+ }
303
+ }
304
+ ```
305
+
306
+ ---
307
+
308
+ ### `BaseProvider`
309
+
310
+ Abstract base class for all providers. Every provider that ships with `py-auth` extends this class. The `id` attribute is automatically derived from the class name (lowercased, with `"provider"` stripped) — e.g. `CredentialsProvider` → `"credentials"`.
311
+
312
+ ```python
313
+ from py_auth import BaseProvider, AuthResult
314
+
315
+
316
+ class MyProvider(BaseProvider):
317
+ async def handle_request(self, *args, **kwargs) -> AuthResult: ...
318
+ ```
319
+
320
+ ---
321
+
322
+ ### Schemas & TypedDicts
323
+
324
+ #### `AuthResult`
325
+ ```python
326
+ class AuthResult(TypedDict):
327
+ data: Any | None
328
+ error: AuthError | None
329
+ ```
330
+
331
+ #### `AuthError`
332
+ ```python
333
+ class AuthError(TypedDict, total=False):
334
+ code: str # machine-readable, e.g. "InvalidSessionToken"
335
+ status_code: int # HTTP status to send to the client
336
+ message: str # human-readable description
337
+ details: dict # optional structured detail (e.g. validation errors)
338
+ ```
339
+
340
+ #### `PyAuthAdapterProtocol`
341
+ ```python
342
+ class PyAuthAdapterProtocol(Protocol):
343
+ async def create_session(self, session_data: dict) -> dict: ...
344
+ async def get_session_by_session_token_hash(
345
+ self, token_hash: str
346
+ ) -> dict | None: ...
347
+ async def delete_session_by_session_token_hash(self, token_hash: str) -> None: ...
348
+ async def delete_session(self, session_id: str) -> None: ...
349
+ async def update_session(
350
+ self, session_id: str, updates: Dict
351
+ ) -> dict | None: ...
352
+ ```
353
+
354
+ > The adapter only manages sessions. User lookup and creation live entirely inside your
355
+ > `authorize()` callback — giving you full control over hashing, validation, and
356
+ > any other user-creation logic your app needs.
357
+
358
+ #### `CookieOptions`
359
+ ```python
360
+ class CookieOptions(BaseModel):
361
+ http_only: bool | None = None
362
+ secure: bool | None = None
363
+ same_site: Literal["lax", "strict", "none"] | None = None
364
+ path: str | None = None
365
+ domain: str | None = None
366
+ max_age: int | None = None # seconds
367
+ expires: datetime | None = None
368
+ ```
369
+
370
+ ---
371
+
372
+ ### Exceptions
373
+
374
+ All exceptions inherit from `PyAuthError` and carry a `status_code` attribute for easy HTTP mapping.
375
+
376
+ | Exception | Default `status_code` | When raised |
377
+ |---|---|---|
378
+ | `PyAuthError` | `500` | Base class; general catch-all |
379
+ | `ConfigurationError` | `500` | Adapter missing required methods, or provider not configured |
380
+ | `AdapterError` | `500` | Database engine setup failure |
381
+ | `DuplicateEntryError` | `409` | Unique constraint violation (e.g. duplicate session token) |
382
+ | `ForeignKeyViolationError` | `400` | Foreign key violation (e.g. referenced user no longer exists) |
383
+ | `RecordNotFoundError` | `404` | Requested record not found |
384
+
385
+ ---
386
+
387
+ ## Integrations
388
+
389
+ ### FastAPI — `py-auth-fastapi`
390
+
391
+ The official FastAPI integration is a separate package that removes all the boilerplate of wiring `py-auth-core` into a FastAPI app. **It's a single line.**
392
+
393
+ You still configure the pieces you own — your providers and your `PyAuth` instance — and the integration handles everything else internally: mounting the auth routes, setting and reading cookies, and returning the right HTTP responses.
394
+
395
+ ```python
396
+ # You set up your PyAuth instance as normal...
397
+ auth = PyAuth(adapter=my_adapter, providers=[credentials_provider])
398
+
399
+ # ...then hand it to the integration. That's it.
400
+ app.include_router(PyAuthFastAPI(auth), prefix="/auth", tags=["Authentication"])
401
+ ```
402
+
403
+ The integration exposes ready-made routes for sign-in, session verification, and sign-out — no manual cookie handling, no manual response construction.
404
+
405
+ ```bash
406
+ pip install py-auth-fastapi
407
+ ```
408
+
409
+ ---
410
+
411
+ ## Available Adapters
412
+
413
+ | Package | Supported Databases | Install |
414
+ |---|---|---|
415
+ | [`py-auth-sqlalchemy`](https://pypi.org/project/py-auth-sqlalchemy/) | PostgreSQL (`asyncpg`), MySQL (`aiomysql`), SQLite (`aiosqlite`) | `pip install py-auth-sqlalchemy` |
416
+
417
+ > More adapters (Tortoise ORM, Motor/MongoDB, Beanie, etc.) are on the roadmap. Community contributions are welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md).
418
+
419
+ ---
420
+
421
+ ## Roadmap
422
+
423
+ `py-auth-core` is in early release (`0.0.1`). Here's what's planned:
424
+
425
+ **Providers**
426
+ - [ ] `GoogleProvider` — Google OAuth 2.0
427
+ - [ ] `GithubProvider` — GitHub OAuth
428
+ - [ ] `EmailProvider` — passwordless magic-link sign-in
429
+
430
+ **Integrations**
431
+ - [x] `py-auth-fastapi` — FastAPI integration
432
+ - [ ] `py-auth-django` — Django integration
433
+ - [ ] `py-auth-flask` — Flask / Quart integration
434
+ - [ ] `py-auth-litestar` — Litestar integration
435
+
436
+ **Adapters**
437
+ - [ ] Tortoise ORM adapter
438
+ - [ ] Motor (async MongoDB) adapter
439
+ - [ ] Beanie adapter
440
+
441
+ These will land as the project gains traction. If you'd like to see something added sooner, open an issue or a PR on [GitHub](https://github.com/jamaldeen09/py-auth).
442
+
443
+ ---
444
+
445
+ ## Security Notes
446
+
447
+ - **Session tokens are never stored in plain text.** Only a SHA-256 hex digest is persisted; the raw token lives only in the client cookie.
448
+ - **CSRF validation uses `hmac.compare_digest`** — immune to timing attacks.
449
+ - **Cookie defaults follow the `__Host-` prefix convention** for session cookies: `Secure`, `httpOnly`, `Path=/`, no explicit `Domain`. This provides the strongest possible same-origin binding.
450
+ - The **CSRF cookie intentionally omits `httpOnly`** so your frontend can read it and attach it as a request header for server-side comparison.
451
+ - In `ENVIRONMENT=production`, the `secure` flag is always forced to `True` on every cookie regardless of user configuration.
452
+
453
+ ---
454
+
455
+ ## Contributing
456
+
457
+ Want to build a new provider, adapter, or integration? See [CONTRIBUTING.md](./CONTRIBUTING.md) for architecture guidelines, how the adapter protocol works, and how to get started.
458
+
459
+ ---
460
+
461
+ ## License
462
+
463
+ MIT — see [LICENSE](./LICENSE) for details.