Skip to content

Phase 1: auth middleware strategy-dispatcher refactor#79

Merged
kennethphough merged 4 commits into
masterfrom
feature/phase-1-strategy-dispatcher
Apr 24, 2026
Merged

Phase 1: auth middleware strategy-dispatcher refactor#79
kennethphough merged 4 commits into
masterfrom
feature/phase-1-strategy-dispatcher

Conversation

@kennethphough

Copy link
Copy Markdown
Member

Summary

Phase 1 of the Kyte MCP / auth migration. Refactors the inline HMAC auth code in Api::validateRequest into a strategy-pattern abstraction, without changing behavior for existing clients. Four commits:

  1. Introduce scaffolding as dead code. src/Core/Auth/AuthStrategy.php (interface), HmacSessionStrategy.php (wraps the legacy three-layer HMAC-SHA256 flow line-for-line), AuthDispatcher.php (first-match-wins router). No wiring yet. Bumps $key, $signature, $utcDate from private to public on Api so strategies can populate.
  2. Wire the dispatcher behind a per-install flag. AUTH_STRATEGY_DISPATCHER constant added to $defaultEnvironmentConstants with default 'off'. When 'off', legacy inline auth runs — byte-identical behavior to v4.2.0. When 'on', AuthDispatcher::buildDefault() selects a strategy, preAuth() runs before URL parsing, verify() runs after response hydration — same two points where the legacy code called parseIdentityString and verifySignature, preserving response-body ordering on 403s.
  3. Live shadow-mode harness. AuthShadowHarness::runAndCompare(). When AUTH_STRATEGY_DISPATCHER='shadow', legacy auth runs normally and serves the response. After it completes, the harness resets Api state to pre-auth, re-runs the dispatcher, and compares fingerprints (key_id, account_id, user_id, response[session/token/uid/name/email], signature, utc_epoch). Divergences log via ActivityLogger as AUTH_SHADOW_DIFF; new-path exceptions log as AUTH_SHADOW_EXCEPTION. Legacy state is restored in a finally block before the caller returns.
  4. Parallel characterization tests + strategy-selection log. tests/HmacSessionStrategyTest.php with 12 tests exercising matches / preAuth / verify directly — same edge cases as Phase 0's SignatureTest (boundary, expired, malformed, unknown key/account, undefined-session quirk). A lightweight VERBOSE_LOG-gated error_log of the selected strategy name.

Flag mechanism decision

Design doc O2 originally proposed AppConfig for this flag. Revised here to a per-install PHP constant because AppConfig is keyed by kyte_account (required), which the auth flow itself is responsible for resolving — chicken-and-egg. Per-install constants match the pattern of IS_PRIVATE, SIGNATURE_TIMEOUT, etc. Design doc O2 updated with the rationale.

Known side effects of shadow mode

Documented in AuthShadowHarness.php:

  • SessionManager::validate() runs twice per request during shadow (legacy, then new). Each call may update a session's last_activity timestamp. Acceptable for the dev-only soak; should not be enabled on customer installs.
  • One extra DB read for KyteAccount + KyteAPIKey per request.

After the Phase 1 cutover on a given install, shadow mode is never used again there.

Testing

  • 30/30 unit tests green (18 Phase 0 + 12 new), 103 assertions, zero warnings / zero deprecations under PHPUnit 11 / PHP 8.2.
  • CLI smoke test of flag=on with real DB fixtures: dispatcher selects hmac_session, preAuth loads account + key, verify accepts signature.
  • CLI smoke test of flag=shadow with matching-state case (no diff logged) and forced-mismatch case (diff detected, log attempted, state cleanly restored).

Explicitly deferred to Phase 2

  • Structured AUTH_STRATEGY_SELECTED activity-log entries. With only one strategy today, per-request logging would be noise. When Phase 2 introduces McpTokenStrategy, the stub in Api.php can graduate.

Post-merge plan

  1. Tag v4.3.0.
  2. Upgrade dev Kyte server to v4.3.0, set AUTH_STRATEGY_DISPATCHER='shadow' in its config.php.
  3. Soak for 2–4 weeks. Verify SELECT * FROM KyteActivityLog WHERE action LIKE 'AUTH_SHADOW_%' returns zero rows (or only rows we can explain).
  4. Flip to AUTH_STRATEGY_DISPATCHER='on'. Soak another week on dev.
  5. Open feature/phase-2-mcp-tokens off the tagged commit.

Test plan

  • 30/30 unit tests green locally under Docker (MariaDB 10.5.29, PHP 8.2)
  • GitHub Actions green on push
  • Dev server: install v4.3.0, flag=off → verify legacy path works (no-op upgrade)
  • Dev server: flip flag=shadow → 2-4 week soak, zero unexplained AUTH_SHADOW_* rows
  • Dev server: flip flag=on → 1 week soak, no auth regressions
  • Dev server: flip flag=off → verify rollback still works

🤖 Generated with Claude Code

kennethphough and others added 4 commits April 24, 2026 04:04
…spatcher

Adds the strategy-pattern scaffolding described in design doc section 3.5,
as dead code (not wired into validateRequest yet — that lands in commit 2).

- src/Core/Auth/AuthStrategy.php: two-phase interface (matches, preAuth,
  verify, name). Two phases preserve current validateRequest ordering
  where parseIdentity runs before URL parsing and verifySignature runs
  after response hydration.

