oficina / docs Public
Local reauthentication navigation before Home linking
Status: design amendment approved by claude-fable-5 at dccdb746303b87f64f4156053943441ad2ca60af, 2026-09-08. Review used pinned source excerpts inline; it did not execute tests. Supersedes only the local-signin continuation paragraph of the approved Docs account-link design2e69825. The navigation helper is implemented on feat/local-link-navigation; application routing/configuration and whole-branch code review remain integration work.
Why the earlier continuation cannot work
The approved design says that local signin returns to a confirmation continuation only while a valid pending link exists. But linkstore.Start correctly requires an already fresh local proof. A stale user opening GET /account/home therefore has no valid link initiation to resume. Moreover, successful local reauthentication rotates the actual session token, so a previous initiation bound to the old token must not survive it.
The pinned Rastrillo v0.23.0 auth.Verify and auth.Callback call admit. admit mints a session with sessions.SignIn and redirects to the fixed Config.SignedInPath, default /. Neither auth handler reads returnto. sessions.SafeReturn exists in the sessions package, reads r.FormValue("returnto"), and is not called by auth. It neither persists navigation through email nor supplies a dynamic post-verification destination. Do not depend on it for this flow or intercept auth response bodies to infer verification success.
Navigation-only continuation
For suite-configured Docs only, set the local auth plugin's SignedInPath to the fixed /auth/local/complete route. Standalone Docs keeps its existing /. Ordinary suite-local signins without a continuation reach that fixed bridge and immediately return to /.
When a stale local session reaches GET /account/home, invalidate any previous link initiation/proof and issue a navigation-only marker before directing the person to the existing local signin page. The existing gateBegin already forces the actual magic-link method server-side and retains its normal allowlist and CSRF checks. Showing this GET must not send mail, initiate a link, bind a nonce, or create a proof.
The marker is an AEAD-sealed host-only Secure/HttpOnly/SameSite=Lax cookie using the reviewed shared sealer, a dedicated purpose and the exact configured Docs origin. It contains only version1, the fixed action link-start, issued-at and expiry. It contains no person ID, address, subject, local session token/hash, Home claim, link proof, assertion, or arbitrary return path. Its TTL is at most ten minutes. Decode only one cookie with a hard encoded-size bound (1024 bytes), canonical strict base64url, strict closed JSON fields, no trailing value, exact supported version/action, and issued/expiry bounds: issued>0, issued<=now<expiry, expiry-issued<=600 seconds. The cookie max-age does not replace sealed expiry validation. Missing, duplicate, tampered, malformed, unsupported or expired markers clear the marker and fall back to /.
/auth/local/complete is a GET navigation bridge. It clears the marker on every request. With a valid marker it resolves the actual newly presented session and checks freshLocalAuthentication against the current clock. Only a fresh local magiclink/keymail session can reach the fixed /account/home destination; missing, duplicate, refused, unsupported, stale or future session proof falls back to /. The bridge itself creates no session, calls no Home exchange and writes no identity link. No request parameter selects its destination. Replaying a copied marker can at most navigate to the same initiation page while the current local session is fresh; there is no replay-backed single-use or identity-proof claim for this cookie.
The /account/home page identifies the currently signed-in local account. An explicit fresh same-origin POST is still required to create a new linkstore initiation, bound to that actual current session and person, followed by the new shared Home nonce exchange and separate confirmation POST. A user who reauthenticated as another local account merely reaches that account's page; the old account's proof cannot transfer. A stale or changed initiation/proof is invalidated and never resumes after rotation. The original resource-return-path guarantee belongs to a newly initiated valid link's sealed state; this navigation marker stores no resource path and never resurrects a stale flow's path or proof.
Session source constraints
Rastrillo auth.SessionFrom(r) directly resolves the request cookie and returns signin.Identity, copying session Subject into Address even for Method=home-assertion. It is not a local-address resolver. sessions.From returns sessions.Session; auth.RequireSession also stashes this under sessions.Current. Use the typed Docs identity adapter for ownership and grants, and the method-aware local predicate for link proof.
sessions.SignIn(w,r,sess) revokes the presented old cookie's row, creates a fresh row, and sets a new cookie. It preserves supplied AuthTime, but its database create uses time.Now for created_at; supplied Session.At is ignored. The next bridge request sees the new cookie. Do not resolve the original verification request and mistake its old cookie for the newly minted session. A Home session's new At never becomes local proof because home-assertion is refused by the predicate.
Rastrillo v0.23.0 sessions.resolve and SignIn use r.Cookie, accepting the first matching cookie. Root integration must reject duplicate session cookies before sensitive resolution or mutation (including local completion and Home session replacement), and count the exact configured cookie name. Missing cookies remain normal for ordinary local signin, but cannot be used as a current local link proof. Keymail Callback similarly takes the first pending cookie; if kept reachable, its wrapper rejects duplicate pending cookies before completion. These are guards around existing APIs, not new identity mechanisms.
Acceptance
Actual local email verification with a valid marker returns through the fixed bridge to the initiation page; no Home or linkstore operation occurs until the explicit new POST. Ordinary local signin still lands /. Failed verification never reaches the bridge. Marker missing/duplicate/tamper/expiry/unknown fields, hostile query destinations and wrong origin purpose all fall back to /. Browser tests include JS off. A changed local account displays its own account before a new initiation. Stale prior proofs cannot be confirmed after session rotation; the original session token is revoked. A freshly minted Home session cannot use At to pass local freshness. A meaningful mutation must fail if the bridge skips the method/freshness guard or if a stale initiation resumes. Whole integration code review, actual CI and canary acceptance remain required.
Review implementation notes (non-blocking, to honor): explicitly guard duplicate session cookies before GET /auth/verify, GET /auth/callback, GET /auth/local/complete, the initiation/confirmation POSTs and Home exchange/session replacement. Check signin still renders to an already-signed-in user (the current h.signin renders directly without checking a session); preserve this in suite integration and JS-off browser acceptance. Set Cache-Control: no-store on the bridge and every marker-setting response. A cross-site top-level GET to /account/home may invalidate an existing flow; this deliberately fails closed by destroying a flow, never granting or creating one. Review result: /tmp/oficina-docs-local-continuation-review/review.ndjson.
Helper implementation: homelocalnavigation.go supplies newLocalLinkNavigation(sealer, sessions, origin), Begin and Complete. Begin clears the agreed _Host-docs-home-init and _Host-docs-home-proof browser pointers; old database rows remain bounded by their existing expiry and current-session/freshness binding. Complete only redirects and never calls linkstore. Tests include actual captured local email verification with session rotation, changed local account, ordinary signin without a marker, refused duplicate markers/sessions, strict encoding and expiry, wrong host/origin purpose, and local method/time boundaries. The test mail sender discards delivery after the app captures its synthetic message in memory.