diff --git a/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-02-SUMMARY.md b/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-02-SUMMARY.md new file mode 100644 index 00000000..b6047f4f --- /dev/null +++ b/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-02-SUMMARY.md @@ -0,0 +1,300 @@ +--- +phase: 19.5-social-login-in-sm-user-plugin +plan: 02 +subsystem: auth +tags: [goth, oauth, social-login, sm-sociallogin-plugin, aes-gcm, cooler, release, changelog] + +# Dependency graph +requires: + - phase: 19.5-01 + provides: "goth v1.82.0 approval, cooler.Cache.Pull, sm-user-plugin OAuthProviderSource, FinishLogin, RegisterOptions.PasswordLess, PHP last-identity unlink" +provides: + - "New repository golem15/sm-sociallogin-plugin (private), module git.golem15.com/golem15/sm-sociallogin-plugin, package sociallogin, plugin golem15.sociallogin, tag v0.1.0" + - "GET /oauth/{provider} and /oauth/{provider}/callback (throttle:20,1) for google, facebook, github, microsoft; sealed AES-256-GCM state cookie summer_oauth_" + - "GET /_user/api/v1/oauth-complete, POST /_user/api/v1/oauth-register-complete, POST /_user/api/v1/oauth-link-ticket" + - "Provider, Profile, ProviderConfig, Factory, Registry (Register, Names, Label, Build, Has); goth only in classes/providers_goth.go" + - "Admin settings singleton golem15_sociallogin_settings (SettingsCode sociallogin) with encrypted write-only secrets overriding config per field" + - "summercms v0.3.1 tag (pushed, changelog post), sm-user-plugin v0.1.4 tag (pushed)" +affects: [19.5-03, 19.5-04, sm-demo-app, sm-bm-app, fonoteka.go, sm-summercmsio-app] + +actuals: + tokens: 46300 + tasks: 6 + commits: 6 +plan_head_before: null +plan_head_after: adfcfbff0af65c3b0eee19ab55d02ffec13e0604 +# No ledger in summercms.go: this plan made no code commit there (only this SUMMARY). +# Measured per repo: +# sm-sociallogin-plugin: created by this plan, git rev-list --count master = 5 (root c15e8b9 .. adfcfbf) +# sm-summercmsio-app: git rev-list --count 752099d..8581576 = 1 (unpushed) +# tokens: chars/4 over the tracked files of sm-sociallogin-plugin without go.sum (185152 chars), plus a one-line script change. + +tech-stack: + added: + - "github.com/markbates/goth v1.82.0 (core + google, facebook, github, azureadv2; never gothic), sm-sociallogin-plugin only" + - "golang.org/x/oauth2 v0.36.0 (token exchange errors), sm-sociallogin-plugin only" + patterns: + - "Plugin-owned provider interface and Registry; third-party OAuth library wrapped in one file so no exported identifier mentions it" + - "Per-browser OAuth state sealed with AES-256-GCM under an HKDF key from the app key, AAD the cookie name" + - "One-time handoff codes in cooler consumed with Cache.Pull; validation before the pull so a 422 never consumes" + - "Config defaults overridden per field by a settings singleton read per request" + +key-files: + created: + - ../sm-sociallogin-plugin/plugin.go + - ../sm-sociallogin-plugin/routes.go + - ../sm-sociallogin-plugin/config.go + - ../sm-sociallogin-plugin/service.go + - ../sm-sociallogin-plugin/admin_settings.go + - ../sm-sociallogin-plugin/admin_permissions.go + - ../sm-sociallogin-plugin/classes/provider.go + - ../sm-sociallogin-plugin/classes/config.go + - ../sm-sociallogin-plugin/classes/credentials.go + - ../sm-sociallogin-plugin/classes/statecookie.go + - ../sm-sociallogin-plugin/classes/callback.go + - ../sm-sociallogin-plugin/classes/codes.go + - ../sm-sociallogin-plugin/classes/flow.go + - ../sm-sociallogin-plugin/classes/registration.go + - ../sm-sociallogin-plugin/classes/messages.go + - ../sm-sociallogin-plugin/classes/providers_goth.go + - ../sm-sociallogin-plugin/controllers/redirect.go + - ../sm-sociallogin-plugin/controllers/callback.go + - ../sm-sociallogin-plugin/controllers/complete.go + - ../sm-sociallogin-plugin/controllers/register_complete.go + - ../sm-sociallogin-plugin/controllers/link_ticket.go + - ../sm-sociallogin-plugin/controllers/helpers.go + - ../sm-sociallogin-plugin/models/registry.go + - ../sm-sociallogin-plugin/models/settings.go + - ../sm-sociallogin-plugin/models/settings/fields.yaml + - ../sm-sociallogin-plugin/updates/registry.go + - ../sm-sociallogin-plugin/updates/202610200001_create_golem15_sociallogin_settings.go + - ../sm-sociallogin-plugin/config/config.yaml + - ../sm-sociallogin-plugin/lang/en/lang.yaml + - ../sm-sociallogin-plugin/lang/pl/lang.yaml + - ../sm-sociallogin-plugin/testmain_test.go + - ../sm-sociallogin-plugin/harness_test.go + - ../sm-sociallogin-plugin/fakeprovider_test.go + - ../sm-sociallogin-plugin/tracer_smoke_test.go + - ../sm-sociallogin-plugin/flow_smoke_test.go + - ../sm-sociallogin-plugin/link_smoke_test.go + - ../sm-sociallogin-plugin/providers_smoke_test.go + - ../sm-sociallogin-plugin/settings_smoke_test.go + - ../sm-sociallogin-plugin/README.md + - ../sm-sociallogin-plugin/go.mod + - ../sm-sociallogin-plugin/go.sum + - ../sm-sociallogin-plugin/go.work + - ../sm-sociallogin-plugin/.gitignore + modified: + - ../sm-summercmsio-app/scripts/changelog-post.py + +key-decisions: + - "Task 5 reply: release v0.3.1 (Recommended); framework v0.3.1, sm-user-plugin v0.1.4, sm-sociallogin-plugin v0.1.0, private repository" + - "Changelog blocker reply: fix-ua (Recommended); changelog-post.py sends User-Agent summercms-changelog-post/1 (Cloudflare bans Python-urllib)" + - "sm-user-plugin tagged v0.1.4, not the planned v0.1.2 (v0.1.2 and v0.1.3 already existed)" + - "SettingsCode is sociallogin (cabana rejects dotted settings codes)" + - "The pending registration code is pulled inside the final DB transaction after every 422 check; a lost race answers 410, a 422 never consumes" + - "Inactive or banned users get the real messages (A7); Polish copy for every message, PHP Polish byte-for-byte where PHP has it" + - "providers.microsoft.tenant defaults to empty, which falls back to common" + +patterns-established: + - "Plugin-owned provider interface wrapping a third-party OAuth library in a single file" + - "Sealed per-browser state cookie with HKDF purpose label separate from lagoon's" + +requirements-completed: [SC-1, SC-2, SC-3, D-01, D-02, D-03, D-04, D-05, D-06, D-07, D-08, D-09, D-10, D-11, D-12, D-14, D-16, D-17, D-18, D-19, D-20, D-21, D-22, D-23] + +coverage: + - id: D1 + description: "Returning user signs in through a fake provider: sealed state, identity match, one-time oauth-complete, oauth-providers lists the provider" + requirement: "SC-1" + verification: + - kind: integration + ref: "../sm-sociallogin-plugin/tracer_smoke_test.go#TestTracerSmoke (Postgres)" + status: pass + human_judgment: false + - id: D2 + description: "Email matching, pending registration, account creation at the callback and at oauth-register-complete, consent, throttle, beta gate, token cookie" + requirement: "SC-2" + verification: + - kind: integration + ref: "../sm-sociallogin-plugin/flow_smoke_test.go#TestFlowSmoke (5 subtests, Postgres)" + status: pass + human_judgment: false + - id: D3 + description: "Linking through the link ticket and bearer; start-request errors land on the SPA" + requirement: "SC-3" + verification: + - kind: integration + ref: "../sm-sociallogin-plugin/link_smoke_test.go#TestLinkSmoke (8 subtests, Postgres)" + status: pass + human_judgment: false + - id: D4 + description: "Google, Facebook, GitHub and Microsoft wrappers with the D-11 email-verified rules against fake provider APIs" + requirement: "D-11" + verification: + - kind: unit + ref: "../sm-sociallogin-plugin/providers_smoke_test.go#TestGothProvidersSmoke (8 subtests, no Docker)" + status: pass + human_judgment: false + - id: D5 + description: "Admin settings override config per field; secrets never returned by the admin JSON" + requirement: "D-16" + verification: + - kind: integration + ref: "../sm-sociallogin-plugin/settings_smoke_test.go#TestSettingsOverrideSmoke (Postgres)" + status: pass + human_judgment: false + - id: D6 + description: "Remote operations in D-21 order: framework tag + changelog, sm-user-plugin push + tag, new repository + tag" + requirement: "D-21" + verification: + - kind: other + ref: "git ls-remote --exit-code --tags origin v0.3.1 (summercms.go); ... v0.1.4 (sm-user-plugin); ... v0.1.0 and refs/heads/master (sm-sociallogin-plugin); curl https://summercms.io/changelog/summercms-0-3-1 = 200" + status: pass + human_judgment: true + rationale: "The release train needed the human go-ahead (Task 5) and a decision on the changelog blocker (fix-ua)" + +duration: 25min +completed: 2026-10-21 +status: complete +--- + +# Phase 19.5 Plan 02: Social sign-in plugin Summary + +**New private repository sm-sociallogin-plugin v0.1.0: PHP-compatible /oauth/{provider} sign-in for Google, Facebook, GitHub and Microsoft through goth wrapped in one file, a sealed AES-256-GCM state cookie, one-time SPA handoff, pending registration and link-ticket linking, released after summercms v0.3.1 (changelog posted) and sm-user-plugin v0.1.4** + +## Performance + +- **Duration:** about 25 min from the first plan commit (2026-10-20T21:42:34Z) to the last tag push (about 22:06Z), not counting the work before the first commit; includes the Task 5 and changelog checkpoint waits +- **Started:** 2026-10-20T21:42:34Z (first plan commit) +- **Completed:** 2026-10-20T22:06:00Z (2026-10-21 local) +- **Tasks:** 6 of 6 (Task 5 is the human go-ahead) +- **Files:** 43 tracked files created in sm-sociallogin-plugin, 1 modified in sm-summercmsio-app + +## Accomplishments + +- sm-sociallogin-plugin requires `golem15.user`; an application without it has no goth dependency, no /oauth routes and no sociallogin config (framework go.mod has no goth). +- Its own `Provider`/`Profile`/`ProviderConfig`/`Registry`; goth imported only in `classes/providers_goth.go`; no gothic, no gorilla/sessions, no gopkg.in/yaml.v3 in the dependency graph. +- PHP branches ported: identity login, no-email pending (fb_no_email), verified email auto-link, unverified match pending, direct creation on register with consent, throttle and beta gate; action=link through a 60-second link ticket or the request user; no JWT in a URL. +- Admin settings screen with encrypted, write-only client secrets overriding config per field without a restart. +- Release train in D-21 order: summercms v0.3.1 tagged, pushed and posted to the changelog; sm-user-plugin pushed and tagged v0.1.4; the plugin pinned to both, repository created, pushed and tagged v0.1.0. + +## Task Commits + +sm-sociallogin-plugin (`/media/nvme/dev/golem15/summercms.io/summercms/sm-sociallogin-plugin`): + +1. **Task 1: tracer through a fake provider** - `c15e8b9` (feat) +2. **Task 2: pending registration, account creation and email matching** - `f588a38` (feat) +3. **Task 3: identity linking with a one-time link ticket and start-request errors** - `62c8586` (feat) +4. **Task 4: Google, Facebook, GitHub and Microsoft through goth with a settings override** - `65244a9` (feat) +5. **Task 5: go-ahead** - no commit (reply below) +6. **Task 6: require summercms v0.3.1 and sm-user-plugin v0.1.4** - `adfcfbf` (chore) + +sm-summercmsio-app (`/media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app`): + +- **Task 6 (fix-ua): changelog-post sends its own User-Agent** - `8581576` (fix), **not pushed** (push not authorised) + +**Plan metadata:** this SUMMARY commit, `docs(19.5-02): ...` (summercms.go, local, not pushed) + +## Task 5 reply + +> release v0.3.1 (Recommended) + +Recorded verbatim. Chosen framework tag: **v0.3.1**. Interpreted with the orchestrator as: framework v0.3.1, sm-user-plugin v0.1.4, sm-sociallogin-plugin v0.1.0, private repository via `tea repos create --owner golem15 --name sm-sociallogin-plugin`. + +Per the Task 5 resume signal, the reply also authorises **plan 03** to push sm-bm-plugin before its pointer bump and to push sm-demo-app, sm-bm-app and fonoteka.go after its commits, each only when the unpushed commits belong to this phase. Nothing is ever force-pushed or deployed. + +## Changelog blocker reply + +> fix-ua (Recommended) + +Recorded verbatim. The first publish failed with Cloudflare 403, error 1010 `browser_signature_banned` (ray `a4db4c6b7efc60a4`, 2026-10-20T22:00:59Z), because `scripts/changelog-post.py` sent the default `Python-urllib` User-Agent. The orchestrator confirmed `GET https://summercms.io/_journal/api/v1/posts` answers 403 with `Python-urllib/3.13` and 200 with `summercms-changelog-post/1`. Fix: one `req.add_header("User-Agent", "summercms-changelog-post/1")` line in `call()`, committed as `8581576`; the retried publish succeeded. + +## Remote operations (Task 6, D-21 order) + +1. **summercms.go** (previous continuation): `git push origin master` `dd047eb..5cf5c4f`; annotated tag `v0.3.1` (tag object `e13723c`, on `5cf5c4f`) pushed. The push also carried another session's planning commit `5cf5c4f` "docs(roadmap): add phase 32 console commands overhaul" (that session has since renumbered it to 19.8 in the unpushed local commit `c996790`, not ours). + - Changelog post: id 34, https://summercms.io/changelog/summercms-0-3-1 (published_at 2026-10-20T22:00:28Z; GET answers 200). Excerpt: "One-time cache values with Cache.Pull and branded error pages for browser visitors." +2. **sm-user-plugin** (`fonoteka.go/plugins/golem15/user`): fetched; 0 behind, 3 ahead (`78a867b`, `1741618`, `c43ba87`, all plan 19.5-01's); v0.1.4 absent locally and on origin; vet and short tests green. `git push origin master` `1fc0ce7..c43ba87`; annotated tag `v0.1.4` (tag object `2e32ed0`) pushed. `origin/master..master` empty. +3. **sm-sociallogin-plugin**: go.mod requires `summercms v0.3.1` and `sm-user-plugin v0.1.4` (the go.mod replace of summercms to `../summercms.go` kept; go.work keeps both local replaces); `GOWORK=off go mod tidy` downloaded sm-user-plugin v0.1.4 from origin; vet, short and full suites green, `GOWORK=off go vet/build` green; README Dependencies names both versions; committed `adfcfbf`. `tea repos create --owner golem15 --name sm-sociallogin-plugin --private` created https://git.golem15.com/golem15/sm-sociallogin-plugin (anonymous GET answers 404, so private). `git remote add origin git@git.golem15.com:golem15/sm-sociallogin-plugin.git`; `git push -u origin master` (new branch, `c15e8b9..adfcfbf`, 5 commits); annotated tag `v0.1.0` (tag object `76c65fb`) pushed. `ls-remote --exit-code` for `refs/heads/master` and `v0.1.0` both exit 0. + +Nothing deployed. Nothing force-pushed. + +## Files Created/Modified + +sm-sociallogin-plugin (new repository): +- `plugin.go`, `routes.go`, `config.go`, `service.go` - plugin, routes, config loader, boot wiring +- `admin_settings.go`, `admin_permissions.go`, `models/settings.go`, `models/settings/fields.yaml`, `updates/*` - settings singleton and migration +- `classes/provider.go`, `models/registry.go` - provider interface and Registry +- `classes/credentials.go`, `classes/config.go` - credentials (config, env, settings overlay), config types +- `classes/statecookie.go` - sealed state cookie +- `classes/callback.go`, `classes/flow.go`, `classes/registration.go`, `classes/codes.go`, `classes/messages.go` - callback branches, account creation, one-time codes, messages +- `classes/providers_goth.go` - the only goth importer +- `controllers/*` - redirect, callback, oauth-complete, oauth-register-complete, oauth-link-ticket +- `config/config.yaml`, `lang/en/lang.yaml`, `lang/pl/lang.yaml` +- `*_test.go` - harness, fake provider, five smoke tests +- `README.md`, `go.mod`, `go.sum`, `go.work`, `.gitignore` + +sm-summercmsio-app: +- `scripts/changelog-post.py` - User-Agent header + +## Verification + +- sm-sociallogin-plugin after the require bump: `go vet ./...`, `go test -count=1 -short ./...`, `go test -count=1 ./...` green; `-v` shows PASS for TestTracerSmoke, TestFlowSmoke, TestLinkSmoke, TestSettingsOverrideSmoke, TestGothProvidersSmoke (none skipped). +- At the Task 5 checkpoint: vet, short and full suites green (TestTracerSmoke, TestFlowSmoke 5, TestLinkSmoke 8, TestSettingsOverrideSmoke, TestGothProvidersSmoke 8); goth only in `classes/providers_goth.go`, no gothic, UseProviders, gorilla/sessions or yaml.v3; framework go.mod has no goth; summercms.go vet and short suite green; fonoteka.go builds. +- sm-user-plugin before push: vet and short suite green. +- Acceptance: `git ls-remote --exit-code --tags origin v0.3.1` (summercms.go), `... v0.1.4` (sm-user-plugin), `... v0.1.0` (sm-sociallogin-plugin) all exit 0; `grep -c 'git.golem15.com/golem15/sm-user-plugin v0.1.4' go.mod` = 1 (the plan's v0.1.2 string is replaced by v0.1.4, deviation 13); `origin/master..master` empty in sm-user-plugin. + +## Deviations from Plan + +### Auto-fixed Issues and adjustments + +1. **[Rule 1] SettingsCode `sociallogin`** instead of `golem15.sociallogin.settings`: cabana rejects dotted settings codes. (`65244a9`) +2. **[Rule 2] `models.Settings` has `Rules()`** for admin validation. (`65244a9`) +3. **[Rule 3] Config types live in `classes`** (root `sociallogin.Config` is an alias plus `LoadConfig` wrapper) to avoid an import cycle. (`c15e8b9`) +4. **[Rule 3] `classes.Message(ctx, app, key, params)`** plus `RegisterMessage` for localized messages. (`c15e8b9`, `f588a38`) +5. **[Rule 2] `classes.ClientIP`** (surf.ClientIP with trusted proxies) for last IP, creation IP and the register throttle. (`f588a38`) +6. **[Rule 1] Pending code pulled inside the final DB transaction after every 422 check**: a lost race answers 410 (the winner 200); a 422 never consumes. (`f588a38`) +7. **[Rule 1] `providers.microsoft.tenant` defaults to ""**, which falls back to `common` (`DefaultTenant`). (`65244a9`) +8. **[Rule 2] Beta key carried from any start request** into the sealed cookie and the pending payload (Pitfall 14). (`f588a38`) +9. **[Rule 2] Inactive or banned link user answers `must_be_logged_in`.** (`62c8586`) +10. **[Rule 2] Real messages for inactive and banned users (A7)**, Polish copy for every message, PHP Polish byte-for-byte where PHP has it. (`f588a38`, `62c8586`) +11. **[Rule 3] `bootSocial` returns `*socialEnv`** to share the wiring between Boot and tests. (`c15e8b9`) +12. **[Rule 3] Extra exported identifiers** beyond the plan's list: `ErrInvalidName`, `Registry.Has`, `ErrInvalidState`, `StateCookieSecure`, `WithQuery`, `CallbackRoute`, `NewHostSet`, `OutcomePending`, `ReasonNoEmail`, `ReasonUnverifiedMatch`, `PendingPayload.Profile()`, `CompletePending`, `ErrPendingConsumed`, `CheckBetaKey`, `ValidationError`, `IsValidation`, `PasswordBootstrapSource`, `ClientIP`, `DefaultTenant`, `models.Providers`, `models.ConfigSecretKey`, `Settings.Credentials`. README documents them. +13. **[Rule 3] sm-user-plugin tag v0.1.4 instead of the plan's v0.1.2**: v0.1.2 and v0.1.3 already existed. The plugin's requires were initially `summercms v0.3.0` / `sm-user-plugin v0.1.3`, bumped to `v0.3.1` / `v0.1.4` in `adfcfbf`. +14. **[Rule 3, human decision] Changelog blocked by the Cloudflare User-Agent ban**, fixed in sm-summercmsio-app (`8581576`, reply "fix-ua (Recommended)"); the commit is **unpushed** because pushing sm-summercmsio-app was not authorised. +15. **[Context] Extra commit pushed with summercms.go master**: another session's planning commit `5cf5c4f` "docs(roadmap): add phase 32 console commands overhaul" went out with the v0.3.1 push. + +--- + +**Total deviations:** 15 (no architectural changes) +**Impact on plan:** Contracts and routes as planned; names extended, not changed; version numbers adjusted to existing tags. + +## Issues Encountered + +- The first changelog publish failed (Cloudflare 403, error 1010); resolved by deviation 14 and a successful retry. + +## Known Stubs + +None. + +## Threat Flags + +None beyond the plan's threat model (all new surface is the planned /oauth and /_user/api/v1/oauth-* routes and the settings screen). + +## User Setup Required + +To enable a provider in an application: set `golem15.sociallogin.providers.

