lichen.page.review from the reviewed point 0b92cb61 to stack eb5a1653, as one PR per subsystem (review set)

full 14: Front server: auth, hosts and routing #77

targeting full/13-rooms
Open opened by and assigned to notplants-bot.bsky.social
targeting full/13-rooms

The front server's sign-in, sessions and routing. A server with a dashboard now answers three roles of host: the dashboard, each site's edit host (reached through a one-time handoff from the dashboard), and the site's published host. Every site request is forwarded to the site's own process once the site's permission group admits it; since FULL-SITE-PROCESS that includes what the front server used to answer itself (a publish, the editor's git panel, clients' git). Several fixes from the security-table review land here too: same-origin checks for state-changing requests, a rate limit on password sign-ins, a verified CLI atproto login, and stricter session handling.


AST-verified changes

  • auth/mod.rs → auth.rs, auth/atproto/mod.rs → auth/atproto.rs, auth/file/mod.rs → auth/file.rs and auth/oidc/mod.rs → auth/oidc.rs: four of OPERATOR-REVIEW-FIXES U1's 19 foo/mod.rs renames (f6a10c32). Run id not recorded: "3004 items, 48 moved, 69 carried, 19 renamed, 2 visibility as mapped, 233 module declarations" (lichen-builder.md 2087); adversary notify-abab368a, same counts.
  • try_refresh_token from the front server's git/hooks.rs (#23) to credentials.rs (P2c-2a seam 2, 6ec79159). "move 6ec79159 ok (1 moved, 0 visibility), plant refused" (lichen-builder.md 2339, run id not recorded); adversary "equal=true, 1452 items, 1 moved, 0 visibility".
  • ApiTokenPermissionScope, ApiTokenAuthenticatedUser and unsafe_action_guard from auth/token.rs to a new token_scope.rs (P2c-2a seam 4, 499e34df), which P2c-2b commit 1a then moved to lichen-site (#37). The builder's count is not recorded; adversary "equal=true 1457 items 3 moved 0 visibility (matches builder)".
  • AuthProvider from users.rs to lichen-site's owner.rs, and random_hex from auth/token.rs to lichen-site's utils.rs (#37; P2c-2b commit 0, 97e70426). The builder's count is not recorded; adversary "moved4/vis1". Allowed: one pub(crate) → pub widening, declared in the map.
  • SiteDomain and SiteDirectory from server.rs to lichen-site's extensions.rs (#37; P2c-2b commit 1a, ff26fefc). "5 moved, 48 carried, 8 files renamed, 19 visibility as mapped" (lichen-builder.md 2350, run id not recorded); adversary 5/19.
  • SECURITY-FOLDER-AND-PRINCIPALS unit 1 (46529b05): same_origin.rs, session_scope.rs, host_roles.rs, handoff.rs and credentials.rs into lichen-server's security/, part of seven product files moved there with their test files (12 files renamed; crate:: paths rewritten in lichen-server). Checked by notify-eebdd32e: "items 2471/2471, moved 0, stayed 2343, carried 128, files_renamed 12, visibility_changes 0, comments_compared 2429, module_declarations_checked 147, errors 0"; adversary rerun (19:08Z, its checker built from ast-checker/): both runs "equal=True (2471/2471, renamed 12, visibility 0, errors 0)". The rustfmt (d6c25912, proven by cargo fmt) and the non-move edits (bd48e739: literals) are separate commits.

Roughly 320 of this PR's 4,160 changed lines are code these moves carried verbatim, mostly what left auth/token.rs and users.rs. With rename detection the mod.rs renames show only their changes.

On this forge the 4 renamed files here show as a whole deletion and a whole addition, so its diff reads 5,622 changed lines; with rename detection (git diff -M full/13-rooms full/14-front-auth) it is 4,160.


Hosts and roles (ADMIN-EDIT-HOST-SPLIT). The dashboard is dash.<domain> (admin). Each site has an edit host, <site>.edit.<domain> or edit.<custom domain>, which serves the editor only. On the site's published host, /cms editing is also allowed (operator note N10). An edit host has its own session cookie, bound to that host and its one site; the owner signed in on the dashboard reaches it through a single-use handoff code, posted in a top-level form and never put in a URL. Each role sets its own framing headers (frame-ancestors, X-Frame-Options, and Origin-Agent-Cluster on lichen's own hosts). server.rs dispatches by host, and every route is in its permission group (#18).

Sites are served by their processes (PER-SITE-PROCESS, SITE-FRONT-BOUNDARY 11). The front server admits every site request and forwards it to the site's process, started if needed, once the site's group admits it, together with the identity it was admitted as. Since FULL-SITE-PROCESS (6c–6h) that includes what it answered itself until then: a publish and its record (publish/front.rs, #25), the editor's git panel, clients' smart-HTTP git (to the hub the process serves), and, on a single site (which has no dashboard), its admin acts (iroh peers, collaborators, revert), forwarded with Role::Admin (single_site_admin_acts, FULL-SITE-PROCESS 3e), and its compact, force-pull and force-push (single_site_rewrite_router). A revert on a multi-site host is refused there: it is a dashboard act. The site's server routes moved under /cms/api (item 6's one URL scheme).

Fixes from the security-table conformance review, each named in the code:

  • STC-CSRF1: a browser's state-changing request must come from a page of this host, because the session cookie is shared with sibling sites on a dashboard domain.
  • Password sign-ins are rate-limited per client and account, and per client alone, on account login, token login and the share-password login. trusted_proxies says whose X-Forwarded-For to believe.
  • STC-ATPROTO1: the CLI's atproto login is verified: the handle against the DID, a loopback client id, and the grant stored as the server refreshed it. atproto discovery follows no redirect, so a redirect cannot reach a private address.
  • TOKEN-SESSION1: a bearer token's identity applies to that request only; it no longer leaks into the session cookie.
  • STC-GUEST1 / STC-FIXATION1: a share guest is signed out when the share password changes, and every login cycles the session id.

Files (crates/lichen-server/src/)

  • security/host_roles.rs — new: which host is which role, and the edit-host name rules. Read this first.
  • server.rs — host dispatch, role headers, the edit host's router, the forwarding router to site processes, the forwards of clients' smart-HTTP git, the editor's git panel and a single site's rewrites and admin acts, permission groups on every router.
  • security/handoff.rs — new: the dashboard-to-edit-host sign-in code (H2a).
  • security/same_origin.rs — new: STC-CSRF1.
  • security/session_scope.rs — the cookie's name and scope, binding a login to its host, the guard.
  • auth.rs — 401 for the API and a redirect otherwise, the share guest's session bound to the password hash (read by the site's domain from its area), session-id cycling.
  • auth/login_limit.rs — new: the rate limit.
  • auth/token.rs — TOKEN-SESSION1, the CLI's grant stored as refreshed, and the scope types moved out.
  • auth/atproto/cli_login.rs — new: what the CLI's login must prove.
  • auth/atproto/client.rs — the no-redirect client, and discovery checked as public.
  • auth/atproto/handlers.rs, auth/atproto/discovery.rs, auth/oidc/handlers.rs, auth/atproto.rs, auth/oidc.rs, auth/file.rs — routes in groups, the loopback client id, fixation on OIDC login, renames.
  • security.rs — new in this round: the security/ module list. Its seven members stay with their subjects: five here, access.rs in #18, share.rs in #20.
  • security/credentials.rs — new: the owner's atproto session, refreshed here and lent to the site process's admin thread (ToSite::Credentials) for its push to a pds:// remote and its standard.site records; the refresh token stays here. A new grant is lent again (owner_session_changed, in auth/token.rs and auth/atproto/handlers.rs).
  • users.rs — read_owned for blocking workers; AuthProvider moved out.

Lives elsewhere

  • security/access.rs, the front server's permission policy: #18. security/share.rs, the share password: #20. Taking the session cookie off what is forwarded to a site process (STC-FWDCRED1): site_process/front.rs in #19.
  • The sign-in pages, drawn by React on the server (sign_in_render.rs): #33.
  • server_settings.rs (trusted_proxies) and the single-site owner claim in state.rs: #31.
  • docs/HOSTS.md, which describes the hosts for operators, is in the base commit.
  • Tests (auth/tests/, tests/security__same_origin.rs, security__host_roles.rs, security__handoff.rs, server.rs, e2e) are in the base commit.

Category: Front server: auth, hosts and routing Size: 19 files, +3,289/−871 Rounds: round 0 at stack ae42e3c0; round 1 at 6b650b2a (SECURITY-FOLDER-AND-PRINCIPALS): five files moved into security/ (unit 1), with the new module root security.rs placed here, where most of its members are; path rewrites and rustfmt in server.rs and auth*.rs. Read that round in its interdiff: https://tangled.org/notplants-bot.bsky.social/lichen.page.october/pulls/30/round/1/interdiff; round 2 at b66397c0 (FULL-SITE-PROCESS): server.rs forwards what moved into the site process (clients' smart-HTTP git to its hub; the editor's git panel and a single site's admin acts and rewrites to its admin thread; the publish through #25's publish/front.rs); the share password's hash is read by domain; security/credentials.rs lends the owner's session to the site process, and a new grant is lent again. Read this round in its interdiff: https://tangled.org/notplants-bot.bsky.social/lichen.page.october/pulls/30/round/2/interdiff Plans and ledger: FULL-SITE-PROCESS-2026-10-25.md (6c–6e, 6h); ADMIN-EDIT-HOST-SPLIT-2026-10-13.md (H1, H1e, H2a, H3), OPERATOR-REVIEW-NOTES-2026-10-13.md (N10), SECURITY-TABLE-CONFORMANCE-2026-10-11.md and -2-2026-10-13.md, PER-SITE-PROCESS-2026-10-09.md (P2b, P2c-2a, P2c-2b), SITE-FRONT-BOUNDARY-2026-10-17.md (9b, 11b1, 11e11), OPERATOR-REVIEW-FOLLOWUPS-2026-09-30.md (item 6's URL scheme), OPERATOR-REVIEW-FIXES-2026-10-07.md (U1); lichen-builder.md 2087, 2339, 2350. Inline #[cfg(test)] modules: none. Twelve files point to a test file with #[path]; auth.rs and auth/file.rs have test-only items. Bucket moves: none against round 1. As in round 1, auth/atproto/* and security/handoff.rs are here rather than where the starting map put them (iroh/sync and site area, by name). Round 1: #10 (Front server: auth, hosts and routing) covered these files from 0b92cb61; auth/file.rs and auth/oidc.rs are new in this range (renames).

This pull request has no versions submitted yet.