feat: add browser auth shell
This commit is contained in:
@@ -134,8 +134,8 @@ remain the sole authority for authorization regardless of what a browser loaded.
|
||||
|
||||
### The browser host mounts module UI; it grants nothing
|
||||
|
||||
Ekklesia's shell (`surfaces/shell`) discovers installed module UIs through
|
||||
`GET /api/v1/module-ui` and mounts them with the generic loader
|
||||
Ekklesia's shell (`surfaces/shell`) discovers installed module UIs through the resolved
|
||||
`GET /api/v1/dashboard/navigation` view and mounts each embedded host-neutral descriptor with the generic loader
|
||||
([ADR 0026](../architecture/decisions/0026-minimal-ekklesia-module-ui-host.md)). Three properties are
|
||||
security-relevant.
|
||||
|
||||
@@ -158,6 +158,44 @@ shell owns the container, the loading state, and every error, so a failing modul
|
||||
rather than the shell. None of that is containment: once imported, a module UI is ordinary same-realm
|
||||
JavaScript, exactly as described above.
|
||||
|
||||
### Dashboard authentication stays outside module code
|
||||
|
||||
[ADR 0034](../architecture/decisions/0034-host-owned-browser-authentication.md) makes the Dashboard
|
||||
surface — not a module — the owner of login, logout, and session-transition UX. Principal resolution
|
||||
still precedes every protected navigation request and module mount. The public module UI context
|
||||
remains `moduleId`, scoped API, narrow principal, permission hints, and realtime; it receives no
|
||||
credential, cookie, token, auth endpoint, or sign-out callback.
|
||||
|
||||
The browser posts email/password to Better Auth's existing `/api/auth/sign-in/email` endpoint with
|
||||
credentials included. Better Auth establishes the HttpOnly cookie, but its native success JSON also
|
||||
contains a redundant session token and user object. `apps/api` therefore normalizes any body-bearing
|
||||
successful `2xx` on that exact route to fixed `{"ok":true}` while preserving the status and
|
||||
`Set-Cookie`; browser application JavaScript never receives the token and must resolve
|
||||
`/api/v1/principal` before authenticated rendering. The richer token-bearing native paths
|
||||
`/api/auth/get-session`, `/api/auth/list-sessions`, `/api/auth/update-session`, and
|
||||
`/api/auth/change-password` are deliberately unavailable behind the same sanitized `404` as an
|
||||
unsupported route. The last path is denied even though password/account management is out of scope:
|
||||
the pinned endpoint may return a new raw session token when revoking other sessions. The form clears
|
||||
the submitted password, suppresses duplicate attempts, uses fixed error messages, and does not log
|
||||
credentials, raw responses, or caught exceptions.
|
||||
|
||||
Sign-out calls `/api/auth/sign-out` and then re-resolves the principal; hiding the shell alone is
|
||||
never treated as invalidation. Leaving authenticated state removes the active module frame, so its
|
||||
existing unmount closes API/realtime work without a module-specific auth event. A `401` from the
|
||||
platform-owned bootstrap contract maps directly to unauthenticated; a platform navigation `403`
|
||||
receives a principal recheck for the principal/navigation expiry race. Module responses are not host
|
||||
session authority: either module `401` or `403` triggers one deduplicated principal revalidation,
|
||||
while the module retains the original response. Only anonymous unmounts the module and shows login;
|
||||
authenticated preserves the session and mounted module. A stale response from an already-unmounted
|
||||
frame is ignored, and network/server failures do not fabricate logout.
|
||||
|
||||
The flow remains at the current Dashboard URL and accepts no callback/return target, so this pass
|
||||
adds no open-redirect input. It exposes no signup or recovery link: production signup is prohibited
|
||||
and no email delivery/recovery system exists. The existing cookie stays host-only; exact trusted
|
||||
origins, credentialed CORS, and explicitly enabled Better Auth origin/CSRF checks remain the
|
||||
boundary. Broadening the cookie to a parent domain or inventing a Dashboard hostname would be a
|
||||
separate deployment and threat-model decision.
|
||||
|
||||
### A display page is authorized; its artifact bytes are not authority
|
||||
|
||||
Manifest version 6 binds a display artifact to one permission declared by the same module
|
||||
@@ -445,7 +483,9 @@ This checklist gates a **public launch**. It does not gate development: Surfaces
|
||||
work all proceed against the local development infrastructure while every item below is still
|
||||
outstanding.
|
||||
|
||||
1. The reverse-proxy path serves the API successfully.
|
||||
1. The reverse proxy serves the built Dashboard assets and `/api` on the intended origin, or an
|
||||
explicitly split origin is covered by the exact trusted-origin/CORS configuration. The
|
||||
repository currently supplies neither deployment.
|
||||
2. The origin port is unreachable from the public Internet.
|
||||
3. The origin port is also unreachable from any untrusted network/VLAN — only the intended private
|
||||
proxy path connects. Security must not depend on an unpublished DNS name or origin IP.
|
||||
@@ -458,6 +498,13 @@ outstanding.
|
||||
7. Proxy logs do not record cookies, authorization values, request bodies, or raw forwarding chains.
|
||||
8. PostgreSQL is reachable only from the API host, over TLS, with the certificate and hostname
|
||||
verification Ekklesia already requires.
|
||||
9. The deployed login/logout flow preserves the host-only HttpOnly cookie, returns fixed
|
||||
`{"ok":true}` rather than a token on successful sign-in, keeps the four token-bearing native
|
||||
session/password paths unavailable, and accepts no external/protocol-relative post-login
|
||||
destination.
|
||||
10. The installation has a supported, audited way to create its first production identity before
|
||||
consuming the single-use platform-owner bootstrap. This repository does not provide that way
|
||||
yet.
|
||||
|
||||
### Rate limiting is per-process today
|
||||
|
||||
@@ -471,6 +518,10 @@ If either changes — public signup is enabled, or a second instance is added
|
||||
edge rate limiter becomes mandatory, and it must be proven that its counters are shared across
|
||||
instances, survive process restart, and cannot be re-bucketed by a spoofed client-IP header.
|
||||
Ekklesia does not ship one. Do not describe the current limiter as distributed throttling.
|
||||
Its production sign-in/sign-up-prefixed rule is three attempts per ten seconds, and the
|
||||
server-owned client address prevents a spoofed forwarding header from selecting a fresh bucket.
|
||||
Those facts still do not make counters durable across restart or sufficient across replicas; the
|
||||
public Dashboard login must be assessed with the actual edge/proxy controls before launch.
|
||||
|
||||
## Continuous integration boundary
|
||||
|
||||
|
||||
Reference in New Issue
Block a user