Architecture Decision Record

0020. MFA, step-up, WebAuthn, and passwordless

Context

Multi-factor and passwordless authentication are security table stakes against Entra/Okta/Auth0, and the most security-sensitive place to get the constitution wrong. The factors themselves are unremarkable — a second method in the login ceremony. The hard part is how factor satisfaction reaches the point of enforcement without a core read on the hot path. "This operation requires MFA" must be enforceable at the edge, but whether a principal did MFA is, naively, a fact about a past login — exactly the kind of stateful lookup the constitution forbids on the verification path (ADR-0001 rule 1). This ADR fixes the MFA and passwordless architecture so enforcement stays edge-stateless.

Decision

  1. Factors are composable AuthenticationMethods (ADR-0015), combined by a per-realm authentication policy (ADR-0012). v1-reserved-with-seats: WebAuthn / passkeys (via webauthn-rs, named in ADR-0002), TOTP (a v1 method already), and passwordless flows (passkey-only, magic link). Passwordless is "drop the password factor," not a separate subsystem. Three WebAuthn specifics are pinned now because they touch other seams: the RP ID is the realm's issuer host (ADR-0012) — a realm that later adds a custom domain (proposal 0007) changes its origin, so the custom-domain decision must land before passkeys are registered against it or credentials bind to the wrong RP ID; challenge state lives in the SessionStore (ADR-0016) — ceremony state is region-local issuance-side state, never core state; and the signature counter is best-effort — recorded and alarmed on regression as a clone signal (ADR-0031), but not a strong per-login core write, which would put a strongly-consistent write on the login path that ADR-0012's write-rate ceiling cannot afford (passkey sync ecosystems have already made counters unreliable as a hard check).

  2. Assurance is stamped into the token, enforced at the edge. The factors satisfied and the assurance level reached are recorded as the OIDC amr/acr claims in the issued token (ADR-0015). A relying party — or Vör, for a Vord-protected resource — enforces an MFA requirement by reading the signed token's amr/acr, never by a core query about the user's recent authentication. Assurance is a property of the artifact, so the rule-1 hot path stays pure.

  3. Step-up is a re-authentication at Modgud, not an enforcement read at Vör. When an operation needs higher assurance than the current token carries (OIDC acr_values / max_age), the relying party sends the principal back to Modgud (ADR-0009) for an additional factor; Modgud mints a new token with the higher acr. The ceremony is stateful and at the admission plane; enforcement is the edge reading the new token. The session (ADR-0016) carries assurance so step-up need not re-run satisfied factors.

  4. The amr/acr claim shape is a reserved seat in the token format from v1 (ADR-0003), even though MFA is built later — so that no token-format break is needed when it ships, and so that the v1 issuer already emits assurance claims that future verifiers can rely on.

Consequences

  • The constitution holds at its second-most-tempting point (after ADR-0015): an "MFA-required operation verified at the edge with the core unavailable" is just an extension of the ADR-0010 property test, and it passes, because the proof of MFA is in the token, not the core.
  • Vord gets passkeys, TOTP, and step-up — modern, phishing-resistant authentication — with the same edge-stateless enforcement as everything else; the security feature does not cost a scaling property.
  • WebAuthn registration and assertion are issuance-side ceremonies at Modgud; the passkey public key lives in the directory (ADR-0011) and is read only at login, never at verification (rule 2).
  • Reserving the amr/acr seat now is the small v1 cost that keeps MFA, step-up, and FAPI-grade assurance (ADR-0025) additive — the same reserved-seat discipline ADR-0003 applies to the token format generally.