Skip to content

Suspension: login can still mint an inert credential in the check-then-mint window #579

Description

@mforce

Split out of #534, where it is listed as required behaviour but was never implemented — deliberately, by a decision recorded in #532. Filing it so the gap is tracked rather than dropped when #534 closes.

The gap

Login does two things in sequence, in separate steps:

  1. checks Account.IsActive (AuthEndpoints, the if (!account.IsActive) branch);
  2. mints the token pair (IdentityProvider.LoginAsync).

A suspend-account that commits between them leaves login returning 200 with a freshly minted refresh-token row that post-dates the suspension's revocation sweep. AccountSuspensionService's header states this plainly today: the guarantee is immediate for use, not for issuance.

Why it is not urgent

The credential produced in that window is inert:

  • its access token is refused on its very next request — CredentialEpochMiddleware re-reads Account.IsActive live, with the epoch already bumped;
  • its refresh token is refused by RefreshAsync's own suspended-farm check;
  • reactivation's epoch bump and revoke sweep destroy the row for good, pinned by AccountSuspensionTests.ReactivationRevokesTheSessionsMintedBetweenSuspendAndReactivate, which inserts exactly this artifact on purpose because the race is not reproducible on demand.

So the observable symptom is a user who appears to sign in and is bounced on their first action — not access to a suspended farm.

Why it was declined

Closing the window requires login to take a FOR SHARE lock on the account row inside its issuance transaction. That puts a lock plus an extra round trip on the login hot path — paid by every farm on every login — to prevent a credential that can never be used. #532 weighed that and declined it on cost, not on difficulty.

Worth noting it is a cost question, not a deadlock question: every locking path in this codebase takes the account row first, so consistent ordering would serialise login against suspension, never deadlock it.

If it is ever picked up

  • The lock belongs in login's issuance transaction, taking the account row before minting, matching the account-first ordering AccountSuspensionService and AdminRecoveryService already use.
  • It needs a test that drives the actual interleaving rather than the inserted artifact the current test uses — that is the harder half, and is why the existing test fabricates the row instead.
  • Epic EPIC: Phase 1.6 — Multi-farm tenancy #530's finish line says "immediate, race-safe suspension". Decide there whether race-safe is meant to cover issuance, or only use; today only use is covered, and EPIC: Phase 1.6 — Multi-farm tenancy #530 decision 15 reads as though that was the intent.

Acceptance

Either the window is closed with a test that exercises the real interleaving, or this is closed as won't-fix with the trade-off written into AGENTS.md / the decision record so the next reader does not re-derive it.

Activity

  1. added
    sliceThin vertical work item
    area:apiAPI/endpoint layer
    epic-1.6Phase 1.6 — Multi-farm tenancy
    on Aug 21, 2026
  2. added 4 commits that reference this issue on Aug 22, 2026
  3. added a commit that references this issue on Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:apiAPI/endpoint layerepic-1.6Phase 1.6 — Multi-farm tenancysliceThin vertical work item

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions