[auth] hero_proxy as the single authentication authority — add password + email flows, retire hero_auth #63

Open
opened 2026-06-22 12:04:21 +00:00 by timur · 8 comments
Owner

Problem

Authentication is fragmented across the stack — three overlapping systems, no single source of truth:

  • hero_proxy is already the network entry point and the identity authority for the convergence: OAuth2/OIDC, secp256k1 signature, the full users → groups → roles → claims RBAC, per-route auth_mode, sessions, and X-Hero-User/X-Hero-Context/X-Hero-Claims injection. But it has no local password credential, no email-driven flows (verification, reset, self-registration), and no SMTP — it can only authenticate by delegating to an external IdP or by signature.
  • hero_auth is a separate OAuth2 server with exactly those missing bits (email+password registration, JWT, admin UI, encrypted-TOML user store) — but it's ~2 months stale and being retired (removed from hero_os per home#118, avoided by embedder/memory), and it duplicates the proxy with a second identity store + a second token system.
  • cm50_app rolls its own in-app auth (password_hash, SessionStore, login(email,password), cm50_ui/pages/login.rs); freezone uses bespoke X-Proxy-*. Every app reinventing login is exactly what we're converging away from.

Decision

Make hero_proxy the single authentication authority. Add local password auth and the email-driven flows to the proxy (reusing its existing user store, sessions, OIDC, and claim-resolution/injection), delegate the raw email send to hero_mail, and finish retiring hero_auth. Apps then get login for free — the proxy authenticates → opens a session → injects X-Hero-* — so cm50's in-app auth and freezone's X-Proxy-* both retire onto it (consistent with the X-Hero-* standardization in home#309).

Email nuance: the proxy owns the auth flow + verification/reset token state but delegates delivery to hero_mail — keeps SMTP/provider churn out of the critical-path gateway.

Scope

In hero_proxy:

  • Local password credential (Argon2) + a password/local auth_mode alongside oauth/signature.
  • Email verification, password reset, and gated self-registration — flow + token state in the proxy, delivery via hero_mail.
  • Session issuance for password/magic-link logins (build on the existing OAuth session machinery + __Host- cookie).

Migration (separate, tracked here):

  • Finish retiring hero_auth (home#118); move its few remaining SDK consumers (books, maybe embedder/memory) to proxy-injected claims.
  • Converge cm50 (in-app login) and freezone (X-Proxy-*) onto proxy-issued sessions + X-Hero-*.

Caveat

The proxy is critical infra; centralizing auth raises its blast radius. Mitigations: delegate email transport (above), keep the local-password path optional per route, treat the auth modules as a well-tested unit. Net still the right call — a separate auth service re-introduces a second identity store + an extra hop, the opposite of the one-trunk/one-identity thesis.

References

  • home#309 (convergence; X-Hero-* standardization), home#118 (hero_auth removal)
  • hero_proxy/docs/claims.md, skills herolib_openrpc_authorize / hero_claim_format / hero_context
  • docs/2_blueprint/4_auth/ in hero_skills (the authorization side)

Plan to follow in a comment. Work lands on development_timur branches so existing apps (incl. the cm50 demo) keep working until cutover.

## Problem Authentication is fragmented across the stack — three overlapping systems, no single source of truth: - **hero_proxy** is already the network entry point and the identity authority for the convergence: OAuth2/OIDC, secp256k1 signature, the full `users → groups → roles → claims` RBAC, per-route `auth_mode`, sessions, and `X-Hero-User`/`X-Hero-Context`/`X-Hero-Claims` injection. **But** it has no local password credential, no email-driven flows (verification, reset, self-registration), and no SMTP — it can only authenticate by delegating to an external IdP or by signature. - **hero_auth** is a *separate* OAuth2 server with exactly those missing bits (email+password registration, JWT, admin UI, encrypted-TOML user store) — but it's ~2 months stale and being retired (removed from hero_os per home#118, avoided by embedder/memory), and it duplicates the proxy with a second identity store + a second token system. - **cm50_app** rolls its **own** in-app auth (`password_hash`, `SessionStore`, `login(email,password)`, `cm50_ui/pages/login.rs`); freezone uses bespoke `X-Proxy-*`. Every app reinventing login is exactly what we're converging away from. ## Decision Make **hero_proxy the single authentication authority.** Add local password auth and the email-driven flows to the proxy (reusing its existing user store, sessions, OIDC, and claim-resolution/injection), **delegate the raw email send to hero_mail**, and **finish retiring hero_auth**. Apps then get login for free — the proxy authenticates → opens a session → injects `X-Hero-*` — so cm50's in-app auth and freezone's `X-Proxy-*` both retire onto it (consistent with the `X-Hero-*` standardization in home#309). **Email nuance:** the proxy *owns the auth flow + verification/reset token state* but *delegates delivery to hero_mail* — keeps SMTP/provider churn out of the critical-path gateway. ## Scope In hero_proxy: - Local password credential (Argon2) + a `password`/`local` `auth_mode` alongside `oauth`/`signature`. - Email verification, password reset, and gated self-registration — flow + token state in the proxy, delivery via hero_mail. - Session issuance for password/magic-link logins (build on the existing OAuth session machinery + `__Host-` cookie). Migration (separate, tracked here): - Finish retiring **hero_auth** (home#118); move its few remaining SDK consumers (books, maybe embedder/memory) to proxy-injected claims. - Converge **cm50** (in-app login) and **freezone** (`X-Proxy-*`) onto proxy-issued sessions + `X-Hero-*`. ## Caveat The proxy is critical infra; centralizing auth raises its blast radius. Mitigations: delegate email transport (above), keep the local-password path optional per route, treat the auth modules as a well-tested unit. Net still the right call — a separate auth service re-introduces a second identity store + an extra hop, the opposite of the one-trunk/one-identity thesis. ## References - home#309 (convergence; `X-Hero-*` standardization), home#118 (hero_auth removal) - `hero_proxy/docs/claims.md`, skills `herolib_openrpc_authorize` / `hero_claim_format` / `hero_context` - `docs/2_blueprint/4_auth/` in hero_skills (the authorization side) Plan to follow in a comment. Work lands on `development_timur` branches so existing apps (incl. the cm50 demo) keep working until cutover.
Author
Owner

Implementation plan

Phased so each lands as a self-contained, reviewable commit. All work on development_timur branches; existing apps (incl. the cm50 demo) keep working until an explicit cutover. I'll comment here as I commit each step.

Phase 1 — credentials layer (additive, no behavior change)

  • credentials table keyed on user_id (Argon2 hash, email, email_verified, timestamps); migration alongside the existing users table.
  • db: set_password, verify_password, get_credential_by_email, mark_email_verified. Add the argon2 crate.
  • Unit tests: hash round-trip, wrong-password reject, unknown-email reject, constant-time compare.
  • Safe to merge on its own — nothing calls it yet.

Phase 2 — password login + local auth_mode

  • HTTP /auth/login (verify credential → create_oauth_session with provider_name="local" → set __Host-hero_proxy_session cookie) and /auth/logout; reuses the existing session + resolve_claims_for_user + X-Hero-* injection unchanged.
  • Accept local in route auth_mode (route enforcement in proxy.rs): valid session → proceed; missing → the login page instead of an OAuth redirect.
  • auth.set_password RPC (self, or admin via proxy.users.write); required_claim_for_method entries for the new public/gated methods.
  • Tests: login success/failure, session issuance, claim injection parity with the OAuth path.

Phase 3 — email flows via hero_mail

  • Proxy owns the flow + token state (verification_tokens table: purpose, token hash, expiry); delegates delivery to hero_mail over its JSON-RPC socket.
  • auth.register (gated self-signup) → email verification; auth.request_password_resetauth.reset_password.
  • Tests with a mock mail sink (no live SMTP in CI).

Phase 4 — login UI

  • Minimal proxy-served login / register / reset pages (same template stack as the existing admin), wired to the Phase 2/3 endpoints. Light/dark, no new JS framework.

Phase 5 — migrations (separate, gated on 1–4 green)

  • Retire hero_auth (home#118): move remaining SDK consumers (books; embedder/memory if still linked) to proxy-injected claims; drop the second token system.
  • Converge cm50 in-app login → proxy session on cm50_app:development_timur (current demo stays on main/development).
  • Converge freezone X-Proxy-*X-Hero-*.

Sequencing

Phases 1–2 first (the foundational password path), then 3–4 (email + UI), then 5 (migrations). Starting now on development_timur.

## Implementation plan Phased so each lands as a self-contained, reviewable commit. All work on `development_timur` branches; existing apps (incl. the cm50 demo) keep working until an explicit cutover. I'll comment here as I commit each step. ### Phase 1 — credentials layer (additive, no behavior change) - `credentials` table keyed on `user_id` (Argon2 hash, `email`, `email_verified`, timestamps); migration alongside the existing `users` table. - `db`: `set_password`, `verify_password`, `get_credential_by_email`, `mark_email_verified`. Add the `argon2` crate. - Unit tests: hash round-trip, wrong-password reject, unknown-email reject, constant-time compare. - *Safe to merge on its own — nothing calls it yet.* ### Phase 2 — password login + `local` auth_mode - HTTP `/auth/login` (verify credential → `create_oauth_session` with `provider_name="local"` → set `__Host-hero_proxy_session` cookie) and `/auth/logout`; reuses the existing session + `resolve_claims_for_user` + `X-Hero-*` injection unchanged. - Accept `local` in route `auth_mode` (route enforcement in `proxy.rs`): valid session → proceed; missing → the login page instead of an OAuth redirect. - `auth.set_password` RPC (self, or admin via `proxy.users.write`); `required_claim_for_method` entries for the new public/gated methods. - Tests: login success/failure, session issuance, claim injection parity with the OAuth path. ### Phase 3 — email flows via hero_mail - Proxy owns the flow + token state (`verification_tokens` table: purpose, token hash, expiry); **delegates delivery to hero_mail** over its JSON-RPC socket. - `auth.register` (gated self-signup) → email verification; `auth.request_password_reset` → `auth.reset_password`. - Tests with a mock mail sink (no live SMTP in CI). ### Phase 4 — login UI - Minimal proxy-served login / register / reset pages (same template stack as the existing admin), wired to the Phase 2/3 endpoints. Light/dark, no new JS framework. ### Phase 5 — migrations (separate, gated on 1–4 green) - Retire **hero_auth** (home#118): move remaining SDK consumers (books; embedder/memory if still linked) to proxy-injected claims; drop the second token system. - Converge **cm50** in-app login → proxy session on `cm50_app:development_timur` (current demo stays on `main`/`development`). - Converge **freezone** `X-Proxy-*` → `X-Hero-*`. ### Sequencing Phases 1–2 first (the foundational password path), then 3–4 (email + UI), then 5 (migrations). Starting now on `development_timur`.
Author
Owner

Phase 1 done — credentials data layer. Commit edc8b77c3e on development_timur (diff).

  • credentials table (1:1 with users, Argon2id PHC hash, login email, email_verified), added as an idempotent migration so existing OAuth-only DBs gain it on upgrade — purely additive, no behavior change yet.
  • ProxyDb::{set_password, verify_password, get_credential_by_email, mark_email_verified} + private Argon2 hash/verify helpers (per-call random salt; verify_password returns the same None for unknown-email and wrong-password — no account oracle).
  • Added the argon2 dep.
  • 7 unit tests green (roundtrip, wrong/unknown reject, empty refused, salting, replace, verify-flag); cargo build + fmt + clippy clean.

Next: Phase 2 — /auth/login + /auth/logout (verify → create session → set __Host- cookie, reusing the existing session + X-Hero-* injection) and accepting local in route auth_mode.

**Phase 1 done — credentials data layer.** Commit `edc8b77c3e` on `development_timur` ([diff](https://forge.ourworld.tf/lhumina_code/hero_proxy/commit/edc8b77c3ea943113584327da4d7857be431b3d9)). - `credentials` table (1:1 with `users`, Argon2id PHC hash, login email, `email_verified`), added as an idempotent migration so existing OAuth-only DBs gain it on upgrade — purely additive, no behavior change yet. - `ProxyDb::{set_password, verify_password, get_credential_by_email, mark_email_verified}` + private Argon2 hash/verify helpers (per-call random salt; `verify_password` returns the same `None` for unknown-email and wrong-password — no account oracle). - Added the `argon2` dep. - 7 unit tests green (roundtrip, wrong/unknown reject, empty refused, salting, replace, verify-flag); `cargo build` + `fmt` + `clippy` clean. Next: Phase 2 — `/auth/login` + `/auth/logout` (verify → create session → set `__Host-` cookie, reusing the existing session + `X-Hero-*` injection) and accepting `local` in route `auth_mode`.
Author
Owner

Phase 2 done — password login + local auth_mode. On development_timur.

2a — endpoints (6335f1d):

  • GET /auth/login serves a minimal sign-in form (polished UI is Phase 4); POST /auth/login verifies the credential, opens a session keyed by the local username with provider local, and sets the same __Host- cookie the OAuth callback uses — so the existing validate_oauth_session + X-Hero-* injection honors it unchanged. /auth/logout reuses the provider-agnostic logout handler.
  • Both added to resolve_caller's public allowlist (else a configured auth_token would 401 the login POST). safe_return_to clamps post-login redirects to same-origin paths (no open redirect); form values html-escaped; same response for unknown-email vs wrong-password.

2b — route enforcement (2fdf458):

  • dispatch_domain_route + the info-route guard now accept auth_mode = "local": valid session → inject identity downstream (same path as oauth); no session → 302 to /auth/login?return_to=<path>.

Tests: safe_return_to allow/deny, html_escape, and both local arms redirecting to login. 90 lib tests green; build/fmt/clippy clean. No change to existing oauth/signature/none/bearer behavior.

Next: Phase 3 — email flows (verify / reset / gated registration) with delivery delegated to hero_mail.

**Phase 2 done — password login + `local` auth_mode.** On `development_timur`. **2a — endpoints** ([`6335f1d`](https://forge.ourworld.tf/lhumina_code/hero_proxy/commit/6335f1d)): - `GET /auth/login` serves a minimal sign-in form (polished UI is Phase 4); `POST /auth/login` verifies the credential, opens a session keyed by the local username with provider `local`, and sets the same `__Host-` cookie the OAuth callback uses — so the existing `validate_oauth_session` + `X-Hero-*` injection honors it unchanged. `/auth/logout` reuses the provider-agnostic logout handler. - Both added to `resolve_caller`'s public allowlist (else a configured `auth_token` would 401 the login POST). `safe_return_to` clamps post-login redirects to same-origin paths (no open redirect); form values html-escaped; same response for unknown-email vs wrong-password. **2b — route enforcement** ([`2fdf458`](https://forge.ourworld.tf/lhumina_code/hero_proxy/commit/2fdf458)): - `dispatch_domain_route` + the info-route guard now accept `auth_mode = "local"`: valid session → inject identity downstream (same path as oauth); no session → 302 to `/auth/login?return_to=<path>`. Tests: `safe_return_to` allow/deny, `html_escape`, and both `local` arms redirecting to login. **90 lib tests green; build/fmt/clippy clean.** No change to existing oauth/signature/none/bearer behavior. Next: Phase 3 — email flows (verify / reset / gated registration) with delivery delegated to hero_mail.
Author
Owner

Phase 3a done — verification-token layer. Commit d847e51 on development_timur.

  • verification_tokens table (idempotent migration) for email-verify + password-reset. Only the SHA-256 of the token is stored — a DB read never yields a usable token; the raw value lives only in the email link.
  • ProxyDb::{create_verification_token, consume_verification_token} — single-use; consume DELETE scoped to (hash, purpose) so a wrong-purpose probe can't burn another purpose's token; {:+} seconds TTL stays a valid SQLite modifier for negatives.
  • 4 tests (consume-once, wrong-purpose no-match + no-burn, expired, unknown). 94 lib tests green; fmt/clippy clean.

Heads-up that changes 3b: hero_mail is currently a stub. Its mail.mail.send returns canned JSON and ignores params — no Stalwart delivery is wired. So the remaining email flows will ship behind a pluggable MailSender seam whose default impl logs the verification/reset link (fully usable for dev), with the real hero_mail delegation dropping in once hero_mail actually delivers. The proxy owns the flow + token state either way.

One decision for @timur before 3b: self-registration policy — should /auth/register be open (anyone can sign up, email-verify gates activation) or invite/admin-only (admins create users; password-reset + verify only)? That choice shapes whether 3b includes the register endpoint or just verify + reset.

**Phase 3a done — verification-token layer.** Commit [`d847e51`](https://forge.ourworld.tf/lhumina_code/hero_proxy/commit/d847e51) on `development_timur`. - `verification_tokens` table (idempotent migration) for email-verify + password-reset. **Only the SHA-256 of the token is stored** — a DB read never yields a usable token; the raw value lives only in the email link. - `ProxyDb::{create_verification_token, consume_verification_token}` — single-use; consume DELETE scoped to `(hash, purpose)` so a wrong-purpose probe can't burn another purpose's token; `{:+} seconds` TTL stays a valid SQLite modifier for negatives. - 4 tests (consume-once, wrong-purpose no-match + no-burn, expired, unknown). **94 lib tests green; fmt/clippy clean.** **Heads-up that changes 3b: `hero_mail` is currently a stub.** Its `mail.mail.send` returns canned JSON and ignores params — no Stalwart delivery is wired. So the remaining email flows will ship behind a pluggable `MailSender` seam whose default impl **logs the verification/reset link** (fully usable for dev), with the real `hero_mail` delegation dropping in once hero_mail actually delivers. The proxy owns the flow + token state either way. **One decision for @timur before 3b:** self-registration policy — should `/auth/register` be **open** (anyone can sign up, email-verify gates activation) or **invite/admin-only** (admins create users; password-reset + verify only)? That choice shapes whether 3b includes the register endpoint or just verify + reset.
Author
Owner

Confirmed: hero_mail is fully non-functional, and the proxy will own mail.

Checked development, integration, and main at their latest commits (all 2026-06-11): every RPC method — including mail.mail.send — is a canned-JSON stub that ignores its params, and there is no SMTP/lettre/Stalwart-submit library anywhere in the repo or its Cargo deps. Delegating to it would silently drop every email. So we drop the hero_mail delegation: the proxy sends its own mail.

Phase 3b-i done — proxy-native mail seam. Commit ace8741 on development_timur.

  • New mail module: MailSender trait + SmtpMailSender (real SMTP via lettre) + LogMailSender (logs the link; default when SMTP unconfigured — dev/test flows still complete).
  • SmtpConfig::from_db reads smtp_host/smtp_from/smtp_port/smtp_username/smtp_password/smtp_starttls from the settings table; sender_from_db picks SMTP when host+from are set, else logging. Wired into AppState.
  • musl-safe TLS: lettre uses rustls (not native-tls/openssl, which fights the static-musl CI build); SmtpMailSender defensively installs the aws-lc-rs CryptoProvider to avoid the ambiguous-default rustls panic (cf. #60).
  • 5 tests; 99 lib tests green; fmt/clippy clean.

Two caveats to watch: live SMTP-over-TLS can't be verified from here (needs a real server), and the new deps want a musl-CI build confirmation on this branch.

Still need your call on registration policy (blocks the register endpoint only): open self-signup vs invite/admin-only? Password-reset + email-verification flows are policy-independent and I can build them next regardless.

**Confirmed: hero_mail is fully non-functional**, and the proxy will own mail. Checked `development`, `integration`, and `main` at their latest commits (all 2026-06-11): every RPC method — including `mail.mail.send` — is a canned-JSON stub that ignores its params, and there is **no SMTP/lettre/Stalwart-submit library anywhere** in the repo or its Cargo deps. Delegating to it would silently drop every email. So we drop the hero_mail delegation: **the proxy sends its own mail.** **Phase 3b-i done — proxy-native mail seam.** Commit [`ace8741`](https://forge.ourworld.tf/lhumina_code/hero_proxy/commit/ace8741) on `development_timur`. - New `mail` module: `MailSender` trait + `SmtpMailSender` (real SMTP via `lettre`) + `LogMailSender` (logs the link; default when SMTP unconfigured — dev/test flows still complete). - `SmtpConfig::from_db` reads `smtp_host`/`smtp_from`/`smtp_port`/`smtp_username`/`smtp_password`/`smtp_starttls` from the settings table; `sender_from_db` picks SMTP when host+from are set, else logging. Wired into `AppState`. - **musl-safe TLS:** lettre uses rustls (not native-tls/openssl, which fights the static-musl CI build); `SmtpMailSender` defensively installs the aws-lc-rs `CryptoProvider` to avoid the ambiguous-default rustls panic (cf. #60). - 5 tests; 99 lib tests green; fmt/clippy clean. **Two caveats to watch:** live SMTP-over-TLS can't be verified from here (needs a real server), and the new deps want a musl-CI build confirmation on this branch. **Still need your call on registration policy** (blocks the register endpoint only): open self-signup vs invite/admin-only? Password-reset + email-verification flows are policy-independent and I can build them next regardless.
Author
Owner

Phase 3 complete — the full local-auth backend is in place. On development_timur.

3b-ii — reset + verification (52761f2):

  • POST /auth/request-password-reset → 1h token + emailed link; generic notice always (no account oracle).
  • GET/POST /auth/reset → password validated (≥8) before the single-use token is consumed (a weak password doesn't burn the link); then set_password + mark verified.
  • GET /auth/verify?token= → consume + mark verified → /auth/login?verified=1.

3b-iii — registration, policy-gated (ef912b9):

  • Both policies supported via the registration_mode setting: open = self-signup; anything else (default) = closed/invite-only. GET/POST /auth/register creates an unverified local account (same provisioning as the OAuth path) and emails a 24h verify link; closed by default.

Phase 3 total: token store + SMTP/log mail seam + reset + verify + register. 101 lib tests green; fmt/clippy clean throughout.

cm50 will set registration_mode=open.

What remains: Phase 4 (polished login/register/reset UI — minimal functional forms exist now) and Phase 5 (migrations: retire hero_auth; move cm50 onto proxy auth on cm50_app:development_timur; freezone X-Proxy-*X-Hero-*). Plus the two standing caveats: live SMTP-over-TLS verification and a musl-CI build check for the new deps.

**Phase 3 complete — the full local-auth backend is in place.** On `development_timur`. **3b-ii — reset + verification** ([`52761f2`](https://forge.ourworld.tf/lhumina_code/hero_proxy/commit/52761f2)): - `POST /auth/request-password-reset` → 1h token + emailed link; generic notice always (no account oracle). - `GET/POST /auth/reset` → password validated (≥8) **before** the single-use token is consumed (a weak password doesn't burn the link); then `set_password` + mark verified. - `GET /auth/verify?token=` → consume + mark verified → `/auth/login?verified=1`. **3b-iii — registration, policy-gated** ([`ef912b9`](https://forge.ourworld.tf/lhumina_code/hero_proxy/commit/ef912b9)): - **Both policies supported** via the `registration_mode` setting: `open` = self-signup; anything else (default) = closed/invite-only. `GET/POST /auth/register` creates an unverified local account (same provisioning as the OAuth path) and emails a 24h verify link; closed by default. **Phase 3 total:** token store + SMTP/log mail seam + reset + verify + register. **101 lib tests green; fmt/clippy clean** throughout. cm50 will set `registration_mode=open`. **What remains:** Phase 4 (polished login/register/reset UI — minimal functional forms exist now) and Phase 5 (migrations: retire hero_auth; move cm50 onto proxy auth on `cm50_app:development_timur`; freezone `X-Proxy-*`→`X-Hero-*`). Plus the two standing caveats: live SMTP-over-TLS verification and a musl-CI build check for the new deps.
Author
Owner

Verification. Full-workspace cargo build is clean with the new deps, and I added an end-to-end lifecycle test (5f6122a) that drives the real handlers through one AppState with a capturing MailSender: register → read the verify link from the captured email → verify → login (session cookie issued) → request-reset → reset → old password rejected, new accepted. 102 hero_proxy_server tests green; fmt/clippy clean.

⚠️ Pre-existing, unrelated: cargo test --workspace currently fails to compile hero_proxy_examples/tests/integration.rsconnect() is annotated Result<_, OpenRpcError> but the generated connect_socket now returns herolib_openrpc::error::RpcError (SDK error-type drift). This is present on origin/development, not from the auth work; hero_proxy_server builds + tests clean. Worth a one-line fix on development independently.

Still open from before: live SMTP-over-TLS verification against a real server, and a musl-CI build confirmation for lettre/rustls on this branch.

**Verification.** Full-workspace `cargo build` is clean with the new deps, and I added an **end-to-end lifecycle test** ([`5f6122a`](https://forge.ourworld.tf/lhumina_code/hero_proxy/commit/5f6122a)) that drives the real handlers through one `AppState` with a capturing `MailSender`: **register → read the verify link from the captured email → verify → login (session cookie issued) → request-reset → reset → old password rejected, new accepted.** 102 hero_proxy_server tests green; fmt/clippy clean. ⚠️ **Pre-existing, unrelated:** `cargo test --workspace` currently fails to compile `hero_proxy_examples/tests/integration.rs` — `connect()` is annotated `Result<_, OpenRpcError>` but the generated `connect_socket` now returns `herolib_openrpc::error::RpcError` (SDK error-type drift). This is present on `origin/development`, **not** from the auth work; `hero_proxy_server` builds + tests clean. Worth a one-line fix on development independently. Still open from before: live SMTP-over-TLS verification against a real server, and a musl-CI build confirmation for `lettre`/`rustls` on this branch.
Author
Owner

Phase 3c — passwordless magic-link login (generic, for cm50 and any proxy-gated app). Commit 1940690.

Exactly the flow you described: request a link by email → click it → session cookie set → signed in. Reuses everything already built (single-use token store, mail seam, the same __Host- session cookie + X-Hero-* injection as password login).

  • GET/POST /auth/magic-link — mints a 15-min single-use magic_login token and emails the link; sends only when the address can sign in (existing account, or registration_mode=open so the click provisions one); same generic notice always (no account oracle).
  • GET /auth/magic?token= — consumes → finds (or, when open, auto-provisions a passwordless + verified) user → opens a session → sets the cookie → redirects.
  • Login page links to it; routes on the public allowlist.
  • 2 tests (auto-provision → login → single-use replay rejected; closed-signup unknown email sends nothing). 104 lib tests green; fmt/clippy clean.

So the proxy now offers three sign-in modes generically: password, OAuth/OIDC, and magic link — any auth_mode="local" route gets all of them. Next: the cm50 migration onto this (cm50_app:development_timur), and the hero_components auth-admin pane (pending coordination with @mik-tf, whose shell is actively changing).

**Phase 3c — passwordless magic-link login (generic, for cm50 and any proxy-gated app).** Commit [`1940690`](https://forge.ourworld.tf/lhumina_code/hero_proxy/commit/1940690). Exactly the flow you described: request a link by email → click it → session cookie set → signed in. Reuses everything already built (single-use token store, mail seam, the same `__Host-` session cookie + `X-Hero-*` injection as password login). - `GET/POST /auth/magic-link` — mints a 15-min single-use `magic_login` token and emails the link; sends only when the address can sign in (existing account, or `registration_mode=open` so the click provisions one); same generic notice always (no account oracle). - `GET /auth/magic?token=` — consumes → finds (or, when open, **auto-provisions** a passwordless + verified) user → opens a session → sets the cookie → redirects. - Login page links to it; routes on the public allowlist. - 2 tests (auto-provision → login → single-use replay rejected; closed-signup unknown email sends nothing). **104 lib tests green; fmt/clippy clean.** So the proxy now offers three sign-in modes generically: **password**, **OAuth/OIDC**, and **magic link** — any `auth_mode="local"` route gets all of them. Next: the cm50 migration onto this (`cm50_app:development_timur`), and the hero_components auth-admin pane (pending coordination with @mik-tf, whose shell is actively changing).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_proxy#63
No description provided.