Metadata-Version: 2.5
Name: enlace_auth
Version: 0.1.21
Summary: Authentication, sessions, admin dashboard, and per-user stores for the enlace platform
Project-URL: Homepage, https://github.com/i2mint/enlace_auth
Author: Thor Whalen
License: Apache-2.0
License-File: LICENSE
Keywords: admin,argon2,auth,enlace,fastapi,platform,session
Requires-Python: >=3.10
Requires-Dist: argh>=0.31.0
Requires-Dist: argon2-cffi>=23
Requires-Dist: dol>=0.2
Requires-Dist: email-validator>=2.0
Requires-Dist: enlace>=0.1.25
Requires-Dist: fastapi>=0.100.0
Requires-Dist: itsdangerous>=2.1
Requires-Dist: pydantic>=2.0.0
Provides-Extra: dev
Requires-Dist: authlib>=1.3; extra == 'dev'
Requires-Dist: cryptography>=42; extra == 'dev'
Requires-Dist: httpx; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: pytest-asyncio; extra == 'dev'
Requires-Dist: python-multipart>=0.0.9; extra == 'dev'
Provides-Extra: oauth
Requires-Dist: authlib>=1.3; extra == 'oauth'
Requires-Dist: cryptography>=42; extra == 'oauth'
Requires-Dist: httpx>=0.24.0; extra == 'oauth'
Requires-Dist: python-multipart>=0.0.9; extra == 'oauth'
Description-Content-Type: text/markdown

# enlace_auth

Authentication, sessions, an admin dashboard, and per-user stores for the
[enlace](https://github.com/i2mint/enlace) multi-app platform.

`enlace` itself is auth-agnostic — it composes apps and routes traffic. This
package plugs in at compose time and adds:

- `/auth/login`, `/auth/logout`, `/auth/register`, `/auth/whoami`,
  `/auth/csrf`, `/auth/me/password`, `/auth/shared-login`
- `/auth/account` — the signed-in user changes their own password
- `/auth/forgot-password` — self-service reset by emailed link
- `/_admin/api/*` — list/create/delete users, mint a password-reset link,
  set a password directly, view app policy, and **grant/revoke per-app access
  at runtime** (optional expiry). Gated by an admin allowlist.
- per-user data injection via `request.state.store`
- `PlatformAuthMiddleware` + `CSRFMiddleware`
- optional OAuth2 / OIDC via Authlib

## Quick start

```python
from enlace import build_backend, PlatformConfig
from enlace_auth import plugin as auth_plugin

config = PlatformConfig.from_toml("platform.toml")
app = build_backend(config, plugins=[auth_plugin])
```

Or, if you serve via `uvicorn --factory enlace.compose:create_app`, set:

```bash
export ENLACE_PLUGINS=enlace_auth:plugin
```

## Configuration

In `platform.toml`:

```toml
[auth]
enabled = true
session_cookie_name = "enlace_session"
session_max_age_seconds = 86400
signing_key_env = "ENLACE_SIGNING_KEY"
secure_cookies = true

[auth.stores]
backend = "file"
path = "~/.enlace/platform_store"

[stores.user_data]
backend = "file"
path = "~/.enlace/user_data"
```

### OAuth 2.1 authorization server (MCP connectors)

`enlace_auth` can act as the authorization server that issues the JWTs an MCP
custom connector validates. **One** server serves *every* connector on the
platform, so anything a user reads during the connect flow must be keyed on the
connector — the `resource` the client asks for:

```toml
[auth.oauth_server]
enabled = true
issuer = "https://apps.example.com"

# Who may authorize for each connector. A resource that is not listed is open
# to any authenticated user; a listed one denies everyone else.
[auth.oauth_server.resource_allowlist]
"https://apps.example.com/connector-a-mcp" = ["alice@example.com"]

# The name the consent screen shows, keyed by the SAME resource URL. A resource
# with no entry gets generic copy that names no product — never default to one
# connector's name, or every other connector's consent screen inherits it.
[auth.oauth_server.resource_display_names]
"https://apps.example.com/connector-a-mcp" = "Connector A"
"https://apps.example.com/connector-b-mcp" = "Connector B"
```

Plus environment variables:

- `ENLACE_SIGNING_KEY` — signing key (32+ chars). Generate with `python -c
  "import secrets; print(secrets.token_urlsafe(32))"`.
- `ENLACE_ADMIN_EMAILS` — comma-separated admin emails (gate `/_admin`).
- `ENLACE_ALLOW_UNSIGNED=1` — opt-out from fail-fast (diagnostics only).
- `ENLACE_SMTP_HOST` (+ `_PORT`, `_USER`, `_PASSWORD`, `_FROM`, `_TLS`) — mail
  sender for password-reset emails. Unset means **no email is sent**; see below.

## Passwords

Three ways a password gets set, for three different situations:

| Situation | Path |
|---|---|
| User knows their password, wants a new one | `/auth/account` |
| User is locked out, **SMTP configured** | `/auth/forgot-password` → emailed link |
| User is locked out, **no SMTP** | admin mints a link and delivers it by hand |

That last one is what keeps the platform usable with no mail server:

```bash
enlace-auth reset-link someone@example.com          # prints a 72h, single-use link
enlace-auth reset-link someone@example.com --hours 4
```

or the **Reset link** button in `/_admin/`. Prefer it to setting a password for
someone: the admin never invents, learns, or transmits another person's
credential, and the recipient chooses their own.

`enlace-auth set-password <email>` still exists for break-glass use.

Reset links are signed, not stored. Each carries the account's current
password-hash fingerprint and its own absolute expiry, so a link dies on first
use — and any other password change invalidates every link outstanding for that
account. With no SMTP configured, `/auth/forgot-password` says so and points at
the admin rather than promising an email that only reaches the log.

## Per-app access & runtime grants

Each app declares an `access` level in its `app.toml`
(`public | protected:shared | protected:user`). A `protected:user` app may also
declare a static baseline allow-list:

```toml
access = "protected:user"
allowed_users = ["owner@example.com"]   # always allowed; edit-in-code baseline
```

On top of that baseline you can grant access **at runtime — no redeploy** — from
the admin dashboard or the CLI. Runtime grants are *additive* (effective access =
`allowed_users ∪ active grants`) and may carry an optional UTC expiry:

```bash
enlace-auth grant vault alice@example.com --expires 2026-12-31   # end of day UTC
enlace-auth list-grants --app vault
enlace-auth revoke-grant vault alice@example.com
```

Grants live in a `grants/` store alongside `sessions/` and `users/` under
`[auth.stores] path`, so they persist across restarts and redeploys. A grant on
an app with an *empty* `allowed_users` (open to any authenticated user) is
rejected — it would have no additive effect and would unintentionally restrict an
open app. To remove a user listed in `allowed_users`, edit `app.toml` (that layer
is intentionally code-managed); the admin panel manages the runtime layer.

## Doctor checks

```python
from enlace.doctor import run_doctor
from enlace_auth.diagnostics import static_checks, http_checks

report = run_doctor(
    config,
    base_url="http://localhost:8000",
    extra_static_checks=static_checks,
    extra_http_checks=http_checks,
)
```

## Status

Extracted from `enlace` 0.0.11. The Python API is stable; an admin frontend
ships separately as a normal enlaced app.
