diff --git a/.planning/phases/19.6-two-factor-auth-and-device-flow-in-sm-user-plugin/19.6-UAT.md b/.planning/phases/19.6-two-factor-auth-and-device-flow-in-sm-user-plugin/19.6-UAT.md new file mode 100644 index 00000000..8bcd0172 --- /dev/null +++ b/.planning/phases/19.6-two-factor-auth-and-device-flow-in-sm-user-plugin/19.6-UAT.md @@ -0,0 +1,40 @@ +--- +status: testing +phase: 19.6-two-factor-auth-and-device-flow-in-sm-user-plugin +source: [19.6-VERIFICATION.md] +started: 2026-10-26T16:03:15.784Z +updated: 2026-10-26T16:03:15.784Z +--- + +## Current Test + +number: 1 +name: Run a real-browser WebAuthn ceremony against an instance with webauthn in two_factor_available_methods, served over HTTPS with a hostname RP ID: register a security key (POST 2fa/webauthn/register/options, navigator.credentials.create, POST 2fa/webauthn/register), sign out, sign in with the password and complete the challenge through 2fa/webauthn/options and 2fa/webauthn/verify. With two_factor_passwordless_login on, register a passkey (passwordless true) and sign in through 2fa/webauthn/passwordless/options and /verify. +expected: | + Registration answers {message, credential:{id,name,created_at}, status} (plus recovery_codes on the first set); the challenge completes with {token, user}; the passkey sign-in issues a token with no second challenge; a second-factor-only key is refused for passwordless with 401 WebAuthn verification failed. +awaiting: user response + +## Tests + +### 1. Run a real-browser WebAuthn ceremony against an instance with webauthn in two_factor_available_methods, served over HTTPS with a hostname RP ID: register a security key (POST 2fa/webauthn/register/options, navigator.credentials.create, POST 2fa/webauthn/register), sign out, sign in with the password and complete the challenge through 2fa/webauthn/options and 2fa/webauthn/verify. With two_factor_passwordless_login on, register a passkey (passwordless true) and sign in through 2fa/webauthn/passwordless/options and /verify. +expected: Registration answers {message, credential:{id,name,created_at}, status} (plus recovery_codes on the first set); the challenge completes with {token, user}; the passkey sign-in issues a token with no second challenge; a second-factor-only key is refused for passwordless with 401 WebAuthn verification failed. +result: [pending] + +### 2. On the demo (vue-demo-app served by sm-demo-app at the v0.1.13 / v0.1.2 / v0.1.5 pins): sign in, open /account/security, enable TOTP by scanning the QR code with a real authenticator app and entering a code, keep the recovery codes shown once, sign out, sign in again and answer the challenge with an app code; repeat with a recovery code; then disable TOTP. +expected: The QR renders as an image and scans; the recovery codes appear once after confirm; the login page switches to the code step on two_factor_required and stores the token after verify or recovery; a used recovery code is refused the second time; after disable the login issues a token directly. +result: [pending] + +### 3. Review the judgment-tier prohibition verdicts in the 'Prohibitions' section of this report (non-authoritative LLM-judge verdicts, 19 items across plans 01-04). +expected: Confirm or dispute each verdict; all are recorded as no violation found. +result: [pending] + +## Summary + +total: 3 +passed: 0 +issues: 0 +pending: 3 +skipped: 0 +blocked: 0 + +## Gaps diff --git a/.planning/phases/19.6-two-factor-auth-and-device-flow-in-sm-user-plugin/19.6-VERIFICATION.md b/.planning/phases/19.6-two-factor-auth-and-device-flow-in-sm-user-plugin/19.6-VERIFICATION.md index 0f5f6cbc..01fda9d4 100644 --- a/.planning/phases/19.6-two-factor-auth-and-device-flow-in-sm-user-plugin/19.6-VERIFICATION.md +++ b/.planning/phases/19.6-two-factor-auth-and-device-flow-in-sm-user-plugin/19.6-VERIFICATION.md @@ -1,6 +1,6 @@ --- phase: 19.6-two-factor-auth-and-device-flow-in-sm-user-plugin -verified: 2026-10-26T15:58:30Z +verified: 2026-10-26T16:05:13Z status: human_needed score: 45/45 must-haves verified covered_files: @@ -13,7 +13,7 @@ covered_files: - .planning/phases/19.6-two-factor-auth-and-device-flow-in-sm-user-plugin/19.6-04-PLAN.md - .planning/phases/19.6-two-factor-auth-and-device-flow-in-sm-user-plugin/19.6-04-SUMMARY.md - scripts/check-phase19.6.sh -covered_digest: "v2:sha256:0f825ccda8084c2d5ee75b6cdf0337c23e2682ba80413cdf44a5a640f3690320" +covered_digest: "v2:sha256:96a0670daff17ba82f47a71e37a4bbbdb227e5dbe07d458f1a268f3389078072" # Files outside this repository are pinned by commit instead of digest (all equal to the release tags on the remotes): # sm-user-plugin 07196a4 = v0.1.13 (checkout fonoteka.go/plugins/golem15/user), sm-mfa-plugin 463c750 = v0.1.2, # sm-sociallogin-plugin ecfb8a7 = v0.1.5, vue-demo-app 44a5229 (origin master), sm-demo-app 8fcdbdd (origin master; @@ -22,6 +22,8 @@ covered_digest: "v2:sha256:0f825ccda8084c2d5ee75b6cdf0337c23e2682ba80413cdf44a5a # b752177), fonoteka.go e36577a (user gitlink 07196a4, unpushed by design). behavior_unverified: 0 overrides_applied: 0 +# Refreshed after bc8f954 (gate requires the six review-fix regression tests): --self-test and --docker re-run, digest recomputed. +# Human items below seed 19.6-UAT.md (created 2026-10-26T16:03:15Z, 3 pending, no results yet). human_verification: - test: "Run a real-browser WebAuthn ceremony against an instance with webauthn in two_factor_available_methods, served over HTTPS with a hostname RP ID: register a security key (POST 2fa/webauthn/register/options, navigator.credentials.create, POST 2fa/webauthn/register), sign out, sign in with the password and complete the challenge through 2fa/webauthn/options and 2fa/webauthn/verify. With two_factor_passwordless_login on, register a passkey (passwordless true) and sign in through 2fa/webauthn/passwordless/options and /verify." expected: "Registration answers {message, credential:{id,name,created_at}, status} (plus recovery_codes on the first set); the challenge completes with {token, user}; the passkey sign-in issues a token with no second challenge; a second-factor-only key is refused for passwordless with 401 WebAuthn verification failed." @@ -37,9 +39,9 @@ human_verification: # Phase 19.6: Two-factor auth and device flow in sm-user-plugin Verification Report **Phase Goal:** `sm-user-plugin` has the PHP user plugin's second factors and device login: TOTP, WebAuthn, email codes, recovery codes and trusted devices, a challenge step during login, and QR/code device authorization. Per CONTEXT D-01 the second factors live in the new `sm-mfa-plugin`; per D-03 the device flow stays in sm-user-plugin. -**Verified:** 2026-10-26T15:58:30Z +**Verified:** 2026-10-26T16:05:13Z (refreshed after bc8f954; first pass 15:58:30Z) **Status:** human_needed -**Re-verification:** No, initial verification +**Re-verification:** No, initial verification, refreshed once after the gate change bc8f954 (no prior VERIFICATION had gaps) **MVP-mode note:** ROADMAP marks the phase `mode: mvp`, but the goal is not a user story. Every plan records the decision not to invent one (as in 19.5), so this report runs standard goal-backward verification against SC-1..SC-4, D-01..D-20 (with D-12a/b/c) and the 41 plan must-haves. No "User Flow Coverage" table is produced; the user-facing demo flow is human item 2. @@ -57,8 +59,8 @@ Abbreviations: **U** sm-user-plugin, **M** sm-mfa-plugin, **S** sm-sociallogin-p |---|-------|--------|----------| | SC-1 | TOTP setup/confirm/disable, WebAuthn registration and login challenges, email codes, recovery codes and trusted devices work against the PHP contract; the WebAuthn dependency has a decision note before code lands | VERIFIED (browser interop: human item 1) | M `routes.go` mounts PHP's 5 challenge routes and all 13 management routes, compared one by one with PHP `routes.php:303-336,372-453`. Note `24d9d2a` (09:49) lands before M's first go.mod commit `5c6ad2f` (11:05) and the go-webauthn require `2662252` (12:32). Gate `--mfa` and `--docker`: TestTOTP, TestQRSVG, TestRecovery, TestEmailCode, TestTrustedDevice, TestWebAuthn, TestPasswordless and TestPHPErrorBodies all PASS. The WebAuthn tests use a software authenticator with real ECDSA and CBOR (`webauthn_authenticator_test.go`). | | SC-2 | Login with a second factor returns the unauthenticated challenge step, rate-limited by `2fa-verify` | VERIFIED | U `controllers/api_controller.go:139-157` consults the gates and answers the gate body. M `classes/gate.go` returns `{two_factor_required, challenge_token, available_methods, expires_at}`. M `buckets.go` sets 10/min per client IP, and `routes.go` puts the challenge group under `throttle:2fa-verify`. TestGate, TestLoginGateSmoke and TestRoutesAndBuckets (11th request 429) PASS. | -| SC-3 | `devices/initiate`, `devices/status/{token}`, `devices/authorize`, device list and revoke work on `DeviceAuthSession` | VERIFIED | U `routes.go` mounts the 5 routes in the user-api group. U `models/device_auth_session.go` and migration `202610260001_create_device_auth_sessions` match PHP's columns (v2.1.0-v2.7.1) plus `jwt_jti`. TestDeviceInitiate, TestDeviceStatus, TestDeviceAuthorize, TestDeviceHandOverConcurrent, TestDeviceListAndRevoke, TestDeviceRevokeAfterRefresh and TestDeviceFlowSmoke PASS in the gate. I also ran TestDeviceRevokeRacesRefresh and TestDeviceAccessRevocation by name: both PASS. | -| SC-4 | Six models and migrations ported; a security review; unit tests in the last plan | VERIFIED | M `models/` has TwoFactorMethod, TwoFactorChallenge, TwoFactorRecoveryCode, TrustedDevice and WebAuthnCredential; U has DeviceAuthSession. Migrations M `202610220001..0006` and U `202610260001` carry PHP's table and column names (compared with PHP `updates/v3.0.0`, `v3.1.0`, `v2.1.0`). `19.6-SECURITY-REVIEW.md` maps T-19.6-01..38 with a D-14 section. Plan 04 delivered 41 named tests; the gate runs 15+1 U, 21 M and 2 S Postgres tests plus the Docker-free names. | +| SC-3 | `devices/initiate`, `devices/status/{token}`, `devices/authorize`, device list and revoke work on `DeviceAuthSession` | VERIFIED | U `routes.go` mounts the 5 routes in the user-api group. U `models/device_auth_session.go` and migration `202610260001_create_device_auth_sessions` match PHP's columns (v2.1.0-v2.7.1) plus `jwt_jti`. TestDeviceInitiate, TestDeviceStatus, TestDeviceAuthorize, TestDeviceHandOverConcurrent, TestDeviceListAndRevoke, TestDeviceRevokeAfterRefresh and TestDeviceFlowSmoke PASS in the gate. Since bc8f954 the gate also requires TestDeviceRevokeRacesRefresh and TestDeviceAccessRevocation: both PASS. | +| SC-4 | Six models and migrations ported; a security review; unit tests in the last plan | VERIFIED | M `models/` has TwoFactorMethod, TwoFactorChallenge, TwoFactorRecoveryCode, TrustedDevice and WebAuthnCredential; U has DeviceAuthSession. Migrations M `202610220001..0006` and U `202610260001` carry PHP's table and column names (compared with PHP `updates/v3.0.0`, `v3.1.0`, `v2.1.0`). `19.6-SECURITY-REVIEW.md` maps T-19.6-01..38 with a D-14 section. Plan 04 delivered 41 named tests and the review round 6 more; the gate runs 17+1 U, 24 M and 3 S Postgres tests plus the Docker-free names. | **Plan 01 (sm-user-plugin seams and device flow)** @@ -72,7 +74,7 @@ Abbreviations: **U** sm-user-plugin, **M** sm-mfa-plugin, **S** sm-sociallogin-p | 1.6 | D-15: permission `golem15.users.reset_two_factor`, Danger action, offered only with a published TwoFactorAdmin, Run in cabana's transaction | VERIFIED | `users_admin_controller.go:820-847` (`Lookup[classes.TwoFactorAdmin]`, plus the WR-04 privileged guard). `config_form.yaml:17`. TestResetTwoFactorAction PASS. | | 1.7 | D-03/D-11/D-16: 5 routes, PHP columns, SHA-256 token, XXXX-XXXX code, 5-minute TTL, status enum, `jwt_jti`, configurable auth_url | VERIFIED (two recorded deviations) | Migration ID `202610260001` instead of the planned `202610210002` (04-SUMMARY deviation 5). The auth_url default is now `{frontend_url}/device/scan/{short_code}` instead of D-11's `{token}` (WR-01, released in v0.1.13, user decision in DISPOSITION). Authorize accepts a short code in `token`, so QuestStream's `useDeviceAuth.ts`/`useProfile.ts` keep working. | | 1.8 | D-12a: the first poll after confirm gets `{status:'confirmed', user_id, message, token, user}` through one guarded UPDATE; concurrent polls give one winner; used and revoked never expire | VERIFIED | `devices.go:298-300` (`WHERE status='confirmed' AND expires_at > NOW()`). `ExpiredAt` exempts used and revoked. TestDeviceHandOverConcurrent (50 polls) PASS. Since WR-01 the hand-over also requires the initiating User-Agent. | -| 1.9 | D-12b: revoke blacklists the jti; refresh follows the jti | VERIFIED | `DeviceDestroy` runs `UPDATE … RETURNING jwt_jti` and calls `Lookup[bouncer.BlacklistStore]` (`devices.go:534-552`). `Refresh` calls `FollowDeviceJTI` and fails closed (`api_controller.go:316-333`, CR-01). TestDeviceRevokeAfterRefresh PASS (gate); TestDeviceRevokeRacesRefresh PASS (my run). | +| 1.9 | D-12b: revoke blacklists the jti; refresh follows the jti | VERIFIED | `DeviceDestroy` runs `UPDATE … RETURNING jwt_jti` and calls `Lookup[bouncer.BlacklistStore]` (`devices.go:534-552`). `Refresh` calls `FollowDeviceJTI` and fails closed (`api_controller.go:316-333`, CR-01). TestDeviceRevokeAfterRefresh and TestDeviceRevokeRacesRefresh PASS (gate, since bc8f954). | | 1.10 | D-12c: authorize needs only a valid bearer JWT; one guarded pending-to-confirmed UPDATE; no 2FA re-check | VERIFIED | `devices.go:412-414`. TestDeviceAuthorize PASS. | | 1.11 | D-17: every change to U is additive; S and A compile | VERIFIED | Gate additive check: "no exported declaration removed or changed … since 50da92c (63 declarations added; 1 allowed migration)". A builds in the fonoteka stage. S and M build with `GOWORK=off` at their own requires (exit 0, my run). | @@ -82,7 +84,7 @@ Abbreviations: **U** sm-user-plugin, **M** sm-mfa-plugin, **S** sm-sociallogin-p |---|-------|--------|----------| | 2.1 | D-01: module `git.golem15.com/golem15/sm-mfa-plugin`, plugin `golem15.mfa` requires `golem15.user`; Boot needs no database | VERIFIED | `plugin.go:72-89`: Boot only adds the gate, publishes TwoFactorAdmin and listens. Gate `--mfa` checks the pins. | | 2.2 | SC-2: challenge body and the 2fa-verify bucket | VERIFIED | Same evidence as SC-2. | -| 2.3 | D-05: PHP 401 and 422 bodies; TTL 5, 5 attempts, one completion | VERIFIED | `classes/challenge.go:96,119,137` (guarded `attempts < max_attempts`, `completed_at IS NULL`). TestChallengeLifecycle (20 parallel verifies, one token) and TestPHPErrorBodies PASS. Plus WR-02 (TestChallengeAfterRevocation) and WR-07 (TestCompletionFailureSpendsNothing), both PASS in my run. | +| 2.3 | D-05: PHP 401 and 422 bodies; TTL 5, 5 attempts, one completion | VERIFIED | `classes/challenge.go:96,119,137` (guarded `attempts < max_attempts`, `completed_at IS NULL`). TestChallengeLifecycle (20 parallel verifies, one token) and TestPHPErrorBodies PASS. Plus WR-02 (TestChallengeAfterRevocation) and WR-07 (TestCompletionFailureSpendsNothing), both required by the gate since bc8f954 and PASS. | | 2.4 | D-04/D-05: TOTP setup `{secret, qr_code_svg, qr_code_url}`, ±1 step, encrypted secret, replay refused, disable | VERIFIED | `controllers/management.go:78-80`, `classes/totp.go`, `classes/qr.go`. TestTOTP (replay), TestQRSVG and TestTokensAtRest PASS. | | 2.5 | D-05/D-19: recovery codes, 10 × `XXXXX-XXXXX`, bcrypt, single use, count only, regenerate, first set returned | VERIFIED | `classes/recovery.go:19-26,136`. TestRecovery and TestRecoveryCodeFormat PASS. | | 2.6 | D-05/D-18 A8 and A9: 6-digit bcrypt email code, template `golem15.mfa::mail.two_factor_code` (and -en), gating, 3 sends, `expiry_minutes` = min | VERIFIED | `classes/email.go:22,102`, `mail.go:22-23`. TestEmailCode and TestEmailExpiryMinutes PASS. Code lifetime is bound to the challenge, as PHP's `EmailMethod::verify` is (IN-02 is PHP parity). | @@ -101,7 +103,7 @@ Abbreviations: **U** sm-user-plugin, **M** sm-mfa-plugin, **S** sm-sociallogin-p | 3.2 | D-05/D-06: WebAuthn state server-side, consumed once; PHP register, verify and remove bodies | VERIFIED | `cache.Pull` in `classes/webauthn.go:318` and `classes/passwordless.go:69`. TestWebAuthn and TestWebAuthnShapes/registration_state PASS. | | 3.3 | D-16: `golem15_user_webauthn_credentials` with PHP columns + `credential_data`, `passwordless`, unique `credential_id` | VERIFIED | Migration `202610220006`. TestMigrations PASS. | | 3.4 | D-14: passwordless routes in the bucket, 403 while off, passkeys only, UV required, activation, ban and suspension enforced, Source passkey, no second challenge | VERIFIED | SECURITY-REVIEW "D-14" table. TestPasswordless and TestPasswordlessSmoke PASS. The WR-05 partial (passkeys demoted on a password change) is pinned by TestPasswordless/password_change_demotes_passkeys. | -| 3.5 | D-09: S consults the gate in signIn (Source social), skipped by default, the challenge body on both completions, refusals and errors fail closed, link never gated | VERIFIED | S `classes/flow.go:343-359`. TestSocialGate PASS (gate). WR-08 TestChallengedSignInFiresOnCompletion PASS (my run). | +| 3.5 | D-09: S consults the gate in signIn (Source social), skipped by default, the challenge body on both completions, refusals and errors fail closed, link never gated | VERIFIED | S `classes/flow.go:343-359`. TestSocialGate PASS (gate). WR-08 TestChallengedSignInFiresOnCompletion PASS (gate, since bc8f954). | | 3.6 | D-20: the demo sets up TOTP with QR and confirm, shows recovery codes once, disables, and answers the login challenge with TOTP or a recovery code; bm stays API-only | VERIFIED (code and unit; browser: human item 2) | D `pages/account/security.vue` uses `useTwoFactor` (`totp/setup`, `confirm`, `DELETE totp`) and renders ``. `login.vue` and `useDemoAuth.ts` call `2fa/verify` and `2fa/recovery`. The gate's demo-web stage requires `twoFactor.test.ts`. B `frontend/src` has no 2FA code (grep). | | 3.7 | D-17: release order, D and B mount mfa and bump pointers in separate commits, A bumps user and updates TestNoDeferredRoutesResolve without mounting mfa | VERIFIED (B unpushed, recorded) | Remote tags and heads as in "Release state". D `summer.yaml:40` and `plugins.gen.go:12,44`; B `summer.yaml:8` and `plugins.gen.go:9,21`. A `phase07_coverage_test.go:97-100` drops `/devices` and keeps `2fa`. A's `summer.yaml` has no mfa. | | 3.8 | D-01: A has no /2fa route, no 2FA table, no second-factor module | VERIFIED | Gate `--fonoteka` and my own `go list -m all`. | @@ -110,7 +112,7 @@ Abbreviations: **U** sm-user-plugin, **M** sm-mfa-plugin, **S** sm-sociallogin-p | # | Truth | Status | Evidence | |---|-------|--------|----------| -| 4.1 | SC-4: unit tests for every U seam, every M decision and S's gate, on Postgres where needed | VERIFIED | Gate `--docker`: 15 U root, 1 U updates, 21 M and 2 S tests by name. The Docker-free names pass in their stages. | +| 4.1 | SC-4: unit tests for every U seam, every M decision and S's gate, on Postgres where needed | VERIFIED | Gate `--docker` (after bc8f954): 17 U root, 1 U updates, 24 M and 3 S tests by name. The Docker-free names pass in their stages. | | 4.2 | D-02/D-18 A10 table tests | VERIFIED | TestLoginGates (`-race`) and TestLoginGateOrder PASS. | | 4.3 | Device-flow table tests | VERIFIED | The 8 device tests and TestDeviceAuthSessionsMigration PASS. | | 4.4 | M tests pin every PHP body and decision | VERIFIED | TestPHPErrorBodies compares error and 422 bodies byte for byte. Success bodies are compared by key set because Go sorts keys (recorded observation). | @@ -137,7 +139,7 @@ Abbreviations: **U** sm-user-plugin, **M** sm-mfa-plugin, **S** sm-sociallogin-p | S `classes/flow.go` | VERIFIED | `CheckLoginGates` | | D `vue-demo-app/app/pages/account/security.vue` | VERIFIED | The tool flags the missing literal `totp/setup`. The page calls `useTwoFactor().setupTotp()`, which posts to `/_user/api/v1/2fa/totp/setup` (`composables/useTwoFactor.ts`). Wired. | | D `mfa_test.go` (TestMFAMounted) | VERIFIED | PASS in `--demo` | -| `scripts/check-phase19.6.sh` | VERIFIED | My `--all` run exited 0 | +| `scripts/check-phase19.6.sh` | VERIFIED | My `--all` run exited 0 with the script before bc8f954; after bc8f954 (six names added to the docker lists, `bash -n` clean) `--self-test` and `--docker` exit 0 | | M `login_paths_test.go`, `testdata/PHP_METHOD_MAP.md`, `19.6-SECURITY-REVIEW.md` | VERIFIED | verify.artifacts 4/4 | ### Key Link Verification @@ -174,8 +176,9 @@ The `verify.key-links` tool rejects paths outside this repository, so I checked | Behavior | Command | Result | Status | |----------|---------|--------|--------| | Whole phase gate at the released pins, Docker required | `PHASE196_REQUIRE_DOCKER=1 TESTCONTAINERS_HOST_OVERRIDE=127.0.0.1 bash scripts/check-phase19.6.sh --all` | exit 0; self-test, go, userplugin (5 + 1 `-race` + 1, additive check), mfa (11), sociallogin (9), demo (3), demo-web, bm (2), fonoteka, docs, docker (15 + 1 U, 21 M, 2 S) | PASS | -| Review-fix regression tests outside the gate (U) | `go test -count=1 -run '^(TestDeviceRevokeRacesRefresh\|TestDeviceAccessRevocation)$' ./plugins/golem15/user` (fonoteka.go) | both PASS | PASS | -| Review-fix regression tests outside the gate (M, S) | `go test -run '^(TestChallengeAfterRevocation\|TestPasswordConfirmationThrottle\|TestCompletionFailureSpendsNothing)$' .` in M; `-run '^TestChallengedSignInFiresOnCompletion$'` in S | all PASS (4 + 2 + 2 subtests) | PASS | +| Gate after bc8f954: detectors and Docker stage | `bash scripts/check-phase19.6.sh --self-test`; `PHASE196_REQUIRE_DOCKER=1 TESTCONTAINERS_HOST_OVERRIDE=127.0.0.1 bash scripts/check-phase19.6.sh --docker` | self-test passed; docker exit 0: 17 + 1 U, 24 M, 3 S tests by name. Only the docker lists changed, so no other stage is affected. | PASS | +| Review-fix regression tests, run by name before bc8f954 (U) | `go test -count=1 -run '^(TestDeviceRevokeRacesRefresh\|TestDeviceAccessRevocation)$' ./plugins/golem15/user` (fonoteka.go) | both PASS | PASS | +| Review-fix regression tests, run by name before bc8f954 (M, S) | `go test -run '^(TestChallengeAfterRevocation\|TestPasswordConfirmationThrottle\|TestCompletionFailureSpendsNothing)$' .` in M; `-run '^TestChallengedSignInFiresOnCompletion$'` in S | all PASS (4 + 2 + 2 subtests) | PASS | | Demo module-mode build at the tags | `GOWORK=off GOFLAGS=-mod=readonly go build -trimpath .` (sm-demo-app) | ok; resolves user v0.1.13, mfa v0.1.2, sociallogin v0.1.5 | PASS | | M and S build at their own go.mod requires (user v0.1.12) | `GOWORK=off go build ./...` | exit 0 both | PASS | | A carries no second-factor module | `go list -m all \| grep -E 'webauthn\|pquerna\|boombuler'` (fonoteka.go) | no match | PASS | @@ -226,12 +229,12 @@ REQUIREMENTS.md maps no IDs to this phase ("Requirements: TBD"), so no requireme | Area | Active | Skipped | Circular | Assertion level | Verdict | |------|--------|---------|----------|-----------------|---------| -| U gate, device and password tests | 15 + 1 Docker, 7 Docker-free | 0 in the gate run; `t.Skip` only under `-short`, and the gate counts a skip as a failure | no | behavioral (races, transactions, bodies) | OK | -| M decision tests | 21 Docker, 11 Docker-free | 0 | no; PHP bodies are literals from PHP sources, PHP_METHOD_MAP from PHP's test file | value and behavioral | OK | -| S gate tests | 2 Docker + WR-08 test | 0 | no | behavioral | OK | +| U gate, device and password tests | 17 + 1 Docker, 7 Docker-free | 0 in the gate run; `t.Skip` only under `-short`, and the gate counts a skip as a failure | no | behavioral (races, transactions, bodies) | OK | +| M decision tests | 24 Docker, 11 Docker-free | 0 | no; PHP bodies are literals from PHP sources, PHP_METHOD_MAP from PHP's test file | value and behavioral | OK | +| S gate tests | 3 Docker (WR-08 included) | 0 | no | behavioral | OK | | D `twoFactor.test.ts` | in the demo-web vitest run | 0 | no | value (`svgDataUrl` has no `<`) | OK | -Disabled tests on requirements: 0. Circular patterns: 0. Insufficient assertions: 0. **Warning:** the six regression tests the review round added (TestDeviceRevokeRacesRefresh, TestDeviceAccessRevocation, TestChallengeAfterRevocation, TestPasswordConfirmationThrottle, TestCompletionFailureSpendsNothing, TestChallengedSignInFiresOnCompletion) are not in the gate's required-name lists. They pass (my run), but `check-phase19.6.sh` would not catch a later regression of CR-01, WR-02, WR-03, WR-06, WR-07 or WR-08. +Disabled tests on requirements: 0. Circular patterns: 0. Insufficient assertions: 0. The first pass warned that the review round's six regression tests (TestDeviceRevokeRacesRefresh, TestDeviceAccessRevocation, TestChallengeAfterRevocation, TestPasswordConfirmationThrottle, TestCompletionFailureSpendsNothing, TestChallengedSignInFiresOnCompletion) were outside the gate's required-name lists. **Resolved by bc8f954:** the gate now requires all six, and its `--docker` stage passes with them. ### Anti-Patterns Found @@ -279,6 +282,8 @@ I scanned the 137 files the phase touched (U `50da92c..07196a4`, all of M, S `v0 ### Human Verification Required +These three items are seeded into `19.6-UAT.md` (status testing, 3 pending). That file records no results yet, so Step 8b settles none of them. + #### 1. Real-browser WebAuthn ceremony **Test:** On an instance with `webauthn` in `two_factor_available_methods`, served over HTTPS with a hostname RP ID, register a security key and complete a login challenge with it (`2fa/webauthn/register/options` → `navigator.credentials.create` → `2fa/webauthn/register`; then `2fa/webauthn/options` → `navigator.credentials.get` → `2fa/webauthn/verify`). With `two_factor_passwordless_login` on, register a passkey and sign in through `2fa/webauthn/passwordless/options` and `/verify`. @@ -299,9 +304,9 @@ I scanned the 137 files the phase touched (U `50da92c..07196a4`, all of M, S `v0 ### Gaps Summary -No gaps. Every roadmap criterion, every CONTEXT decision and every plan must-have is backed by code at the released tags and by named tests that pass in my own run of the phase gate with Docker required, plus the six review-fix regression tests I ran separately. The deviations from plan wording (AccessRevokedEvent for D-07, migration ID `202610260001`, the auth_url default changed by WR-01) are recorded in the SUMMARYs or the review disposition and keep each decision's intent. Before `passed`, three human items remain: a real-browser WebAuthn ceremony, the demo's TOTP flow in a browser, and the prohibition verdicts. Worth knowing beyond those: sm-bm-app's origin still runs the pre-review plugin versions until its held push, and the gate does not yet require the review round's regression tests by name. +No gaps. Every roadmap criterion, every CONTEXT decision and every plan must-have is backed by code at the released tags and by named tests that pass in my own run of the phase gate with Docker required, including the six review-fix regression tests the gate requires since bc8f954. The deviations from plan wording (AccessRevokedEvent for D-07, migration ID `202610260001`, the auth_url default changed by WR-01) are recorded in the SUMMARYs or the review disposition and keep each decision's intent. Before `passed`, three human items remain: a real-browser WebAuthn ceremony, the demo's TOTP flow in a browser, and the prohibition verdicts. Worth knowing beyond those: sm-bm-app's origin still runs the pre-review plugin versions until its held push. The first pass also noted that the gate did not require the review round's regression tests; bc8f954 resolved that (re-verified: `--self-test` and `--docker` pass). The three human items are seeded into `19.6-UAT.md` (3 pending, no results yet, so Step 8b settles none). --- -_Verified: 2026-10-26T15:58:30Z_ +_Verified: 2026-10-26T16:05:13Z (refreshed after bc8f954)_ _Verifier: Claude (msd-verifier)_