.client_id` and `client_secret` (or the legacy env names, or the admin settings screen) and register the redirect URI `app.url + /oauth/

/callback` with the provider. + +## Next Phase Readiness + +- Plan 03 can mount `git@git.golem15.com:golem15/sm-sociallogin-plugin.git` (v0.1.0) as a submodule and bump sm-user-plugin pointers to `c43ba87` (v0.1.4, pushed); its pushes are authorised as recorded above. +- Open, outside this plan: sm-summercmsio-app `8581576` unpushed; summercms.go has this SUMMARY commit and another session's `c996790` unpushed. + +## Self-Check: PASSED + +- Files: all sm-sociallogin-plugin key files tracked (43); `sm-summercmsio-app/scripts/changelog-post.py` carries the header. +- Commits: `c15e8b9`, `f588a38`, `62c8586`, `65244a9`, `adfcfbf` (sm-sociallogin-plugin, all on origin/master), `8581576` (sm-summercmsio-app), `c43ba87` (sm-user-plugin, on origin/master) exist. +- Tags on origin: summercms `v0.3.1`, sm-user-plugin `v0.1.4`, sm-sociallogin-plugin `v0.1.0`. + +--- +*Phase: 19.5-social-login-in-sm-user-plugin* +*Completed: 2026-10-21*