Auth and UI Lift-Out: ontoref-daemon consumes ontoref-auth + ontoref-ui; JWKS and Introspect Exposed for SSO
Accepted
Context
The daemon historically owned its own session store, Argon2id password hashing, Tera template engine init, and cookie/Bearer extraction logic. Lian-build re-implemented the same surface independently. With ontoref-auth and ontoref-ui shipping as standalone foundation siblings (ADR-016 lift-out pattern), every daemon-internal copy is duplicate code maintained out of band: bug fixes, hardening, and Ed25519 token issuance for SSO had to be re-applied to each consumer. Federation across the family also required a common token vocabulary (issuer, kid, audience) that no daemon had so far. Without consolidation, the SSO mode that lets multiple panels share authentication remains unimplementable.
Decision
Migrate ontoref-daemon to depend on `ontoref-auth` (path dep, axum+persist features) and `ontoref-ui` (path dep, gated behind the existing `ui` feature). Re-export `Role` and `KeyEntry` from `ontoref_auth` through `crate::registry`; rewrite `crate::session` as a thin facade that preserves the historic daemon API surface (`SessionEntry`, `SessionStore`, `SessionView`, `RevokeResult`, `COOKIE_NAME`, `extract_cookie`) while delegating storage and lifecycle to `ontoref_auth::SessionStore`. Replace `tera::Tera::new(glob)` initialization with `ontoref_ui::init_tera(TeraOptions)`; `AppState.tera` now holds `Option<Arc<RwLock<ontoref_ui::TeraEnv>>>`. Daemon-specific page templates (manage.html, project_picker.html, dashboard.html, etc.) remain in `crates/ontoref-daemon/templates/` and continue to be loaded via the `template_dirs` consumer-override path. Add `TokenIssuer` (Ed25519, seed loaded from `ONTOREF_SIGNING_KEY_FILE` when set, ephemeral otherwise with WARNING) and `SignedTokenProvider` to `AppState`. Publish JWKS at `GET /.well-known/ontoref-keys.json` and introspection at `POST /.session/introspect` — both unauthenticated by SSO contract.
Constraints
- Hard ontoref-daemon source must not call tera::Tera::new directly — Tera initialization flows through ontoref_ui::init_tera
- Hard The daemon must publish its JWKS at /.well-known/ontoref-keys.json
- Hard The daemon must publish RFC 7662-shaped introspection at /.session/introspect
Alternatives considered
- Wholesale call-site migration to ontoref_auth::AuthUser extractor and ontoref_auth::Session — rejected: The daemon's AuthUser is multi-project: it extracts a Path<String> slug and validates that the session's scope matches the requested project. ontoref_auth::AuthUser is single-tenant — it resolves a credential to an Identity without slug-awareness. Replacing the daemon's extractor would require either (a) duplicating slug validation in every handler, or (b) wrapping ontoref-auth's extractor with a daemon-specific layer that re-does the slug check. Both are strictly more code than keeping the daemon's slug-aware extractor and routing it to the shared SessionStore.
- Bridge ontoref_auth::Role and registry::Role with a daemon-local enum + From impls — rejected: The variants and serde representations are identical. The two-enum bridge would force every keys-overlay.json reader, every projects.ncl serializer, and every UI template that reads the role string to perform conversions. The re-export keeps the wire format identical and the type stable.
- Embed ontoref-ui's full base.html and override only nav fragments — rejected: ontoref-ui's base.html does not have block hooks for the daemon's domain ontology widget, registry topology bar, or vault state row. Either ontoref-ui grows daemon-specific blocks (which violates the harmonization-first lift-out principle — A.2 explicitly keeps generic core/), or the daemon defines its own base.html that includes ontoref-ui's partials piece by piece. The full-override path the daemon already takes is simpler and preserves the exact pre-migration rendering.
- Defer JWKS until the SignedTokenProvider has a key rotation policy implemented in code — rejected: TokenIssuer::rotate() is already implemented in ontoref-auth and exposes JwksSnapshot containing both current and previous public keys. The policy decision (when to rotate, when to forget_previous) is operational, not structural — it lives in the rotation playbook, not in the JWKS endpoint code. Shipping JWKS now with an ephemeral default unblocks SSO; the rotation policy ships as runbook content alongside the panel daemon's adoption of SSO mode.