diff --git a/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-VERIFICATION.md b/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-VERIFICATION.md new file mode 100644 index 00000000..a8b01e7f --- /dev/null +++ b/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-VERIFICATION.md @@ -0,0 +1,238 @@ +--- +phase: 19.5-social-login-in-sm-user-plugin +verified: 2026-10-21T08:30:00Z +status: human_needed +score: 15/16 must-haves verified +covered_files: + - .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-01-PLAN.md + - .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-01-SUMMARY.md + - .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-02-PLAN.md + - .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-02-SUMMARY.md + - .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-03-PLAN.md + - .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-03-SUMMARY.md + - .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-04-PLAN.md + - .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-04-SUMMARY.md + - docs/services/caching.md + - modules/cooler/README.md + - modules/cooler/cooler.go + - modules/cooler/docker_test.go + - modules/cooler/example_test.go + - modules/cooler/memory.go + - modules/cooler/pull_smoke_test.go + - modules/cooler/pull_test.go + - modules/cooler/rediscache.go + - scripts/check-phase19.5.sh +covered_digest: "v2:sha256:2ca5d998566ef58a4d12cb1ef9527308eca83072bcc092cc78e81badca4a05bb" +# Files outside this repository are pinned by commit instead of digest: +# sm-sociallogin-plugin d7ee28d (tag v0.1.1), sm-user-plugin c43ba87 (v0.1.4; master c7635c5, test-only on top), +# sm-demo-app 61a00e9, sm-bm-app 99af8c2, fonoteka.go f4d5728. +behavior_unverified: 1 +overrides_applied: 0 +behavior_unverified_items: + - truth: "The bm frontend's /auth/oauth/callback view removes ?code= from the address bar and history before it redeems the code exactly once, routes ?pending_registration= to the consent view and shows ?error= messages (plan 03 must-have, T-19.5-26)." + test: "In sm-bm-app with Microsoft credentials (or any configured provider), sign in from /logowanie; on return, watch the URL and browser history; press Back after landing; also open a callback URL with ?error=Test and one with ?pending_registration=x&provider=microsoft." + expected: "The address bar never keeps ?code= after the view mounts; Back does not land on a callback URL with a code; exactly one GET /_user/api/v1/oauth-complete per sign-in; ?error= shows the message; ?pending_registration= lands on /auth/oauth/rejestracja." + why_human: "Only src/lib/oauth.ts (URL building and parsing) is unit-tested; the view's ordering invariant (router.replace before completeOAuth, the redeemed-set guard) is browser behaviour no test exercises." +human_verification: + - test: "Decide on the residual pre-account-takeover path left open by the T-19.5-38 fix. ownerVerifiedEmail treats has_self_set_password=true as proof the owner controls the address, but POST /register with golem15.user activation.activate_mode=auto (the default) creates an activated account with has_self_set_password=true and no email check. An attacker who registers victim@example.com with a password before the victim's first Google/GitHub sign-in gets the victim's verified identity auto-linked onto the attacker's account, and keeps password access." + expected: "Either accept it explicitly (it is exactly PHP's D-10 behaviour, and the variant T-19.5-38 closed was the social-created one), or schedule a follow-up (for example: auto-link only when the account was activated by mail or set its password through the reset code, or require activate_mode=user for apps mounting sociallogin). Also correct 19.5-SECURITY-REVIEW.md / README wording that a self-set password means the owner 'proved the address'." + why_human: "Product/security decision on a deliberate PHP-parity behaviour; no test covers the /register-then-verified-sign-in path (TestPreAccountTakeover only covers accounts created by an unverified social sign-in)." + - test: "Walk the bm callback view in a browser (see behavior_unverified_items)." + expected: "As in behavior_unverified_items." + why_human: "Browser history and redirect ordering; no view-level test." + - test: "Review the judgment-tier prohibition verdicts in the 'Prohibitions' section of this report (non-authoritative LLM-judge verdicts)." + expected: "Confirm or dispute each verdict; all are recorded as no violation found." + why_human: "Judgment-tier prohibitions never pass silently (ADR-550 D4)." +--- + +# Phase 19.5: Social login in sm-user-plugin Verification Report + +**Phase Goal:** `sm-user-plugin` signs users in with Google and Facebook through `/oauth/{provider}` and its callback, with a provider registry open to more providers, the SPA one-time-code handoff and the pending-registration branch, matching the PHP user plugin's contract. Per CONTEXT D-01 the code lives in the new `sm-sociallogin-plugin`, with hooks in sm-user-plugin. +**Verified:** 2026-10-21 +**Status:** human_needed +**Re-verification:** No, initial verification + +**MVP-mode note:** ROADMAP marks this phase `**Mode:** mvp`, but the goal is not a user story (`user-story.validate` returns `valid: false`). The verifier contract says to refuse MVP-mode verification and ask for `/msd mvp-phase 19.5`. Every phase in this ROADMAP carries `Mode: mvp`, and all four plans record the decision not to invent a story. So this report runs the standard goal-backward verification against the ROADMAP goal and SC-1..SC-4. Reformatting the goal is a process choice for the user; this verification does not depend on it. + +## Goal Achievement + +### Observable Truths + +| # | Truth | Status | Evidence | +|---|-------|--------|----------| +| 1 | SC-1: `/oauth/google` and `/oauth/facebook` (and github, microsoft) with `/callback` keep the PHP paths (PHP `routes.php:239-250`, `prefix oauth`, `throttle:20,1`) | VERIFIED | S `routes.go`: `GroupRaw("/oauth", surf.Use("throttle:20,1"))` with `/{provider}` and `/{provider}/callback`; `RegisterBuiltins` registers google, facebook, github, microsoft. Tests: `TestRoutes`, `TestRouteMiddleware`, `TestTracerSmoke`; demo `TestSocialLoginRedirect` (real app: Location host accounts.google.com); bm `TestSocialLoginMounted` (login.microsoftonline.com/). All passed in my gate run. | +| 2 | SC-2a: SPA mode redirects to `{frontend_callback}?code=<64 chars>`, and `GET /_user/api/v1/oauth-complete` exchanges it once (200 `{token,user,action,return_to}`, 422 empty, 410 missing or used) | VERIFIED | S `controllers/callback.go` `finishCallback` → `PutCompletion`; `controllers/complete.go` uses `PullCompletion` (cooler `Pull`); bodies match PHP `ApiController.php:1097-1116` byte for byte (messages, defaults `login` and `/`). Tests: `TestTracerSmoke` (second use 410), `TestOAuthComplete`, `TestCodes` (32 concurrent pulls, 1 winner), `TestTokenCookieMode`. | +| 3 | SC-2b: a new social user goes to `?pending_registration=[&reason=]&provider=[&email=]` and finishes with `POST oauth-register-complete` (PHP fields, 422 consent body with PHP Polish text, 410, `email_exists_link_account`); a 422 does not consume the code | VERIFIED | S `classes/flow.go` `pendingOutcome`, `controllers/callback.go` (PHP parameter order), `controllers/register_complete.go` (Get, all 422 checks, then `PullPending` inside the final transaction via `CompletePending`). Tests: `TestFlowSmoke/register_complete_keeps_code_on_422`, `TestRegisterComplete` (incl. 16 concurrent submits → one 200), `TestPHPSocialAuthGate`, `TestPHPPendingRegistrationTakeover`. | +| 4 | SC-3a: identities are linked on login (identity match, verified email match, `action=link`) | VERIFIED | S `classes/flow.go` `loginWithIdentity`/`upsertIdentity` under `SELECT ... FOR UPDATE`, `linkIdentity`; tokens encrypted with `token_expires_at`, avatar only in `profile_data`. Tests: `TestMatching`, `TestLink`, `TestLinkTicket`, `TestIdentityTokens`, `TestFlowSmoke/verified_match_links`. | +| 5 | SC-3b: identities are listed and unlinked through the existing (host-mounted) identity routes, with the PHP unlink rule, race-safe | VERIFIED | U `controllers/oauth_identities.go`: allow-list adds microsoft and reads the published source's `KnownOAuthProviders`; `FOR UPDATE` then the guarded DELETE (PHP User.php:913). Tests: `TestOAuthIdentitiesDestroyRule`, `TestOAuthIdentitiesDestroyRace` (both orders), `TestOAuthIdentitiesDestroyConcurrentSmoke`; `TestPHPProfileOAuthLink` drives `OAuthIdentitiesIndex` and `OAuthIdentitiesDestroy` over identities the plugin linked. Note: per D-14 the host mounts these routes; only fonoteka.go mounts them today, so sm-demo-app and sm-bm-app users cannot list or unlink an identity (bm's profile only links). This is within D-14, not a gap. | +| 6 | SC-3c: the consuming applications still build and pass their tests | VERIFIED | Gate `--demo` (3 tests), `--bm` (Go 1+2, vitest 211, typecheck), `--fonoteka` (vet + `go test -short ./...`, sociallogin not mounted) all PASS in my run. fonoteka's Docker-only `TestPhase09SecurityRoutes` failure is pre-existing and unrelated (five cabana admin routes; deferred-items.md). | +| 7 | SC-4: the new code has unit tests, delivered in the last plan (19.5-04) | VERIFIED | 19.5-04 adds `modules/cooler/pull_test.go`, 3 user-plugin test files and 15 sociallogin test files. Gate `--all` with `PHASE195_REQUIRE_DOCKER=1` ran 4+2+9+28+10+1 named tests, all passing, including the TestPHP* ports and `TestPHPMethodMap` (38 PHP methods mapped). | +| 8 | Provider registry open to more providers (D-03): plugin-owned `Provider`/`Registry`, no goth type exported, fakes registered through the same registry | VERIFIED | S `classes/provider.go` (no goth import; `Register` refuses reserved and duplicate names); goth imported only in `classes/providers_goth.go` (grep). `Plugin.Register` publishes the registry so another plugin can add providers. Tests: `TestRegistry`, and every flow test registers fake providers. | +| 9 | D-01/D-02: separate plugin requiring `golem15.user`; goth v1.82.0 without gothic; no gorilla/sessions; framework go.mod untouched | VERIFIED | `Requires() = ["golem15.user"]`; S go.mod `github.com/markbates/goth v1.82.0`; no `gothic`, `UseProviders` or `gopkg.in/yaml` in S Go sources; gate `--sociallogin` checks `go list -deps`; summercms.go go.mod has no commit since 26ebb6c. | +| 10 | D-07: state, action, callback, return_to, consent, beta key, link user and goth session in one AES-256-GCM cookie (HKDF purpose key, AAD the cookie name), HttpOnly, SameSite=Lax, Path=/oauth, 600 s, cleared on every callback outcome, state compared in constant time | VERIFIED | S `classes/statecookie.go` (lines 26-32, 122-127, 188-196), `controllers/callback.go` (clear first; `subtle.ConstantTimeCompare`; empty state refused). Test: `TestStateCookie` (refused cases, age window, seal limits, key domain separation, callback outcomes). | +| 11 | D-08: `cooler.Cache.Pull` is atomic on memory and Redis; the plugin pins a framework release that contains it | VERIFIED | `memory.go:90` (otter `Compute` + `InvalidateOp`), `rediscache.go:57` (GETDEL); `git tag --contains 5e0a04e` → v0.3.1; S go.mod requires summercms v0.3.1. Tests: `TestPullConcurrent` (64 pullers), `TestPullExpired`, `TestPullPrefixIsolation`, `TestPullDocker` (redis:7-alpine). | +| 12 | D-10/D-11: PHP matching table; Google verified only by a boolean flag, GitHub by primary-verified `/user/emails`, Facebook and Microsoft never (nOAuth); plus the user-approved T-19.5-38 divergence (no auto-link onto a password-less account) | VERIFIED | S `classes/flow.go` `HandleCallback`, `CompletePending`, `ownerVerifiedEmail`; `classes/providers_goth.go` (rawTrue strict bool; Facebook and Microsoft `EmailVerified = false`; UPN fallback). Tests: `TestMatching`, `TestEmailVerified`, `TestNOAuth`, `TestPreAccountTakeover`, `TestSoftDeleted`, `TestInactiveAndBanned`. See the residual noted under Human Verification 1. | +| 13 | D-15/D-22: `oauth-providers` stays in sm-user-plugin, is unchanged without a source, and lists enabled providers when sociallogin publishes one; no sm-user-plugin migration | VERIFIED | U `api_controller.go` `OAuthProviders` looks up `classes.OAuthProviderSource` per request; S `plugin.go` `app.Publish[userclasses.OAuthProviderSource]`. `git diff 4c5d3b8 -- updates` is empty. Tests: `TestOAuthProvidersSourceSmoke` (exact bytes `{"providers":[],"success":true}`), `TestOAuthProviders`, `TestProvidersList`. | +| 14 | D-16/D-18: credentials from config, legacy env and the admin settings overlay read per request; secrets encrypted and write-only; enabled iff id and secret; redirect URI override or app.url + `/oauth/

