Metadata-Version: 2.4
Name: hayate-auth
Version: 0.5.0
Summary: Authentication for hayate as a pure fetch(Request) -> Response handler: email+password, sessions, CSRF
Keywords: hayate,authentication,session,better-auth,workers
Author: Yusuke Hayashi
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP :: Session
Requires-Dist: hayate>=0.8.0
Requires-Dist: hayate-fetch>=0.1.0
Requires-Python: >=3.12
Project-URL: Repository, https://github.com/hayatepy/hayate-auth
Description-Content-Type: text/markdown

# hayate-auth

Standards-first authentication for [hayate](https://github.com/hayatepy/hayate) —
a mountable, better-auth-style auth handler built on the WHATWG Request/Response
model.

> **Status: alpha (0.5.x).** Email+password, sessions, CSRF, email
> verification, password reset, **OAuth 2.1 + PKCE** (Google / GitHub), **TOTP
> two-factor**, and **API keys** are implemented and attack-regression-tested;
> a `generate` CLI and a Cloudflare D1 adapter ship too. Not yet
> security-audited — see [SECURITY.md](SECURITY.md). The internal design memo
> (Japanese) lives in [DESIGN.md](DESIGN.md).

```python
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](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) |

API keys double as the bridge to [hayate-mcp](https://github.com/hayatepy/hayate-mcp):
`auth.verify_api_key` plugs straight into its OAuth Resource Server, so an API
key protects an MCP server in one line:

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

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

OAuth providers are injected; the token exchange runs over
[hayate-fetch](https://github.com/hayatepy/hayate-fetch), so it works on ASGI
and Workers alike:

```python
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.
- Zero-dependency core (its only dependency is hayate, itself zero-dependency).
  Databases, KDFs, and email are injected protocols.

## 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](docs/asvs.md) (OWASP ASVS V6/V7, ratcheted).
- **You must rate-limit** `/api/auth/*` (hayate middleware or your
  infrastructure): brute-force throttling is deliberately out of core.

## License

MIT
