| 1 | # Connect an app to Home |
| 2 | |
| 3 | `connect` lets an app keep its existing local accounts and accept Home sign-in |
| 4 | after the person explicitly links both identities. The app still decides who |
| 5 | may enter and what they may access. |
| 6 | |
| 7 | Load optional settings with `CredentialsFromEnv("SHEETS")`, then validate them |
| 8 | with `DecodeCredentials(settings, origin)` once you know the app's origin. |
| 9 | The variables end in `_HOME_ISSUER`, `_HOME_CONSUMER_ID`, `_HOME_KEYS_BASE64` |
| 10 | and `_HOME_DEK_BASE64`. All empty means disabled; a partial configuration fails. |
| 11 | `HOME_DIRECTORY_ORIGIN` (unprefixed, the name Home itself reads) optionally |
| 12 | supplies the hub directory origin as `Credentials.Directory`, for an app switcher |
| 13 | and "All apps" link. It enables nothing on its own and must be a bare https origin. |
| 14 | Use a dedicated random 32-byte storage key and a distinct namespace per app. |
| 15 | Keep this key stable: it protects proof state and keyed local-person bindings. |
| 16 | Replacing it without a migration makes existing links unusable; rotation requires |
| 17 | an explicit migration with both keys, which this package does not yet provide. |
| 18 | |
| 19 | Apply `handoff.Migrations` and `linkstore.Migrations` using your framework's |
| 20 | migration package. The latter's namespace is `linkstore.SchemaNamespace`. |
| 21 | This package imports neither framework session nor migration types, so old |
| 22 | and new Rastrillo apps can share the same protocol. |
| 23 | |
| 24 | Construct `Bridge` with the hooks in `Config`. Read the actual session cookie; |
| 25 | `MintHomeSession` stores the opaque Home subject with method `HomeMethod` and |
| 26 | the supplied authentication time. `LookupLocalSession` reads an existing |
| 27 | local account. `LookupPerson` uses its supplied SQL query object and rechecks |
| 28 | current admission; return `sql.ErrNoRows` for missing or disallowed accounts. |
| 29 | Neither hook creates accounts, samples or identity mappings. The returned label |
| 30 | must be the canonical verified local address, never an editable display name. |
| 31 | Local IDs must never be reused, even for a new account with the same address. |
| 32 | Delete the account's `home_identity_links` rows in the same transaction as the |
| 33 | local account. The shared schema cannot impose a foreign key on app-owned tables. |
| 34 | A keyed binding detects reuse with a different address at every proof stage and |
| 35 | resolution. Address changes require explicit removal and fresh linking; old or |
| 36 | unbound links fail closed, rather than adopting the current account. |
| 37 | |
| 38 | Session cookie values must be exactly 43 characters of canonical unpadded |
| 39 | base64url encoding of 32 bytes. `Session.AuthTime` is the method's actual |
| 40 | verification time. `Session.At` is immutable verified-session creation time, |
| 41 | never last-seen time or request time: legacy magic-link sessions can use it when |
| 42 | `AuthTime` is absent. Keymail sessions cannot use that fallback. |
| 43 | |
| 44 | On every authenticated app request, resolve Home sessions through |
| 45 | `Bridge.ResolveSession`. Use the returned local ID for ownership and grants. |
| 46 | Never pass an opaque Home subject through a helper that treats subjects as |
| 47 | email addresses or creates a local account. Keep ordinary local sign-in's |
| 48 | existing behavior in the app's local authentication path. |
| 49 | |
| 50 | `Routes` supplies shared navigation, local reauthentication markers and the |
| 51 | narrow callback exception to your app's CSRF middleware. Mount its methods |
| 52 | alongside `Bridge.Start`, `Callback`, `Finish` and `ConfirmPage`. The local |
| 53 | plugin's successful return must be `LocalCompletePath`. Its pending cookie must |
| 54 | be `__Host-rastrillo_pending`, and the local entry is `/signin/local`. |
| 55 | Mount exactly `Bridge.Callback` at `POST /auth/assertion`: this path alone bypasses |
| 56 | CSRF protection because the bridge verifies its browser-bound assertion. |
| 57 | `LocalLandingPath` optionally sets a validated local destination after ordinary |
| 58 | local sign-in (default `/`); explicit account-link navigation takes precedence. Render shared pages |
| 59 | inside the app's existing Estilo layout; all paths work without JavaScript. |
| 60 | Apps with additional content-security directives must supply their existing |
| 61 | policy with the validated Home issuer added to `form-action`. |
| 62 | |
| 63 | `Routes.Signin` sends a person who already has a live session straight to the |
| 64 | validated `return_to` (or `/`) instead of starting another exchange. `Routes.Signout` |
| 65 | ends the app session and then answers `307` to `{Issuer}/signout`, which ends the |
| 66 | Home sign-in session too and returns the browser to the app's `/signed-out`. Home |
| 67 | admits that hop only from an origin registered in its `HOME_CONSUMERS`, by |
| 68 | `Referer` when the browser nulls `Origin`, so a page showing Sign out must not send |
| 69 | `Referrer-Policy: no-referrer` (use `strict-origin`), and a custom policy must keep |
| 70 | the issuer in `form-action`. |
| 71 | |
| 72 | Wrap the trusted local plugin's Begin handler with `OAuthContinuation` when it |
| 73 | can redirect to Keymail. Render the supplied destination with `OAuthPage` and the |
| 74 | shared `home-oauth-head` and `home-oauth` templates inside your existing layout. |
| 75 | The helper validates the client origin, `/auth/callback`, scope and PKCE method, |
| 76 | then ends the POST with a no-store continuation page so Safari does not apply |
| 77 | form-action to Keymail's redirect chain. It does not discover or trust arbitrary |
| 78 | request-provided destinations. Keep the existing local plugin callback route. |
| 79 | |
| 80 | ## Extraction record |
| 81 | |
| 82 | The link proof protocol, bridge, freshness rules and navigation originate in |
| 83 | Oficina Docs commit `3fffd1be535ec87ab6e92da753046a4f1e4c754c`, whose tree |
| 84 | matches reviewed deployment `a8f5c0e1d67ad042e89793aa941135eacd98aa23`. |
| 85 | The original files live under `internal/docs/home_*` and `internal/docs/linkstore`. |
| 86 | This extraction preserves the consumed proof stages, nonce/session binding, |
| 87 | clock watermark, freshness windows and encrypted flow purpose strings. |
| 88 | |
| 89 | Changes are the neutral session DTO and app lookup hooks, new `home_*` link |
| 90 | tables, app-selected encryption namespace, and shared rendering/navigation. |
| 91 | The local-person foreign key becomes a transaction-scoped existence/admission |
| 92 | lookup, because apps use different local identity tables. Every link write |
| 93 | and subsequent resolution still checks the current local account. Additive |
| 94 | migration 0002 adds a keyed person binding; earlier unbound rows fail closed. |
| 95 | OAuth continuation also derives from Docs `internal/docs/auth_navigation.go`. |
| 96 | |
| 97 | The original Docs state-machine tests were ported with synthetic session and |
| 98 | local-account hooks. The app-owned content-preservation test now checks an |
| 99 | unchanged content row; framework CSRF coverage belongs to the shared Routes |
| 100 | and each app's router integration tests. Existing Docs tables and bytes are |
| 101 | untouched. This is not a migration path for an already deployed Docs instance. |
| 102 | |