# Authentication and privacy verification — 2026-09-06

Scope: original findings #2, #4, #11, #12, #31 and both explicitly deferred 2FA view defects. Reviewed claimed fixes against `findings.json`/`fixes.md`, `CLAUDE.md`, actual request/model/mailer behavior, and installed dependencies. All changes are in `/home/stevan/dev/compass-verify-auth`, branch `fix/review-20260906-verify-auth`, initially clean at `d6a057f9f`. The dirty primary checkout and all sibling worktrees were untouched. The primary checkout's two view diffs were read only to preserve their behavioral intent, without copying its redesign.

## Original finding status

| ID | Status | Verification |
| --- | --- | --- |
| #2 | Fixed; additional rollback coverage passes | Admin email updates use credentials and Devise reconfirmation. The old identity works until confirmation, the pending identity fails before confirmation, and the new identity works afterward. Invalid credentials preserve inputs and return 422; missing credentials return 422. Added an invalid account-association case proving both records roll back, no confirmation queues, and the form does not falsely display a sent-email message. Devise 5's reconfirmation callback runs after commit. |
| #4 | Fixed | The new mailer delivers HTML and text with the singular reset URL. A request test queues and delivers the mail and consumes its token while signed out. Missing/blank/expired/incorrect/timestampless tokens fail, consumption clears the token/timestamp/secret, and old tokens fail after reenrollment. Token validation and removal occur under a row lock. The adjacent reset IP throttle bypass was corrected below. |
| #11 | Fixed | The existing filter covers summary, participants, answers, OTP, reset tokens, and the actual nested `two_factor_recovery.code` key. Existing request-log sentinel checks pass for transcript clarification/review, conversation create/update, successful/failed OTP and recovery submissions, and reset URLs. The recovery failure check now also proves HTTP 200 instead of silently accepting a broken error screen. Public provider `code` remains visible. `decisions[id]` remains unfiltered because it carries enum radio values, not free text; the original claim was overbroad. |
| #12 | Fixed; adjacent bypasses corrected | The separate database budget survives password relogin, multiple sessions/IPs, and mixes OTP/recovery failures. Five failures lock for 15 minutes, valid attempts fail while locked, success resets the budget, and expiry starts a fresh budget. A 12-thread real PostgreSQL probe using independently loaded credentials preserved exactly five failures. A second 12-thread recovery-code probe accepted one request only. Format suffix and HEAD bypasses, plus non-string OTP failures, were fixed below. |
| #31 | Fixed and strengthened; synthetic live transport verified | Existing `--no-session-persistence`, disabled built-in tools and empty setting sources were retained. Added `--safe-mode`: installed help explicitly documents that it disables CLAUDE.md, MCP, skills, hooks, plugins and other customizations while keeping authentication available. An authorized synthetic real CLI run with final flags passed subscription auth, returned the exact sentinel, did not launch a temporary MCP command, ignored conflicting temporary CLAUDE.md, and created zero JSONL sessions for its temporary project. |

## Remaining defects fixed during verification

1. **Deferred recovery screen failure:** `app/views/two_factor_auth_recoveries/new.html.haml` called removed `new_proof_of_identity_path`. Both GET and failed recovery POST returned 500. The view now links back to the real authenticator challenge and tells users who lost backup codes to contact their administrator, matching the in-progress redesign's intent. Also corrected its input label target. Regression tests assert the rendered form/link and a successful HTTP response on the actual invalid-code path.
2. **Deferred enabled-status failure:** `app/views/two_factor_auths/show.html.haml` always claimed 2FA was disabled. It now branches on the enrolled state, displays the enabled date and the existing turn-off/backup-code actions, and offers enrollment only when disabled. Both states have request coverage.
3. **Adjacent IP throttle bypass:** exact pathname comparisons ignored `.json` and other Rails format suffixes. After exhausting the canonical budget, `/two_factor_auth/authenticate.json` and `/two_factor_auth_recoveries.json` returned 401 instead of 429; `/two_factor_auth/reset.json` returned 302 instead of 429. HEAD requests invoke the GET reset action but also bypassed its GET-only condition. Anchored matchers cover format suffixes and optional trailing slashes, and the reset limiter includes HEAD. All variants now share their canonical IP budget. Trailing slashes already normalized correctly in the baseline and are retained as regression checks.
4. **Adjacent malformed OTP failure:** a JSON numeric `otp_code` reached the OTP gem's String-only `empty?` call, returned 500 and rolled back the budget. Rejecting non-String codes inside the locked attempt block now records one failed attempt and returns the existing invalid-code flow.
5. **CLI restrictions were incomplete:** installed help says `--tools ''` restricts the built-in tool set, while `--setting-sources ''` selects settings sources; those flags alone do not promise to disable MCP or CLAUDE.md. `--safe-mode` closes this configuration gap and is verified against the actual installed CLI, without sacrificing subscription auth.