/callback` | VERIFIED | S `classes/credentials.go`, `plugin.go` `copyLegacyEnv`, `models/settings.go`. Tests: `TestSettings` (incl. `admin_json_hides_secrets`, `no_secret_in_logs`), `TestSettingsOverrideSmoke`. | +| 15 | D-21/D-23: sm-demo-app and sm-bm-app mount the plugin at v0.1.1 (every post-release fix included), pushed in D-21 order; bm links Microsoft through the 60 s one-time link ticket with no JWT in a URL | VERIFIED | Gitlinks: sm-demo-app and sm-bm-app `plugins/golem15/sociallogin` = d7ee28d (= v0.1.1), `plugins/golem15/user` = c43ba87 (= v0.1.4); fonoteka.go user = c43ba87. `origin/master..master` is empty in sm-sociallogin-plugin, sm-user-plugin, sm-demo-app and sm-bm-app. The three production fixes (21f3fe5, c99f5f9, d7ee28d) are ancestors of both app pins. bm `ProfileView.vue` → `oauthLinkTicket` → `oauthStartUrl(... linkTicket)`. Tests: `TestLinkTicket` (incl. `no_jwt_in_locations`), `oauth.test.ts`. | +| 16 | bm callback view: removes the code from the address bar before redeeming it once; routes pending and error | PRESENT_BEHAVIOR_UNVERIFIED | `frontend/src/views/OAuthCallbackView.vue` calls `router.replace` before `auth.completeOAuth`, with a module-level `redeemed` set; the pending branch replaces to `oauth-register`. The code is present and wired, but no test mounts the view. See Human Verification 2. | + +**Score:** 15/16 truths verified (1 present, behavior-unverified) + +### Required Artifacts + +| Artifact | Expected | Status | Details | +|----------|----------|--------|---------| +| `.planning/notes/2026-10-20-sociallogin-goth.md` | D-02 decision note | VERIFIED | Names markbates/goth v1.82.0 (gate `--docs`); its commit 26ebb6c is an ancestor of Pull (5e0a04e) | +| `modules/cooler/{cooler,memory,rediscache}.go` | `Cache.Pull` | VERIFIED | Interface plus both drivers; README and docs in the same commit 5e0a04e | +| U `classes/oauth_providers.go` | `OAuthProviderSource` | VERIFIED | Implemented by S `service.go`, looked up by U `OAuthProviders` and the unlink allow-list | +| U `controllers/oauth_identities.go` | row-locked PHP unlink | VERIFIED | `FOR UPDATE` then the guarded DELETE | +| U `controllers/registration.go` | `PasswordLess`, `FinishLogin`, exports | VERIFIED | Called by S `classes/registration.go` and `flow.go` | +| S `classes/provider.go` | Provider, Profile, Registry | VERIFIED | 207 lines, no goth | +| S `classes/statecookie.go` | sealed state | VERIFIED | Contains `golem15.sociallogin.oauth-state.v1` | +| S `classes/flow.go` | D-10 matching | VERIFIED | Contains `unverified_match` and `fb_no_email`; `lower(email) = lower(?)` | +| S `classes/providers_goth.go` | 4 providers, the only goth importer | VERIFIED | azureadv2, `verified_email`, `user/emails`, `userPrincipalName` | +| S `controllers/{redirect,callback,complete,register_complete,link_ticket}.go` | handlers | VERIFIED | Mounted in `routes.go` | +| S `testdata/PHP_METHOD_MAP.md` + `php_method_map_test.go` | 38-method map plus checker | VERIFIED | 17 ported, 18 adapted, 3 not-applicable (relaxed change-password bootstrap, todo filed) | +| `scripts/check-phase19.5.sh` | fail-closed gate | VERIFIED | `--self-test` PASS; `--all` exit 0 | +| `sm-demo-app/sociallogin_test.go`, `sm-bm-app/sociallogin_test.go` | app-level mount tests | VERIFIED | PASS in the gate | +| `sm-bm-app/frontend/src/lib/oauth.ts`, `views/OAuthCallbackView.vue`, `OAuthRegisterView.vue` | bm frontend wiring | VERIFIED (lib) / behavior-unverified (views) | See truth 16 | + +### Key Link Verification + +| From | To | Via | Status | +|------|----|-----|--------| +| U `api_controller.go` `OAuthProviders` | U `classes.OAuthProviderSource` | `app.Lookup[classes.OAuthProviderSource]()` per request | WIRED | +| S `plugin.go` Boot | U `OAuthProviderSource` | `app.Publish[userclasses.OAuthProviderSource](&providerSource{...})` | WIRED | +| S `controllers/complete.go` | `cooler.Cache.Pull` | `PullCompletion` → `cooler.FromApp(app).Pull` | WIRED | +| S `controllers/register_complete.go` | `PullPending` | `consume` closure run inside the final transaction | WIRED | +| S `classes/registration.go` | U `RegisterUser{PasswordLess}`, `FireRegisterEvent`, `FireCreatedEvent`, `FinishLogin`, `IssueToken`, `APIArray` | direct calls | WIRED | +| S `routes.go` | surf throttles | `throttle:20,1` on /oauth, `throttle:user-api` on /_user/api/v1 | WIRED (`TestRouteMiddleware`) | +| U unlink | users row lock | `SELECT id FROM users WHERE id = ? FOR UPDATE` | WIRED (mutation-checked) | +| `modules/cooler/rediscache.go` | Redis GETDEL | `r.client.GetDel` | WIRED | +| bm `OAuthCallbackView.vue` | `/_user/api/v1/oauth-complete` | `auth.completeOAuth` → `userApi.oauthComplete` | WIRED | +| bm `ProfileView.vue` | `/_user/api/v1/oauth-link-ticket` | `userApi.oauthLinkTicket` → top-level navigation with `link_ticket` | WIRED | +| sm-demo-app `summer.yaml` | `plugins.gen.go` | `golem15.sociallogin` / `git.golem15.com/golem15/sm-sociallogin-plugin` | WIRED (`TestBootAllPlugins`) | + +### Data-Flow Trace (Level 4) + +| Artifact | Data | Source | Real data | Status | +|----------|------|--------|-----------|--------| +| `oauth-providers` response | providers | `providerSource.EnabledOAuthProviders` → `ResolveCredentials` (config + settings row per request) | yes | FLOWING | +| `oauth-complete` response | token, user | callback → `IssueToken` and `APIArray` from the matched or created user → sealed cooler value → `Pull` | yes | FLOWING | +| identity row | provider tokens, profile | goth `FetchUser` (or fake) → `upsertIdentity` (lagoon.Encrypted) | yes | FLOWING | +| bm login button | providers list | `userApi.oauthProviders()` | yes | FLOWING | + +### Behavioral Spot-Checks + +| Behavior | Command | Result | Status | +|----------|---------|--------|--------| +| Whole phase gate, Docker required | `PHASE195_REQUIRE_DOCKER=1 bash scripts/check-phase19.5.sh --all` | exit 0; self-test, go (4), userplugin (2), sociallogin (9), demo (3), bm (1+2, vitest 211), fonoteka, docs, docker (1 + 10 + 28) all PASS | PASS | +| Post-release fixes reach the app pins | `git merge-base --is-ancestor d7ee28d ` for sm-demo-app and sm-bm-app | both contain d7ee28d | PASS | +| Remotes in sync | `git log origin/master..master` in S, U, D, B | empty | PASS | + +### Probe Execution + +| Probe | Command | Result | Status | +|-------|---------|--------|--------| +| (none declared; no `scripts/*/tests/probe-*.sh` in the phase) | n/a | the phase gate above plays this role | SKIPPED | + +### Requirements Coverage + +The phase has no REQUIREMENTS.md IDs. Every ID in the plan frontmatter is accounted for: the union of the four plans covers SC-1..SC-4 and D-01..D-25. + +| Requirement | Source Plan(s) | Status | Evidence | +|-------------|----------------|--------|----------| +| SC-1 | 02, 03 | SATISFIED | Truth 1 | +| SC-2 | 02 | SATISFIED | Truths 2, 3 | +| SC-3 | 01, 02, 03 | SATISFIED | Truths 4-6 | +| SC-4 | 04 | SATISFIED | Truth 7 | +| D-01 | 02, 03 | SATISFIED | Truth 9 | +| D-02 | 01, 02 | SATISFIED | Truth 9; human package check "approved" (19.5-01-SUMMARY) | +| D-03 | 02, 04 | SATISFIED | Truth 8 | +| D-04 | 02, 03, 04 | SATISFIED | 4 built-ins; unknown or reserved → 404; disabled → `?error=` (`TestRoutes`) | +| D-05 | 02, 03 | SATISFIED | `mode` ignored; invalid or missing callback → `default_callback` (`TestLinkSmoke/bad_callback_falls_back`) | +| D-06 | 02, 03, 04 | SATISFIED | Query contract; `NormalizeReturnTo` (`TestNormalizeReturnTo`) | +| D-07 | 02, 04 | SATISFIED | Truth 10 | +| D-08 | 01, 02, 04 | SATISFIED | Truth 11; validate-before-pull (truth 3) | +| D-09 | 02, 04 | SATISFIED | Truths 2, 3; opt-in token cookie (`TestTokenCookieMode`) | +| D-10 | 02, 04 | SATISFIED (with a user-approved divergence) | Truth 12 | +| D-11 | 02, 03, 04 | SATISFIED | Truth 12 | +| D-12 | 02, 04 | SATISFIED | `CheckBetaKey` / `betaKeyMatches` constant time on both paths (`TestBetaGate`) | +| D-13 | 01, 04 | SATISFIED | Truth 5 | +| D-14 | 02, 03, 04 | SATISFIED | Link via ticket or `RequestUser`; `account_already_linked` (`TestLink`) | +| D-15 | 01, 04 | SATISFIED | Truth 13 | +| D-16 | 02, 04 | SATISFIED | Truth 14 | +| D-17 | 02, 03, 04 | SATISFIED | Exact-host allow-list, fixed path, rebuilt target (`TestNormalizeFrontendCallback`, 44 cases) | +| D-18 | 02, 03 | SATISFIED | Truth 14 | +| D-19 | 02, 04 | SATISFIED | `TestIdentityTokens` | +| D-20 | 02, 04 | SATISFIED | `TestRouteMiddleware` (21st request 429) | +| D-21 | 01-04 | SATISFIED | Truth 15 | +| D-22 | 01, 02 | SATISFIED | Truth 13; consent details in `RegisterEvent.Payload` | +| D-23 | 02, 03, 04 | SATISFIED | Truth 15 | +| D-24 | 01, 03 | SATISFIED | fonoteka at c43ba87, gate `--fonoteka` PASS | +| D-25 | 01, 04 | SATISFIED | 4 plans, unit tests last | + +No orphaned requirements. + +### Prohibitions (judgment tier; non-authoritative LLM-judge verdicts, unverified-prohibition: human review recommended) + +| Plan | Prohibition | Judge verdict | Evidence | +|------|-------------|---------------|----------| +| 01, 02 | No JWT, provider token, secret or code in a log line or error | no violation found | 19.5-SECURITY-REVIEW "Log calls" table matches the S controllers; `TestSettings/no_secret_in_logs` | +| 01, 02 | sm-user-plugin gets no migration, column, route, payload or config-key change | no violation found | `git diff 4c5d3b8 -- updates` is empty; the gate's additive check passes | +| 01 | Framework go.mod gains no module; no goth before the human check | no violation found | No summercms.go go.mod commit since 26ebb6c | +| 01 | Framework code and docs name no consuming application | no violation found | Gate `--docs`: 429 files checked | +| 01-04 | Nothing pushed or tagged before the go-ahead; never force-pushed | no violation found (process) | Replies recorded in the 02 and 04 SUMMARYs; the 02 push also carried another session's planning commit 5cf5c4f (recorded) | +| 02 | No JWT in a URL; no `?token=` read | no violation found | Redirect reads no token query; `TestLinkTicket/no_jwt_in_locations`, `TestLink` (token in query ignored) | +| 02 | gothic and UseProviders never used; no gorilla/sessions in deps | no violation found | grep plus gate `--sociallogin` deps check | +| 02 | No web-mode fallback; no admin identity screen | no violation found | No /select or session-login code in S | +| 02 | goccy only; no gopkg.in/yaml.v3 import | no violation found | grep plus `go list -deps` | +| 03 | No real credentials committed | no violation found | grep for non-empty `CLIENT_SECRET=` or `client_secret:` in D and B config and deploy: none | +| 03 | bm never puts the JWT in a URL; the callback replaces the route | no violation found in code | `oauth.ts`, `OAuthCallbackView.vue` (behaviour: truth 16) | +| 03 | fonoteka does not mount sociallogin | no violation found | `summer.yaml` has no sociallogin (gate `--fonoteka`) | +| 03 | No new npm dependency in bm | no violation found | `frontend/package.json` and the lockfile are unchanged across the phase's bm commits | +| 04 | Only test-exposed production fixes, each in its own commit | no violation found | 21f3fe5, c99f5f9, d7ee28d, each with a test | +| 04 | Tests never call real providers or hold real secrets | no violation found | The fake transport refuses unknown hosts (`TestGothProviders/fake_transport_refuses_unknown_hosts`) | +| 04 | A skipped Docker test never counts as passed | no violation found | My gate run used `PHASE195_REQUIRE_DOCKER=1`; the detector fails on skips | + +### Anti-Patterns Found + +| File | Line | Pattern | Severity | Impact | +|------|------|---------|----------|--------| +| (none) | | No TBD, FIXME, XXX, TODO or placeholder markers in S non-test sources, the changed U files, cooler, the gate script, or the bm OAuth views and lib | | | +| S `classes/flow.go` `ownerVerifiedEmail` + README / SECURITY-REVIEW wording | | The doc says a self-set password means the owner "proved the address". That holds after a mailed reset, not after `/register` in `activate_mode=auto` | Warning | Overstates the T-19.5-38 closure; the residual path matches PHP (see Human Verification 1) | + +### Human Verification Required + +#### 1. Residual pre-account takeover through password registration (decision) + +**Test:** Default config (`golem15.user.activation.activate_mode: auto`). An attacker POSTs `/_user/api/v1/register` with victim@example.com and a password. The account is activated with `has_self_set_password=true` and no email proof. The victim later signs in with Google (verified email). +**Expected (current code):** `HandleCallback` matches the email, `ownerVerifiedEmail` returns true, and the victim's Google identity is linked onto the attacker's account (signed in as the attacker's account; the attacker keeps password access). PHP does the same (D-10 verbatim). Decide whether to accept this and document it accurately, or follow up, for example by requiring mail activation or a reset-set password before auto-linking. +**Why human:** A security and product decision on deliberate PHP parity. No test covers this path. + +#### 2. bm OAuth callback view in a browser + +**Test:** Sign in through Microsoft 365 (or any configured provider) on bm. Check the URL and history after return. Open callback URLs carrying `?error=` and `?pending_registration=`. +**Expected:** The code leaves the address bar and history before the single `oauth-complete` call; the error is shown; pending goes to `/auth/oauth/rejestracja`. +**Why human:** Browser behaviour; only the URL helpers are unit-tested. + +#### 3. Prohibition verdicts + +**Test:** Read the Prohibitions table above. +**Expected:** Confirm the "no violation found" verdicts. +**Why human:** Judgment-tier prohibitions need human sign-off. + +Live Google, Facebook and Microsoft round-trips with real credentials are not required by the goal. The four goth wrappers are exercised against faked provider endpoints (`TestGothProviders`, `TestGothProvidersSmoke`), and the full flow runs end to end through registry-registered fakes on Postgres. Live setup stays tracked in 19.5-USER-SETUP.md (the demo's nginx does not yet route `/oauth`). + +### Gaps Summary + +No blocking gaps. All four ROADMAP success criteria and all 25 CONTEXT decisions have code, wiring and passing tests. I re-ran the full fail-closed gate here with Docker required, and it exited 0. The released plugin v0.1.1 (d7ee28d) is the commit both applications pin, and every repository is pushed. What remains needs a human: +- the bm callback view's browser behaviour; +- a decision on the password-registration variant of pre-account takeover, which is PHP-parity behaviour that the T-19.5-38 fix (option b) does not cover and its documentation overstates; +- sign-off on the judgment-tier prohibitions. + +--- + +_Verified: 2026-10-21_ +_Verifier: Claude (msd-verifier)_