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; agenerateCLI, 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) -> Responsehandler. 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:
hayateandhayate-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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| hayate_auth-0.10.5.tar.gz | 58.8 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.