## Database and migration evidence

Used only `currencytransfer_test97`, with `DATABASE_URL` removed, `DISABLE_SPRING=1`, `RAILS_ENV=test`, `TEST_ENV_NUMBER=97`, and a synthetic Devise secret. Created the new database and loaded the integration schema. The concurrent probe aborts unless connected to this exact database and cleans only its synthetic credential.

`auth-database-probe.rb` and `auth-database-probe.json` are beside this report. Twelve concurrent OTP/recovery failures all failed, left `otp_failed_attempts=5`, and activated the lock. Twelve simultaneous uses of the same recovery code produced one success and zero remaining codes.

Ran the existing migration down and up inside a transaction on PostgreSQL 16.15 (`server_version_num=160015`), then rolled back the probe. Existing rows received counter zero and a null lock timestamp. The table `relfilenode` stayed unchanged: no heap rewrite. Both operations completed, with the forward migration around 2 ms on this tiny synthetic database. The migration is additive and reversible; production lock-wait time was not measured. No schema or migration changes were needed.

## Commands and results

All Rails commands used this prefix:

```sh
env -u DATABASE_URL DISABLE_SPRING=1 RAILS_ENV=test TEST_ENV_NUMBER=97 DEVISE_SECRET_KEY=synthetic-verification-secret-key
```

- `bundle exec rails db:create db:schema:load` — created and loaded `currencytransfer_test97`.
- Baseline focused suite below — **216 examples, 0 failures**, seed 33063.
- New view/throttle regression checks before fixes — **42 examples, 7 failures**, seed 58686. Failures were precisely the view HTTP/status defects, three format suffix bypasses, and HEAD reset bypass.
- Same suite after view/throttle fixes — **42 examples, 0 failures**, seed 32587.
- Non-string OTP and CLI argument checks before their fixes — **2 examples, 2 failures**, seed 57736.
- Final focused suite below — **228 examples, 0 failures**, seed 6921, 25.71 s execution.
- After simplifying only added test assertions — **24 examples, 0 failures**, seed 2761; final security-settings assertion change — **2 examples, 0 failures**, seed 40065.
- `bundle exec rails runner /tmp/compass-verify-20260906/auth-database-probe.rb` — both concurrency invariants and migration defaults passed.
- `claude --help` — inspected installed flag semantics locally.
- `python3 /tmp/compass-verify-20260906/auth-cli-probe.py` — exit 0, valid JSON, expected synthetic reply, no MCP launch, zero project-session JSONL files, no stderr. Uses final launch flags with `claude-sonnet-5`, synthetic stdin, a temporary working directory, and the same three stripped API environment variables as the app. Output is sanitized in `auth-cli-probe.json`; no credentials or environment were printed.
- `git diff --check` — passed.
- Targeted RuboCop across all eight changed Ruby files reported 13 pre-existing example-length/expectation/time-helper and formatting offenses in existing lines. New-line offenses were corrected; these older unrelated lint issues were not broadly rewritten.

Focused suite:

```sh
bundle exec rspec \
  spec/requests/admins/admins_spec.rb \
  spec/requests/two_factor_auths_spec.rb \
  spec/requests/two_factor_auth_recoveries_spec.rb \
  spec/requests/two_factor_rate_limiting_spec.rb \
  spec/mailers/account_credentials_mailer_spec.rb \
  spec/services/compass/claude_subscription_client_spec.rb \
  spec/requests/admins/compass/transcript_ingestions_spec.rb \
  spec/requests/admins/compass/conversations_spec.rb
```

## Limits and handoff

The live probe verifies synthetic transport/authentication/customization/persistence behavior, not genuine document-extraction semantics or output accuracy. It did not submit any application/user document. Historical pre-fix extraction sessions were not inventoried or removed; that remains the explicitly separate retention/operations task. Request-log filtering was exercised locally; this does not certify external proxy/access-log retention or a live error-monitoring account. The mail flow was delivered with Rails' test delivery method, not external SMTP.

The original five fixes stand with the additional corrections above. No unresolved functional issue from the assigned findings or the two deferred views remains. Read-only manual review of every owned changed line found no further issue. Independent review/integration and broader suite/browser verification remain with the parent; no additional agents or remote publication were invoked by this worker.

## Commits

- `1809d66b4` — `[AUTH] Fix recovery screens and close OTP throttle bypasses` (eight auth/view/config/request-spec files).
- `570d31ee8` — `[AUTH] Disable CLI customizations during transcript extraction` (launcher and its regression spec).

Both commits are local and ready to cherry-pick in that order. Final owned worktree is clean.
