On this page
1 · What you do from your end Operator
cp7 is not one flip — it's a paced rollout. Your part:
- Approve the enrollment drive first. Before any enforcement, staff must enroll. Right now only 3 of 36 users have an authenticator. Nothing can be enforced until that number climbs.
- Send the enroll ask (we can draft the message + link them to
/pure-mfa.html). Give a deadline — e.g. 14 days. - Choose the enforcement order — recommend starting with one small org (or just admins) to prove it, then widening.
- Give go per org once its people are enrolled. Code flips that org's flag; you test one login with 2FA.
- Be reachable for break-glass — as super-admin you can reset a stuck user's factor. (~2 min.)
2 · Current state — why we can't just flip it grounded 7/7
Enrollment plumbing already exists: the pure-mfa.html TOTP card (cp6), and the pure_mfa_enroll + pure_mfa_recovery tables. What's missing is a per-org "required by" date and the gate logic — both additive.
3 · The structural guarantee — no lock-out, ever
The gate follows one rule that makes lock-out impossible:
Concretely, at sign-in the gate asks three questions in order:
- Does this user's org require MFA yet? If no allow (aal1). Most orgs stay off until you turn them on.
- Does this user have a verified factor? If no they are in grace: allowed in, but shown an "enroll now" nudge (hard-blocking to the enroll page only after the org's deadline, and even then the enroll page itself is always reachable).
- They have a factor and the org requires it complete the 6-digit challenge to reach aal2. This is the only case that ever demands a code — and only from someone who provably can produce one.
Paired with the pre-flight guard — an org's flag only flips once 100% of its active users have a verified factor — case 2-after-deadline effectively never fires for real staff. A brand-new hire still lands on the always-open enroll page, never a dead end.
4 · The four layers Code
| Layer | What it is | Type |
|---|---|---|
| Per-org flag | Add mfa_required boolean + mfa_required_at timestamptz to pure_orgs. Default off. This is the master switch you control per org. | Additive schema |
| Client gate | pure-auth.js reads the aal claim from the access token after login and applies the §3 three-question logic — redirect to challenge or enroll as needed. UX layer. | Additive JS |
| Server RLS (durable) | On sensitive tables, add a policy requiring (auth.jwt()->>'aal')='aal2' — but only via a helper that exempts users whose org isn't enforcing yet. The real boundary a bypassed client can't dodge. | Additive policy |
| Break-glass + recovery | Recovery codes (pure_mfa_recovery) issued at enroll; super-admin can reset a user's factor. Guarantees a way back in. | Existing + wiring |
5 · Rollout — enroll drive, then org-by-org
Announce it, link /pure-mfa.html, set a 14-day deadline. Track the count climb from 3/36. No enforcement in this phase.
Add the pure_orgs MFA columns (all off) and ship the pure-auth.js gate reading them. With every flag off, behavior is unchanged — safe to deploy anytime.
Turn on a non-blocking "enroll now" banner for users without a factor. Still no blocking. Measures readiness.
Pick a small, fully-enrolled org (or the admins). Pre-flight guard confirms 100% enrolled flip its mfa_required=true. You test one login with 2FA. If good, add aal2 RLS to that org's sensitive data.
Repeat D org by org as each reaches full enrollment. Never a global flip.
6 · Break-glass & recovery
- Recovery codes: issued at enrollment, stored hashed in
pure_mfa_recovery. A user who loses their phone uses one to get in, then re-enrolls. - Admin reset: super-admin (you) can delete a user's factor via the hat/admin console, dropping them back to grace so they can re-enroll.
- Self-lockout of the last admin — prevented: the guard refuses to enforce for an org if it would leave zero reachable admins with a working factor.
7 · Verification matrix (per org, at Phase D)
| Test | Expected | Who |
|---|---|---|
| Enrolled user, correct code | Reaches aal2, full access | Op |
| Enrolled user, wrong code | Blocked at challenge, retry — not locked out | Op |
| User in a non-enforcing org | Signs in normally (aal1), unaffected | Op |
| New hire, no factor, enforcing org | Lands on the always-open enroll page — never a dead end | Code |
| Recovery code | Grants entry, prompts re-enroll | Code |
| aal1 session hitting aal2-gated data | RLS denies at DB (durable) | Code |
8 · Rollback seconds
Per org: set mfa_required=false on that org — enforcement stops instantly for its users. Global: a single window.PURE_MFA_ENFORCE_OFF kill-flag in the client gate disables all challenge redirects; dropping the aal2 RLS policies reverts the durable layer. No data is changed by any of it.
9 · Go / no-go gate
- GO for Phase B/C (dormant + nudge) anytime — they change nothing until a flag flips.
- GO for Phase D on an org only if: cp5 has been stable ≥24h, that org is 100% enrolled, recovery codes are working, and you're present to test + hold the admin-reset break-glass.
- NO-GO if: any active user in the target org lacks a verified factor, or recovery/admin-reset isn't confirmed, or cp5 isn't proven yet.