Tasks: Auth token-target issuer guard (#4755)

Mission: token-target-issuer-guard-01M319HS Branch: issue-4755-token-target-issuer-guard (planning base + local merge target; PR lands on main) Spec: spec.md · Plan: plan.md · Contract: contracts/issuer-target-helper.md

Completion is event-sourced — record subtasks with spec-kitty agent tasks mark-status Txxx --status done. The rows below are reference rows, not checkboxes.

Subtask Index

IDDescriptionWPParallel
T001IssuerTargetMismatchError(AuthenticationError) in auth/errors.pyWP01
T002Issuer decision + resolve_token_endpoint(session) in auth/server_target.pyWP01
T003Helper unit tests (all branches)WP01
T004Refresh: consume endpoint; guard at TokenManager boundaryWP02
T005saas_client: add error to _dead_session_errors; held-token non-demotion; guard consumes shared decisionWP02
T006Rehydrate: thread session; fail-closed no-op + specific warningWP02
T007me_fetch: strip-less rstrip → canonical normalizerWP02
T008Red-first repro: consequence 1 (self-hosted) refreshWP02
T009Red-first repro: consequence 2 (hostile env) refresh + held-tokenWP02
T010Rehydrate mismatch unit test (token-free)WP02
T011Revoke: RevokeOutcome.ISSUER_MISMATCH; guard before tryWP03[P]
T012_auth_logout: local teardown proceeds + warn revoke skippedWP03[P]
T013Red-first repros: logout consequence 1 + consequence 2WP03[P]
T014Revoke/logout unit tests (token-free)WP03[P]
T015WS: guard before URL/get_access_token; wrap WebSocketProvisioningErrorWP04[P]
T016WS direct-invocation tests (mismatch / match / legacy)WP04[P]
T017WS chained pre-connect-refresh refusal propagationWP04[P]
T018Arch fence: forbid get_saas_base_url in the four token-send modules + allowlist floor + self-mutationWP05
T019Arch fence: second direction — allowlist excludes the four token-send modulesWP05
T020format_saas_mismatch_warning consumes shared decision (keeps returning); config.py docstringWP05
T021Fold #4053: dead except ConfigurationError branch/import/label in _auth_saas_target.pyWP05
T022Fold #4265: auth login mismatch exit code + canonical-URL comparisonWP05
T023Fence-both-directions + display-still-renders + positive-membership + fold unit testsWP05

Work Packages

WP01 — Shared issuer-target authority (foundation)

Goal: one canonical decision (resolve target → compare issuer → verdict) + the new typed error, with the single normalizer. Priority: P1 (blocks all). Independent test: helper unit tests cover match / mismatch / legacy-null / null-session / split-brain. Subtasks: T001–T003. Deps: none. Est: ~250 lines. Prompt: tasks/WP01-shared-issuer-target-authority.md

WP02 — TokenManager wiring (refresh + rehydrate) + held-token non-demotion

Goal: wire refresh + rehydrate to the issuer, guard at the TokenManager boundary, and close the held-token swallow re-leak; unify the saas_client guard. Priority: P1. Independent test: two red-first refresh repros + rehydrate unit. Subtasks: T004–T010. Deps: WP01. Est: ~460 lines. Prompt: tasks/WP02-tokenmanager-refresh-rehydrate-wiring.md

WP03 — Revoke + logout teardown

Goal: revoke targets the issuer or refuses (ISSUER_MISMATCH) with local teardown preserved. Priority: P1. Independent test: logout red-first repros. Subtasks: T011–T014. Deps: WP01. Est: ~320 lines. Prompt: tasks/WP03-revoke-logout-teardown.md

WP04 — WS provisioning (defence-in-depth)

Goal: close the dormant ws leak via the guard, wrapped into WebSocketProvisioningError. Priority: P2. Independent test: direct-invocation tests. Subtasks: T015–T017. Deps: WP01. Est: ~200 lines. Prompt: tasks/WP04-ws-provisioning-guard.md

WP05 — Arch fence + display unification + campsite folds

Goal: non-vacuous fence (both directions) so the class cannot recur; unify the display consumer; fold #4053/#4265. Priority: P1 (recurrence prevention). Independent test: fence self-mutation both directions + positive-membership + display-renders. Subtasks: T018–T023. Deps: WP01, WP02, WP03, WP04 (the fence goes green only once all flows are rewired). Est: ~420 lines. Prompt: tasks/WP05-arch-fence-and-campsite-folds.md

Dependencies

WP01 ──┬── WP02 ──┐
       ├── WP03 ──┤
       └── WP04 ──┴── WP05

MVP scope

WP01+WP02+WP03 close both live consequences (refresh, logout, rehydrate). WP04 is defence-in-depth (dormant path); WP05 hardens against recurrence and folds the campsite findings.