- src/Core/Auth/HmacSessionStrategy.php: wraps the existing HMAC-SHA256
  flow (Api::parseIdentityString + Api::verifySignature) with identical
  logic. Handles both IS_PRIVATE=true (sig required) and IS_PRIVATE=false
  (identity-only) flavors. matches() returns false when IS_PRIVATE and
  no signature header, deferring to the /sign helper path.

- src/Core/Auth/AuthDispatcher.php: picks the first matching strategy.
  buildDefault() returns Phase-1 stack (Hmac only). Returning null means
  "no strategy claimed this request" — caller (commit 2) will interpret
  as generateSignature fallthrough.

- src/Core/Api.php: $key, $signature, $utcDate bumped from private to
  public so strategies can populate. No behavior change; characterization
  tests still use reflection and remain green.

18/18 Phase 0 unit tests still passing. Smoke-tested dispatcher selection
via CLI: null when no auth headers (IS_PRIVATE mode), hmac_session when
both present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds per-install constant AUTH_STRATEGY_DISPATCHER (default 'off').
When 'off', the legacy inline auth path runs — bit-for-bit identical
behavior to v4.2.0, and the Phase 0 characterization tests remain the
contract. When 'on', validateRequest selects an AuthStrategy via
AuthDispatcher::buildDefault() and calls its preAuth() / verify() at
the same two points where the legacy code called parseIdentityString /
verifySignature, preserving response-body ordering on failure.

Minor field additions on Api:
- $authStrategy (public, null on legacy path): the selected strategy.
- AUTH_STRATEGY_DISPATCHER entry in $defaultEnvironmentConstants.

Flag mechanism (per-install constant) deviates from design doc O2's
original AppConfig plan. Reason: AppConfig is keyed by kyte_account,
which the auth flow itself is responsible for resolving — chicken-and-egg.
Per-install constants match the pattern used for IS_PRIVATE, SESSION_TIMEOUT,
SIGNATURE_TIMEOUT, etc. Design doc O2 updated to reflect the revision.

18/18 Phase 0 unit tests green with flag=off. Integration-smoke with
flag=on (CLI script using the same DB fixtures as SignatureTest)
exercises matches → preAuth → verify and confirms: account + key loaded,
signature verified, anonymous session state set correctly in
$api->response.

Commit 3 (shadow-mode harness) is the next step before any flag=on
rollout on real traffic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the AUTH_STRATEGY_DISPATCHER='shadow' mode required by the
Phase 1 exit criteria. When enabled, validateRequest:

 1. Snapshots $api->response before any auth work.
 2. Runs the legacy inline auth path normally — this is what serves the
    client response. Shadow never affects serving.
 3. After legacy's verifySignature completes, invokes
    AuthShadowHarness::runAndCompare, which resets Api state to pre-auth,
    runs AuthDispatcher->select()->preAuth()->verify(), compares the new
    strategy's fingerprint against the legacy fingerprint, logs any
    divergence via ActivityLogger (action types AUTH_SHADOW_DIFF and
    AUTH_SHADOW_EXCEPTION), and restores legacy state in a finally block.

Fingerprint compared: key_id, account_id, user_id, response[session],
response[token], response[uid], response[name], response[email],
signature, utc_epoch.

Known side effects during shadow (documented in the harness):
 - SessionManager::validate() runs twice per request. Each call may
   update a session's last-activity timestamp. Acceptable on dev for a
   2-4 week soak; not for production customers.
 - One extra DB read for account + api key per request.

Shadow mode is intended for the Phase 1 cutover soak only — it verifies
HmacSessionStrategy produces identical auth results to the legacy inline
code before we flip to flag=on. After the soak, shadow is never used on
a given install again.

Smoke-tested via CLI with both the matching-state case (no diff logged,
state cleanly restored) and a forced-mismatch case (diff detected, log
attempt made, state still cleanly restored).

18/18 Phase 0 unit tests still green (flag defaults to 'off', shadow
code is dormant).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- tests/HmacSessionStrategyTest.php: 12 parallel characterization tests
  exercising HmacSessionStrategy::matches / preAuth / verify directly.
  Mirrors SignatureTest's coverage (same timestamp-boundary, unknown-key,
  unknown-account, undefined-session, malformed-identity cases) but goes
  through the new strategy surface instead of Api reflection. Together
  with SignatureTest this forms the Phase 1 equivalence contract: the
  strategy must behave identically to the legacy inline code in every
  branch tested.

- phpunit.xml.dist: registers the new test file in the unit suite.

- src/Core/Api.php: small VERBOSE_LOG-gated error_log of the selected
  strategy name when flag=on. Full per-request ActivityLogger entry was
  considered but deferred to Phase 2: with only one strategy today, an
  AUTH_STRATEGY_SELECTED row per request would be constant noise. When
  Phase 2 adds McpTokenStrategy and the selection becomes informative,
  this stub can graduate to a structured activity-log entry.

30/30 unit tests green (18 original + 12 new), 103 assertions, zero
warnings, zero deprecations.

This completes Phase 1 on branch feature/phase-1-strategy-dispatcher.
Next steps before Phase 2:
 - Push branch, open PR, confirm CI green.
 - Dev-server soak with flag=shadow for 2-4 weeks; verify zero
   AUTH_SHADOW_DIFF log entries before flipping flag=on.
 - Tag v4.3.0 at Phase 1 exit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kennethphough
kennethphough merged commit 2c677c4 into master Apr 24, 2026
3 checks passed
@kennethphough
kennethphough deleted the feature/phase-1-strategy-dispatcher branch May 22, 2026 11:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant