Quickstart / Verification: Auth token-target issuer guard (#4755)
All checks are network-free (fake loopback server / monkeypatched resolver + clients).
Red-first repros (must be RED before the fix)
1. Consequence 1 — self-hosted mis-target: session issuer_url = http://127.0.0.1:PORT set only via config.toml [sync].server_url, access token expired → drive refresh → assert /oauth/token targets the configured host and the refresh token never reaches team.spec-kitty.ai. Same shape for logout → /oauth/revoke. 2. Consequence 2 — hostile-checkout exfil: session issuer_url = https://team.spec-kitty.ai, SPEC_KITTY_SAAS_URL = http://127.0.0.1:ATTACKER → drive logout / refresh / rehydrate → assert every flow refuses and no token reaches the attacker host.
Both pinned @pytest.mark.regression on #4755, through the pre-existing entry points. After the fix they become focused unit tests (not left marked regression).
Post-fix unit assertions
issuer_url=None and session=None → resolved target, no raise, never packaged default.
held-token fast path does NOT return the token on mismatch (M4).
and warns revoke was skipped.
resolve_token_endpoint: issuer==target → normalized endpoint; issuer≠target →IssuerTargetMismatchError;- refresh: mismatch raises and is in
_dead_session_errors();saas_client._usable_access_token - revoke: mismatch →
RevokeOutcome.ISSUER_MISMATCH, no POST;_auth_logoutstill deletes local creds - rehydrate: mismatch →
False+ a warning naming hosts + remedy (token-free). - ws: mismatch →
WebSocketProvisioningError(direct invocation). - diagnostics: token fixture value absent from every refusal message/warning (NFR-006).
- normalizer:
me_fetchURL and the guard comparison agree byte-for-byte.
Unification / arch checks
+ _auth_saas_target.format_saas_mismatch_warning import it (SC-003).
the allowlist → gate RED; legitimate non-token callers → green.
- Positive-membership: the decision lives in one module; the four flows +
saas_client._guard_session_issuer auth status/auth doctorstill render the mismatch as a string (no raise).- Fence: mutate a token-send module to call
get_saas_base_url()→ gate RED; add a token-send module to
Campsite folds
- #4053:
_auth_saas_target.pyno deadexcept ConfigurationError/ import; provenance label correct. - #4265:
auth loginissuer mismatch exits non-zero; custom-endpoint label uses canonical-host compare.
Static gates
ruff check + ruff format --check clean; mypy zero warnings; complexity ≤ 15; blast-radius tests (tests/auth/, tests/saas_client/, tests/cli/, plus tests/architectural/test_egress_consent_boundary.py) green.