Skip to main content

py-auth-core

Modular, framework-agnostic authentication primitives for Python backends.

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.

PyPI version Python versions License: MIT


Table of Contents


Features

  • Async-first — every auth operation is a coroutine
  • Provider pattern — plug in CredentialsProvider or use an upcoming provider
  • Adapter pattern — swap the database layer without touching auth logic
  • Secure by default — SHA-256 session-token hashing, httpOnly + Secure cookies, CSRF protection via hmac.compare_digest
  • Pydantic v2 request validation built-in
  • Framework-agnostic — works with FastAPI, Starlette, Django, Flask, or any async Python backend
  • Typed throughout — ships a py.typed marker; full TypedDict / Protocol coverage

Installation

pip install py-auth-core

py-auth-core requires Python ≥ 3.9 and Pydantic ≥ 2.0.


Quick Start

Below is a minimal example using py-auth-core directly. If you're on FastAPI, see Integrations — the official integration reduces this to a single line.

from pydantic import BaseModel, EmailStr
from py_auth import PyAuth, CredentialsProvider


# 1. Define your credentials schema (Pydantic v2)
class LoginSchema(BaseModel):
    email: EmailStr
    password: str


# 2. Implement your authorization callback
async def authorize(credentials: dict) -> dict | None:
    """Check if the user exists — create them if not. Return None to reject."""
    user = await db.find_user_by_email(credentials["email"])

    if user:
        # Existing user — verify their password
        if not verify_password(credentials["password"], user.hashed_password):
            return None
        return {"id": str(user.id), "email": user.email, "name": user.name}

    # New user — create them and return their details
    new_user = await db.create_user(
        email=credentials["email"],
        hashed_password=hash_password(credentials["password"]),
    )
    return {"id": str(new_user.id), "email": new_user.email, "name": new_user.name}


# 3. Wire everything together
credentials_provider = CredentialsProvider(model=LoginSchema, authorize=authorize)

auth = PyAuth(
    adapter=my_adapter,  # any PyAuthAdapterProtocol-compliant adapter
    providers=[credentials_provider],
)

Once auth is set up, use it in your route handlers:

# Sign in
result = await auth.signin_with_credentials(request_body)

# Verify an active session
result = await auth.verify_session(session_token, csrf_token)

# Sign out
result = await auth.signout(session_id)

Every method returns an AuthResult — a plain dict with data and error keys. Check result["error"] first; if it's None the operation succeeded.


Core Concepts

PyAuth

PyAuth is the central manager. It holds your adapter and providers and exposes async methods for every auth flow.

PyAuth
 ├── adapter          ← talks to your database
 ├── providers        ← one or more auth strategies
 └── cookies          ← merged cookie configuration

Providers

A provider encapsulates a single authentication strategy. py-auth-core ships with one built-in provider today, with more on the way:

Provider Status Description
CredentialsProvider ✅ Available Field-based sign-in (email/password, etc.) via a Pydantic model + async callback
GoogleProvider 🔜 Coming soon Google OAuth 2.0
GithubProvider 🔜 Coming soon GitHub OAuth
EmailProvider 🔜 Coming soon Passwordless magic-link sign-in

Adapters

An adapter is any object that satisfies PyAuthAdapterProtocol. It handles all database I/O: creating sessions, updating sessions and looking up / deleting sessions.

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.

See Available Adapters for ready-made options.

Cookies

py-auth-core manages two cookies:

Cookie Default name Purpose
Session token __Host-py_auth_session Authenticates the session — httpOnly, Secure, SameSite=lax
CSRF token py_auth_csrf Double-submit CSRF protection — JavaScript-readable (no httpOnly)

Defaults are environment-aware: secure=True is always enforced when ENVIRONMENT=production. Override any value via PyAuthCookiesInput:

from py_auth import PyAuth, PyAuthCookiesInput, CookieConfig, CookieOptions

auth = PyAuth(
    adapter=my_adapter,
    providers=[credentials_provider],
    cookies=PyAuthCookiesInput(
        session_token=CookieConfig(
            name="my_session",
            options=CookieOptions(max_age=7 * 24 * 60 * 60),  # 7 days
        )
    ),
)

API Reference

PyAuth class

PyAuth(
    adapter: PyAuthAdapterProtocol,
    providers: list[BaseProvider] | None = None,
    cookies: PyAuthCookiesInput | None = None,
)

Attributes

Attribute Type Description
adapter PyAuthAdapterProtocol The validated adapter instance
cookies dict[str, dict] Merged cookie config (name + options per token)

await auth.signin_with_credentials(request_body: dict) -> AuthResult

Validates request_body with the CredentialsProvider's Pydantic model, calls your authorize callback, creates a session, and returns tokens.

result = await auth.signin_with_credentials({"email": "...", "password": "..."})
# Success:
# result["data"] = {"session_token": "...", "csrf_token": "...", "user": {...}}
# result["error"] = None
#
# Failure:
# result["data"] = None
# result["error"] = {"code": "CredentialsSignIn", "status_code": 401, "message": "..."}

await auth.verify_session(session_token: str, csrf_token: str) -> AuthResult

Hashes the session token, fetches the session from the adapter, checks expiry, and validates the CSRF token with hmac.compare_digest.

result = await auth.verify_session(session_token, csrf_token)
# Success:  result["data"] = {"session": {...}}
# Failure:  result["error"] = {"code": "SessionExpired" | "InvalidCsrfToken" | ..., ...}

await auth.signout(session_id: str) -> AuthResult

Deletes the session identified by session_id.

result = await auth.signout(session_id)
# result["data"] = {"signed_out": True}

auth.get_auth_result(data=None, error=None) -> AuthResult

Utility to build a standardised AuthResult. Useful in custom middleware or route guards.


CredentialsProvider

CredentialsProvider(
    model: Type[BaseModel],
    authorize: Callable[[dict], Any] | Callable[[dict], Awaitable[Any]],
)
Parameter Type Description
model Type[BaseModel] Pydantic v2 model — the request body is validated against this before authorize is called
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

Validation errors are automatically serialised into a structured 422 response:

{
  "error": {
    "code": "ValidationError",
    "status_code": 422,
    "message": "Validation failed.",
    "details": {
      "validation_errors": [
        {"field": "email", "errors": ["value is not a valid email address"]}
      ]
    }
  }
}

BaseProvider

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".

from py_auth import BaseProvider, AuthResult


class MyProvider(BaseProvider):
    async def handle_request(self, *args, **kwargs) -> AuthResult: ...

Schemas & TypedDicts

AuthResult

class AuthResult(TypedDict):
    data: Any | None
    error: AuthError | None

AuthError

class AuthError(TypedDict, total=False):
    code: str  # machine-readable, e.g. "InvalidSessionToken"
    status_code: int  # HTTP status to send to the client
    message: str  # human-readable description
    details: dict  # optional structured detail (e.g. validation errors)

PyAuthAdapterProtocol

class PyAuthAdapterProtocol(Protocol):
    async def create_session(self, session_data: dict) -> dict: ...
    async def get_session_by_session_token_hash(
        self, token_hash: str 
    ) -> dict | None: ...
    async def delete_session_by_session_token_hash(self, token_hash: str) -> None: ...
    async def delete_session(self, session_id: str) -> None: ...
    async def update_session(
        self, session_id: str, updates: Dict
    ) -> dict | None: ...

The adapter only manages sessions. User lookup and creation live entirely inside your authorize() callback — giving you full control over hashing, validation, and any other user-creation logic your app needs.

CookieOptions

class CookieOptions(BaseModel):
    http_only: bool | None = None
    secure: bool | None = None
    same_site: Literal["lax", "strict", "none"] | None = None
    path: str | None = None
    domain: str | None = None
    max_age: int | None = None  # seconds
    expires: datetime | None = None

