Skip to main content

hayate-auth

Hayate ecosystem: Start here · Production golden app · Tested compatibility

Standards-first authentication for hayate — a mountable, better-auth-style auth handler built on the WHATWG Request/Response model.

Status: alpha (0.10.x). Email+password, sessions, CSRF, email verification, password reset, OAuth 2.1 + PKCE (Google / GitHub), TOTP two-factor, API keys, an OAuth 2.1 authorization server (AS mode: RFC 8414 metadata, RFC 7591 dynamic registration, MCP Client ID Metadata Documents, PKCE-only code + refresh grants), magic links, and passkeys (WebAuthn L3, [passkey] extra) are implemented and attack-regression-tested; a generate CLI, a plugin API, and a Cloudflare D1 adapter ship too. Not yet security-audited — see SECURITY.md. The internal design memo (Japanese) lives in DESIGN.md; release history is in CHANGELOG.md.

import os

from hayate import Hayate
from hayate_auth import Auth
from hayate_auth.adapters.sqlite import SQLiteAdapter

adapter = SQLiteAdapter("app.db")
adapter.create_tables()

auth = Auth(secret=os.environ["AUTH_SECRET"], adapter=adapter)

app = Hayate()
auth.register(app)  # serves /api/auth/* (sign-up, sign-in, session, ...)

@app.get("/me", auth.require_session())
async def me(c):
    return c.json(c.get("user"))

The same file runs under any ASGI server and on Cloudflare Python Workers — see examples/todo.

Endpoints

Method / path (under /api/auth) Purpose
POST /sign-up/email Register with email + password, start a session
POST /sign-in/email Verify credentials, start a session
GET /get-session Current {user, session} (or nulls)
POST /sign-out Revoke the session server-side
POST /forget-password → /reset-password Reset flow via a one-shot hashed token
GET /verify-email Confirm an email with a one-shot token
POST /sign-in/social → GET /callback/:provider OAuth 2.1 + PKCE (Google / GitHub)
POST /two-factor/enable · /verify · /disable TOTP (RFC 6238) enrollment
POST /sign-in/two-factor Second step when 2FA is on
POST /api-key/create · /verify · /delete · GET /api-key/list API keys (hashed, scoped, expiring)
GET /oauth2/authorize · POST /oauth2/consent · /oauth2/token · /oauth2/register AS mode: OAuth 2.1 authorization server
POST /sign-in/magic-link → GET /magic-link/verify Magic links (plugin)
POST /passkey/generate-register-options · /verify-registration · /generate-authenticate-options · /verify-authentication · GET /passkey/list-user-passkeys · POST /passkey/delete-passkey Passkeys (WebAuthn L3, [passkey] extra)

Magic links ship as the first AuthPlugin — plugins add routes with the same handler signature the built-ins use:

from hayate_auth.plugins import magic_link

auth = Auth(
    secret=..., adapter=adapter,
    plugins=[magic_link(send=deliver_link_email)],  # async (email, token) -> None
)

Passkeys need the extra (pip install hayate-auth[passkey], pulls py_webauthn) and a relying-party config:

from hayate_auth import PasskeyConfig

auth = Auth(
    secret=..., adapter=adapter,
    passkey=PasskeyConfig(rp_id="example.com", rp_name="My App",
                          origin="https://example.com"),
)

AS mode: be the OAuth authorization server

Pass an AuthorizationServer config and your app issues OAuth 2.1 tokens — authorization-code + PKCE (S256 only), refresh rotation with reuse detection, RFC 8414 metadata at /.well-known/oauth-authorization-server, and open RFC 7591 dynamic client registration (what MCP clients expect). Tokens are opaque and stored hashed; login and consent pages stay yours (login_url / consent_url), the consent decision is one JSON POST.

from hayate_auth import Auth, AuthorizationServer

auth = Auth(
    secret=os.environ["AUTH_SECRET"],
    adapter=adapter,
    authorization_server=AuthorizationServer(
        issuer="https://app.example.com",
        login_url="/login",
        consent_url="/consent",
        scopes_supported=("mcp",),
        resource="https://app.example.com/mcp",
    ),
)

Paired with hayate-mcp's resource server, that is an MCP server and its authorization server in one app — the flow MCP Inspector and Claude Code drive end to end (examples/mcp-oauth):

from hayate_mcp import Authorization, McpMount
McpMount(server, authorization=Authorization(
    resource="https://app.example.com/mcp",
    authorization_servers=["https://app.example.com"],
    verify_token=auth.oauth_token_verifier(resource="https://app.example.com/mcp"),
)).register(app)

MCP 2025-11-25 recommends Client ID Metadata Documents before DCR. Enable them with an injected fetcher so your deployment controls DNS and egress:

from hayate_auth import ClientIdMetadataDocuments

authorization_server = AuthorizationServer(
    # issuer/login_url/consent_url/scopes_supported/resource as above
    client_id_metadata_documents=ClientIdMetadataDocuments(
        fetch_client_metadata,  # async URL -> hayate.Response; must reject redirects
        allow_url=outbound_client_policy,
    ),
)

hayate-auth validates HTTPS URL-form client IDs, exact client_id, same-origin or loopback redirects, public-client metadata, JSON content type, and a 5 KiB body limit. The fetcher remains responsible for DNS-level SSRF protection.

