Repository navigation
Description
Important
Amended by #1274: see #1281 (comment). Needs: becomes #1277. The gate starts after a per-Owner grace period, reads the user's enrolment from the existing epoch check, lets the forced password change through and also applies to connected-app tokens. This slice ships no database migration and comes last.
Epic: #1273 · Size: M · Needs: #1278, #1280
Context
#320 notes that "Owner should be able to require a specific factor for privileged user-admin once >1 factor exists". The maintainer decided on this rule: every Owner must use two-factor authentication. For Managers it stays optional, and a farm setting that requires it for Managers is deferred.
An Owner holds farm configuration (#729) and user administration (#308), so a stolen Owner password is the most damaging single credential in a farm.
Scope
- Every Owner must enrol. An Owner without two-factor is sent to enrolment after the password step, and reaches nothing else except enrolment and logout until they finish. This works like
MustChangePasswordMiddleware, and the design record (2FA slice 1: design record for TOTP at login and as a step-up factor #1274) sets the order between the two. - An Owner cannot turn their own two-factor off. An Owner reset of another Owner or
recover-admin(both 2FA slice 7: turn off and reset TOTP, by the user, an Owner, or recover-admin #1280) clears it, and the Owner must then enrol again at the next sign-in. - Managers, Workers and ReadOnly users may enrol, and are never forced to.
- Deferred, not built here: a farm setting that requires two-factor for Managers.
Acceptance criteria
- An unenrolled Owner can reach only enrolment and logout. A test walks the middleware order next to
MustChangePasswordMiddleware. -
bootstrap-adminandprovision-accountproduce an Owner who signs in, changes the generated password, enrols, and then reaches the app. A test covers the whole chain. - After
recover-admin, the Owner signs in with the temporary password, changes it, and is sent to enrolment. - Promoting a user to Owner forces enrolment at their next sign-in. Demoting an Owner lifts the requirement.
- A Manager, a Worker or a ReadOnly user signs in without enrolling.
- The Playwright fixture and the k6 scripts that sign in as an Owner still work. The design record decides how the simulation Owner gets a known TOTP secret (Daily entry: require grading to reconcile sellable eggs before submit #394).
- GLOSSARY, Help, en/es/tl strings, and screenshots of the forced enrolment at 1280 and 390.
Activity
mforce commentedon Oct 21, 2026
The #1274 record (decisions 8, 14, 16, 18 and 22) settles these:
MustChangePasswordMiddlewareinProgram_PinsTheCompleteCredentialGateSequence. It allows change-password, enrolment, step-up, logout and the enrolment screen's reads, and skips the exception-handler re-execution. An unenrolled Owner past the deadline is refused with 403Auth.TwoFactorEnrolmentRequiredand noWWW-Authenticatechallenge. The gate reads enrolment from the row through the epoch check, and the SPA routes to enrolment on thetwo_factorclaim, which the coordinator agent chose. From the Owner's grace deadline, the gate refuses an unenrolled Owner's connected-app tokens, including tokens issued before this slice, and gates the consent route.TwoFactor:OwnerGraceDaysdays (default 14) fromSecondFactorRequiredSince, which 2FA slice 3: tenant guard on AspNetUserTokens and TOTP secret storage #1276 adds. The agents moved the start to the Owner's first authenticated request after this slice, through the credential check that already runs on every request. That request sets the start once, enrolled or not, with a session or a connected-app token, and role changes and resets never move it. Until the deadline, the gate refuses nothing, connected-app tokens and consent included, and the Owner sees "Set up two-factor by " (the status route gainsrequiredBy). After a reset, an Owner past the deadline enrols again at the next request. Sign-in is never gated. After the deadline, the setup screen is where the app sends the Owner.CredentialEpochVerifierreturnsCredentialCheck(Verdict, TwoFactorEnabled, SecondFactorRequiredSince)from its existing read, and the middleware hands it to the gate, so the gate adds no query and covers tokens that never refresh. A missing check throws. A null start is set byIOwnerGraceStart.StartAsync, given the gate's ownnow, one guardedExecuteUpdateAsyncthat also rotatesConcurrencyStamp, asSetLanguageAsyncwrites the row. If it fails with anything other thanOperationCanceledException, which the gate rethrows, the request passes, the app logsAuth.OwnerGraceStartFailed, and the next request retries, as I decided on 2026-10-21 ("Log an alert event (Recommended)"). A start more than 5 minutes ahead of now counts as past the deadline. The test seed helpers set the start for seeded Owners, from the host'sTimeProvider. The status route returnsrequiredByto an Owner only. The gate andCredentialEpochMiddlewareshare one logout predicate.OwnerGraceDaysbefore redeploying.reset.sh, after the password change, with no exemption flag. The key goes into.sim-cast.json. Playwright, the verify client, k6 andreset.shcompute codes, and wait for a fresh step after each accepted code or use distinct users. The Playwright/connectrun, the consent-with-a-code case from 2FA slice 6: TOTP code as a step-up factor, including connected-app consent #1279,mutants.tsand the session specs land here (G6.5).roleclaim, the epoch check's two columns and that setting (G6.3). The simulation app service has noenv_file:and does not set the days, so it cannot postpone enforcement (G6.8).enrolled: trueand get codes from their recovery codes or a controlledTimeProvider, whichTotpMatcheralso reads, with no wall-clock waits. One shared helper completes the second factor, andLoginAsyncandLoginForAccessTokenAsyncuse it for an enrolled user. G6.6 runs a representative set of Owner endpoints with an enrolled Owner through that helper.CluckworkWebApplicationFactoryraisesRateLimiting:SecondFactor:PermitLimit, and a gate test past the deadline setsSecondFactorRequiredSincein the past directly.recover-admin. The forced change applies (2FA slice 7: turn off and reset TOTP, by the user, an Owner, or recover-admin #1280). The Owner keeps the original grace start, so the gate sends the Owner to enrolment only once that deadline has passed.recover-adminor another Owner.Acceptance criterion to add:
Out of scope: an Owner requiring two-factor for other roles (#1336).
Guards: G6.1 to G6.8 in the record.