Exceptions

All exceptions inherit from PyAuthError and carry a status_code attribute for easy HTTP mapping.

Exception Default status_code When raised
PyAuthError 500 Base class; general catch-all
ConfigurationError 500 Adapter missing required methods, or provider not configured
AdapterError 500 Database engine setup failure
DuplicateEntryError 409 Unique constraint violation (e.g. duplicate session token)
ForeignKeyViolationError 400 Foreign key violation (e.g. referenced user no longer exists)
RecordNotFoundError 404 Requested record not found

Integrations

FastAPI — py-auth-fastapi

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.

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.

# You set up your PyAuth instance as normal...
auth = PyAuth(adapter=my_adapter, providers=[credentials_provider])

# ...then hand it to the integration. That's it.
app.include_router(PyAuthFastAPI(auth), prefix="/auth", tags=["Authentication"])

The integration exposes ready-made routes for sign-in, session verification, and sign-out — no manual cookie handling, no manual response construction.

pip install py-auth-fastapi

Available Adapters

Package Supported Databases Install
py-auth-sqlalchemy PostgreSQL (asyncpg), MySQL (aiomysql), SQLite (aiosqlite) pip install py-auth-sqlalchemy

More adapters (Tortoise ORM, Motor/MongoDB, Beanie, etc.) are on the roadmap. Community contributions are welcome — see CONTRIBUTING.md.


Roadmap

py-auth-core is in early release (0.0.1). Here's what's planned:

Providers

  • GoogleProvider — Google OAuth 2.0
  • GithubProvider — GitHub OAuth
  • EmailProvider — passwordless magic-link sign-in

Integrations

  • py-auth-fastapi — FastAPI integration
  • py-auth-django — Django integration
  • py-auth-flask — Flask / Quart integration
  • py-auth-litestar — Litestar integration

Adapters

  • Tortoise ORM adapter
  • Motor (async MongoDB) adapter
  • Beanie adapter

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.


Security Notes

  • 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.
  • CSRF validation uses hmac.compare_digest — immune to timing attacks.
  • Cookie defaults follow the __Host- prefix convention for session cookies: Secure, httpOnly, Path=/, no explicit Domain. This provides the strongest possible same-origin binding.
  • The CSRF cookie intentionally omits httpOnly so your frontend can read it and attach it as a request header for server-side comparison.
  • In ENVIRONMENT=production, the secure flag is always forced to True on every cookie regardless of user configuration.

Contributing

Want to build a new provider, adapter, or integration? See CONTRIBUTING.md for architecture guidelines, how the adapter protocol works, and how to get started.


License

MIT — see LICENSE for details.

Release files for py-auth-core 0.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for py-auth-core 0.0.1
File Size Uploaded
py_auth_core-0.0.1.tar.gz 18.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for py-auth-core 0.0.1
File Interpreter ABI Platform
py_auth_core-0.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 33.9 kB

Release files / py_auth_core-0.0.1.tar.gz

Download URL py_auth_core-0.0.1.tar.gz
Size 18.6 kB
Tags Source
SHA-256 checksum
How to use checksums
dfcef498d470d17391e68335b5b8c92f43762b65bb1bd61a80f0d761a451f36a
BLAKE2b-256 checksum
How to use checksums
c2c17fdb97085471c2394107c08a2d7b86504b4daffe9025fe0e5d88acaa646f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release files / py_auth_core-0.0.1-py3-none-any.whl

Download URL py_auth_core-0.0.1-py3-none-any.whl
Size 15.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
506398fedaca07c153d148bc376a2875c8ad9079da09757b3b6dd9d1ef16b208
BLAKE2b-256 checksum
How to use checksums
90c39f1c37157e83d9434318b59b0572da5080e0370465eda92e73c95c8638c5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release history Release notifications | RSS feed

This release

0.0.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page