State and precedence
The tables the implementation is written against. Where this document and the code
disagree, the code is wrong — every rule here has a test named after it in
endpoint/tests/.
1. Precedence when deciding what governs a sign-in
Evaluated in this order. The first rule that applies decides.
| # | Condition | Outcome | Test |
|---|---|---|---|
| 1 | Deployment trust or the signed license is missing, unverifiable, or the effective configuration is not usable on this build | Block. The provider does not arm and setup fails. Never "carry on permissively". | bootstrap/an_unusable_configuration_blocks_rather_than_permits |
| 2 | The subject is a local account matching a pinned recovery SID, on the console, with recovery enabled | Local recovery exemption. Precedes block and MFA alike. Windows still checks the password. | recovery/recovery_precedes_a_blocked_path |
| 3 | The subject is any other local account | Block. A local account is never offered to Core as a domain user. | recovery/an_ordinary_local_account_is_not_a_recovery_account |
| 4 | The channel's effective action is ignore | Leave to Windows. Coverage unprotected; no observation claimed. | policy/ignore_reports_unprotected_and_log_reports_alternate_paths |
| 5 | The channel's effective action is log | Observe only. Windows authenticates; we record what we actually see. | same |
| 6 | The channel's effective action is block | Block. Not a firewall, not an RDP listener shutdown. | policy/block_is_refused_while_another_provider_could_complete_the_sign_in |
| 7 | The scenario is not one this build supports | Block. An unsupported scenario is not represented as intercepted. | bootstrap/an_unusable_configuration_blocks_rather_than_permits |
| 8 | Not yet activated, bootstrap_mode is enforce_until_activated | Block. A deliberate lockout risk, overridden only by rule 2. | bootstrap/enforce_until_activated_blocks_covered_sign_ins |
| 9 | Not yet activated, bootstrap_mode is phone_home_first | Allow, basis bootstrap_inactive. A bypass, audited as one. Not an MFA success. | bootstrap/phone_home_first_falls_through_to_windows_when_core_is_unreachable |
| 10 | mode is pass_through and now < pass_through_until | Allow, basis tenant_pass_through. Administrative bypass with a deadline. | grace/tenant_pass_through_applies_offline_and_expires |
| 11 | Otherwise | Intercept. Run the Core-directed flow. | — |
An expired pass_through_until falls through to rule 11, not to unrestricted login.
2. Precedence when Core cannot be reached
Evaluated in order, only for an intercepted path.
| # | Condition | Outcome |
|---|---|---|
| 1 | A known persistent denial applies to this device, user or association | Deny. An older grant is never consumed to override it. |
| 2 | Not yet activated and phone_home_first | Allow, basis bootstrap_inactive |
| 3 | Tenant pass-through still inside its deadline | Allow, basis tenant_pass_through |
| 4 | A valid cached policy_exception lease covers this user and scenario | Allow, basis policy_exception_lease. Not grace, not counted against the grace budget. |
| 5 | The transport failure is not an outage (TLS trust, refused redirect, oversized response, cancellation, missing credential) | Deny. Section 8: a security or configuration error never becomes a grace-eligible timeout. |
| 6 | A valid grace grant covers this user and scenario, the cause is eligible, and budget remains | Allow, basis offline_grace. Debited at release. |
| 7 | Otherwise | Deny, with the specific reason (offline_exhausted, offline_expired, offline_quarantined, offline_clock_untrusted, offline_identity_ambiguous, …) |
A completed exchange that returned 401, 403, 429 or any other protocol error takes none of
these branches: it is a decision from Core, or an authentication failure, and it denies.
3. Offline authorization refusals, in evaluation order
| Condition | Result | Why |
|---|---|---|
| Durable state was missing or corrupt | quarantined | A wiped counter file is not a refilled budget |
| A clock rollback is latched | clock_untrusted | Expiry cannot be judged honestly |
| No resolved SID for the account | identity_ambiguous | We cannot tell whether a known denial applies |
| Grace disabled by the current policy | policy_disabled | |
| This failure is not an eligible cause | cause_not_eligible | |
| The grant does not cover this scenario | scenario_not_covered | |
| A persistent denial applies | denial_in_effect | |
Past min(grant.expires_at, issued_at + current max_age_seconds) | expired | The current ceiling caps an older grant |
used >= min(grant.max_submissions, current max_submissions) | exhausted | Likewise |
| The stored grant no longer verifies | grant_invalid | Its consumption record stays behind |
4. Policy snapshot commit
| Incoming version | Payload | Result |
|---|---|---|
> accepted | valid on this build | Accept. A higher version may deliberately relax policy: that is an authorized update, not a downgrade attack. |
> accepted | not usable on this build | Reject, configuration_rejected audited, previous snapshot stays in force |
== accepted | byte-identical | Idempotent no-op |
== accepted | different | Conflict. Core must increment the version to change anything, including to clear a denial. |
< accepted | any | Conflict. A rollback, even validly signed. |
| any | wrong tenant or wrong device | Binding mismatch |
5. Denials
Recorded when a decision denies with a scope wider than attempt. An otp_incorrect
denial is attempt-scoped and is forgotten with the attempt.
Cleared only when all of these hold:
- the decision is not a deny;
- it names the scope in
clear_denial_scopes; - its
policy_versionis strictly greater than the version that recorded the denial; - the subject matches (by Core identifier, or by SID for a user-scoped denial);
- it is bound to a fresh active attempt.
An ordinary allow clears nothing. A policy refresh at the same version clears nothing. A
late allow from an older version cannot defeat a newer denial. Core is required to
increment the device policy revision when it lifts a restriction, precisely so this rule
can be strict.
6. The attempt state machine
Idle ──BeginValidation──▶ Validating ──┬── require_sms ─────▶ WaitingForCode
├── allow_password_only ─▶ AuthorizedOnce
└── deny ───────────────▶ Denied
WaitingForCode ──BeginVerification──▶ Verifying ──┬── allow_mfa ──▶ AuthorizedOnce
├── incorrect, attempts remain
│ ──▶ WaitingForCode
└── otherwise ──▶ Denied
Idle | Validating ──AuthorizeOffline──▶ AuthorizedOnce
(grace · exception lease · tenant pass-through · bootstrap · not intercepted)
AuthorizedOnce ──BeginSubmission──▶ SubmittingToWindows ──▶ Completed
any ──Cancel──▶ Cancelled any ──Fail──▶ Failed
any ──OnCredentialChanged──▶ Idle, new generation, fresh nonce required
Invariants, each with a test:
BeginSubmissionsucceeds exactly once per authorization. A second call is a conflict.AuthorizeOfflinecannot producemfa_verified. MFA success has exactly one source.AuthorizeOfflineis unreachable fromVerifying: once a code has been submitted, the
answer to this attempt is the verification result.
- A wrong Windows password does not leave a reusable approval. The next submission needs
fresh authorization.
- Cancelling before release consumes nothing, because nothing was released.
7. Authorization bases, and what each may be called
| Basis | Is it MFA? | Audit bypass_kind |
|---|---|---|
mfa_verified | Yes — the only one | none |
online_password_only | No | policy_exception |
policy_exception_lease | No | policy_exception |
tenant_pass_through | No | tenant_passthrough |
offline_grace | No | grace |
local_recovery | No | local_recovery |
bootstrap_inactive | No | bootstrap_inactive |
scenario_not_intercepted | No | scenario_unprotected |
IsMfaSuccess() returns true for exactly one value. No report can aggregate an exception
with a verification, because the two never share a code path.
8. Trusted time
| Source | Property | Used for |
|---|---|---|
| Wall clock | Settable by any administrator | The boot anchor only |
| Monotonic (unbiased interrupt time) | Cannot decrease within a boot | Advancing from the anchor |
| Persisted high-water mark | Only ever moves forward | The floor under every expiry check |
Now() is anchor_wall + monotonic_elapsed, never below the high-water mark. The wall
clock is read once, at construction: changing the system time mid-session moves nothing.
A wall clock more than 900 seconds behind the high-water mark latches
rollback_detected, which refuses all time-based offline authorization until a verified
server timestamp clears it. Ordinary NTP corrections and a VM resuming with a stale RTC
sit inside that tolerance.
What this does not do: it is not rollback resistance against an administrator who
restores a disk image or a VM snapshot. That would need a hardware or server mechanism
this MVP does not have, and the proposal says so in those words.