API keys are the lighter-weight bridge to the same resource server — verify_token=auth.verify_api_key protects an MCP server with a static key instead of the full OAuth dance.

REST and generated OpenAPI share the same authorization declaration:

from hayate_openapi import OpenApi

@app.get("/documents", auth.require_oauth_token(
    "documents:read", resource="https://app.example.com/mcp"
))
async def documents(c):
    return c.json({"subject": c.get("principal")["subject"]})

OpenApi(
    app,
    title="API",
    version="1",
    security_schemes=auth.openapi_security_schemes(),
)

With TOTP enabled, /sign-in/email returns {"two_factor_required": true} plus a short-lived signed challenge cookie instead of a session; the client then posts the authenticator code to /sign-in/two-factor to get the session — so a stolen password alone never signs in.

Email delivery is your callback (send_reset_password / send_verification_email); the core mints and verifies tokens but never builds URLs or sends mail. Generate migration DDL with python -m hayate_auth generate --dialect sqlite|postgres|d1. When upgrading an existing 0.9.1 database to the next release, emit the single-use TOTP column migration with python -m hayate_auth generate --dialect <dialect> --upgrade-from 0.9.1 and apply it exactly once before deploying the new code.

OAuth providers are injected; the token exchange runs over hayate-fetch, so it works on ASGI and Workers alike:

from hayate_auth import Auth, google, github

auth = Auth(
    secret=os.environ["AUTH_SECRET"],
    adapter=adapter,
    providers=[
        google(client_id=..., client_secret=...),
        github(client_id=..., client_secret=...),
    ],
)

Why

  • Python has no equivalent of better-auth: a framework-agnostic, self-hosted, schema-owning auth library. django-allauth is Django-only; fastapi-users is in maintenance mode.
  • better-auth works on every JS framework because its core is a single fetch(Request) -> Response handler. hayate is the only Python framework whose user-facing surface is WHATWG Request/Response — so that architecture finally maps 1:1 to Python.
  • Small portability-first base with 2 direct runtime dependencies: hayate and hayate-fetch. Database drivers, KDFs, email delivery, DPoP cryptography, and WebAuthn remain injected protocols or optional extras.

Security posture

  • Passwords: scrypt at OWASP parameters (N=2^17, r=8, p=1) on every runtime, PBKDF2-HMAC-SHA256 (600k) fallback; PHC-style strings make the backends mutually verifiable. Length-only policy per NIST SP 800-63B.
  • Sessions: opaque 256-bit tokens, only their SHA-256 stored; __Host--prefixed HttpOnly SameSite=Lax cookies on HTTPS.
  • CSRF: SameSite + Origin (RFC 6454) + Fetch Metadata — no token embedding.
  • Sign-in failures are uniform in body and KDF timing (enumeration defense).
  • Coverage ledger: docs/asvs.md (selected OWASP ASVS 5.0.0 controls, exact IDs and explicit gaps; not a certification).
  • Independent review pack: audit/README.md retains the signed v0.9.1 base and pins signed v0.10.3 as the current delta-review target, including release/SBOM hashes, a threat model, SQLite/direct HTTP + PostgreSQL DDL + workerd/D1 profiles, and a reviewer RFP. The audit has not yet been commissioned.
  • You must rate-limit /api/auth/* (hayate middleware or your infrastructure): brute-force throttling is deliberately out of core.
  • TOTP seeds and upstream provider access/refresh tokens are stored recoverably. Production databases and backups must be access-controlled and encrypted; see SECURITY.md for known limitations.
  • Adapters must implement atomic update_many() and return the affected-row count. This is the security boundary for single-use TOTP steps and prevents concurrent authorization-code or refresh-token redemption from minting multiple token families. Durable compromise markers and guarded post-insert finalization also prevent replay detection during token creation from leaving a live family.

License

MIT

Metadata

Release files for hayate-auth 0.10.5

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

Source distribution (sdist)

Source distribution for hayate-auth 0.10.5
File Size Uploaded Provenance
hayate_auth-0.10.5.tar.gz 58.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for hayate-auth 0.10.5
File Interpreter ABI Platform
hayate_auth-0.10.5-py3-none-any.whl Python 3 none any Details

Total release size: 133.7 kB

Release files / hayate_auth-0.10.5.tar.gz

Download URL hayate_auth-0.10.5.tar.gz
Size 58.8 kB
Tags Source
SHA-256 checksum
How to use checksums
da3842d8c7563a841f216df22432e5eb6e83b9013a624dc07f1e94f013c788d4
BLAKE2b-256 checksum
How to use checksums
3ebad79b586176d19fb2b10dc07a422665b71f2a99fed2dba270218d2c26d687
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.

Transparency log

Release files / hayate_auth-0.10.5-py3-none-any.whl

Download URL hayate_auth-0.10.5-py3-none-any.whl
Size 75.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
84e872117d353723166f5cb9c5d6a6e56954f0a94998a145f9a42ae66ab1b3e0
BLAKE2b-256 checksum
How to use checksums
43d49a31ba9e3567d52e2541c9660c022fa2fe06d97a7a72adfa3f338d15d7ad
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.

Transparency log

Security for hayate-auth 0.10.5

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for hayate_auth-0.10.5.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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