---
title: 'Multi-Factor Authentication'
source: 'https://academia.sh/en/courses/identity-and-cryptography/multi-factor-authentication'
course: 'Identity, Access and Cryptography'
language: en
updated: '2026-10-21T05:02:56+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Separates factor types by the path set they close and counts the path left open'
    - 'Derives the paths no factor closes, along with their classes'
    - 'Calculates the cost of closing in terms of registration, recovery, and left-out endpoints'
---

# Multi-Factor Authentication

Measuring factor types by the path they close: in a set of 10 paths, no factor type closing more than 6, 4 paths closing with none of the six types, using two factors that close the same path together not increasing the closed path count, and closing costing between 428 and 856 registration items for 214 people, 9 to 31 recovery requests, and up to 41 left-out endpoints.

The previous lesson counted a single authentication carried to 15 applications, and that single
authentication opening 1562 connections. Authentication itself still rested on a password: if
the password is known, all of the connections open in a single step. **Multi-factor
authentication** splits that single step in two and ties login to a second proof.

Factor classes' independence, the derivation of the time-based code, and the storage of
recovery codes were measured in the **Authentication and Authorization** course; they are not
repeated here. This lesson asks a single question: which factor type closes **which path**, and
how many paths stay open. What is measured is not how a path would be walked, but whether a given
type closes it or not; nowhere in this lesson is there a working sequence of steps. Attempting
such a thing without written authorization and a defined scope is both illegal and
unprofessional.

## A Factor Narrows a Path Set

**EI14:** a factor type closes a **subset** of account takeover paths; the set it closes is a
property of its type, not its strength. **EI15:** no factor type closes every path; the paths
that stay unclosed sit on registration and recovery processes, not on the factor. **EI16:**
using two factors that close the same path together does not increase the number of closed paths
— it only increases the cost.

Measurement runs on three columns. The first is the number of closed paths, the second the
number of paths left open, the third the cost. The cost reads in three items: registration steps
per person, lost-factor requests opened per year, and the number of endpoints left out because
they cannot carry the factor. **A defense whose cost is not written down counts as unmeasured**;
this rule runs through M24 and holds here too.

## Ten Paths, Six Factor Types

The path set below is a **model**: it assumes a set of 214 accounts at a fictional regional
measurement network organization. There is no real organization, no real account, and no working
sequence of steps.

```js
// factor/paths.mjs — account takeover paths and which paths factor types close (model):
// no real organization, no real account, and no working sequence of steps; only which factor
// type closes which path is counted. No randomness.

// EI14: a factor type closes a subset of a path set; the set it closes is a property of
// its type, not its strength. EI15: no factor type closes every path; the paths that stay
// unclosed sit on registration and recovery processes, not on the factor.
// The class column tells which stage the path operates at.
export const PATH = `
  password-reuse                  credential
  password-guessing               credential
  password-harvesting-page        credential
  real-time-proxy-page            auth-moment
  approval-fatigue                auth-moment
  channel-interception            factor-channel
  backup-recovery-channel         recovery
  post-auth-session                post-auth
  registration-compromise         registration
  process-based-reset             process`
  .trim().split("\n").map((r) => {
    const p = r.trim().split(/\s{2,}/);
    return { name: p[0], cls: p[1] };
  });

// closes = the path indices the factor type closes; registration = registration steps per
// person; recovery = lost-factor requests opened per year for 214 people; leftOut = the
// number of endpoints that cannot carry the factor.
export const FACTOR = {
  "password":           { closes: [], registration: 1, recovery: 0, leftOut: 0 },
  "sms-code":           { closes: [0, 1, 2, 4], registration: 2, recovery: 18, leftOut: 6 },
  "time-code":          { closes: [0, 1, 2, 4, 5], registration: 3, recovery: 31, leftOut: 12 },
  "push-notification":  { closes: [0, 1, 2, 5], registration: 3, recovery: 27, leftOut: 24 },
  "matching-approval":  { closes: [0, 1, 2, 4, 5], registration: 3, recovery: 27, leftOut: 24 },
  "hardware-factor":    { closes: [0, 1, 2, 3, 4, 5], registration: 4, recovery: 9, leftOut: 41 },
};

export const PEOPLE = 214;
export const closedBy = (...t) => new Set(t.flatMap((x) => FACTOR[x].closes));
```

```js
// factor/tally.mjs — how many paths each factor type closes, how many stay open, which
// paths close with no factor at all, and what closing costs in terms of registration,
// recovery, and left-out endpoints.
import { PATH, FACTOR, PEOPLE, closedBy } from "./paths.mjs";

const print = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const types = Object.keys(FACTOR);

console.log(`${PATH.length} paths, ${types.length} factor types, ${PEOPLE} people`);

const A = [-20, 9, 8, 11, 11, 10];
console.log("\n1. paths closed and left open per factor type");
print(A, "factor", "closed", "open", "reg items", "recovery", "left out");
for (const t of types) {
  const f = FACTOR[t];
  print(A, t, f.closes.length, PATH.length - f.closes.length,
    PEOPLE * f.registration, f.recovery, f.leftOut);
}

const B = [-28, -18, 10, 12];
console.log("\n2. per path: how many factor types close it");
print(B, "path", "class", "closing", "left open");
for (let i = 0; i < PATH.length; i++) {
  const closing = types.filter((t) => FACTOR[t].closes.includes(i));
  print(B, PATH[i].name, PATH[i].cls, closing.length, types.length - closing.length);
}
const none = PATH.filter((_, i) => types.every((t) => !FACTOR[t].closes.includes(i)));
console.log(`paths closed by no factor ${none.length}/${PATH.length}: ` +
  none.map((p) => p.cls).join(", "));

const C = [-37, 9, 8, 11];
console.log("\n3. if two factors are used together");
print(C, "combination", "closed", "open", "reg items");
for (const [a, b] of [["sms-code", "time-code"], ["time-code", "matching-approval"],
  ["push-notification", "time-code"], ["matching-approval", "hardware-factor"]]) {
  const c = closedBy(a, b);
  print(C, `${a} + ${b}`, c.size, PATH.length - c.size,
    PEOPLE * (FACTOR[a].registration + FACTOR[b].registration));
}

const D = [-20, 9, 12, 15, 15];
console.log("\n4. cost per closed path");
print(D, "factor", "closed", "reg/path", "recovery/path", "left out/path");
for (const t of types.filter((x) => FACTOR[x].closes.length > 0)) {
  const f = FACTOR[t], n = f.closes.length;
  print(D, t, n, (PEOPLE * f.registration / n).toFixed(1), (f.recovery / n).toFixed(1),
    (f.leftOut / n).toFixed(1));
}
```

```
10 paths, 6 factor types, 214 people

1. paths closed and left open per factor type
factor                 closed    open  reg items   recovery  left out
password                    0      10        214          0         0
sms-code                    4       6        428         18         6
time-code                   5       5        642         31        12
push-notification           4       6        642         27        24
matching-approval           5       5        642         27        24
hardware-factor             6       4        856          9        41

2. per path: how many factor types close it
path                        class                closing   left open
password-reuse              credential                 5           1
password-guessing           credential                 5           1
password-harvesting-page    credential                 5           1
real-time-proxy-page        auth-moment                1           5
approval-fatigue            auth-moment                4           2
channel-interception        factor-channel             4           2
backup-recovery-channel     recovery                   0           6
post-auth-session           post-auth                  0           6
registration-compromise     registration               0           6
process-based-reset         process                    0           6
paths closed by no factor 4/10: recovery, post-auth, registration, process

3. if two factors are used together
combination                             closed    open  reg items
sms-code + time-code                         5       5       1070
time-code + matching-approval                5       5       1284
push-notification + time-code                5       5       1284
matching-approval + hardware-factor          6       4       1498

4. cost per closed path
factor                 closed    reg/path  recovery/path  left out/path
sms-code                    4       107.0            4.5            1.5
time-code                   5       128.4            6.2            2.4
push-notification           4       160.5            6.8            6.0
matching-approval           5       128.4            5.4            4.8
hardware-factor             6       142.7            1.5            6.8
```

The numbers are of the **measurement** class; their inputs are EI14–EI16 and the path set.

## Closed Path, Open Path

The first table runs all six types over the same path set. Password alone closes zero paths;
all ten of the ten paths are open. When a second factor is added, the number of closed paths
rises to four or five, and **it does not exceed six in any type**. The origin-bound hardware
factor, the most comprehensive type, closes six paths and leaves four open.

The first three of the closed paths are the same in every type: the password being known from
somewhere else, being guessed, or being typed only into a password-harvesting page. The second
factor closes these three because what is missing in all three is the second proof. In enterprise
login, these three rows are the real gain multi-factor authentication provides, and type choice
makes no difference for these three.

The difference starts after the fourth row. The real-time proxy page — a middleman that forwards
the code the user enters straight to the real login at the same moment — is closed by only
**one** type: the origin-bound hardware factor. The reason is not that the type is stronger; it is
that the proof it produces is bound to **the address it is presented to**; when presented to a
different address, the proof is not valid. Any type that reads and types a code cannot test who is
asking for the code.

The fifth and sixth rows knock out two types one at a time. Approval fatigue — repeated,
unrequested approval prompts — is only open with instant push notification; a prompt like that
never arises with types that ask for a code. Channel interception, in turn, is only open with
the SMS code, because the channel itself is the factor's carrier. **Each type opens a path born
of its own mechanics**, and that path is as much a property of the type as the paths it closes.

The fourth row's weight comes from the previous lesson. In the organization, 15 applications are
bound to a single identity provider; the second factor is applied once, at that provider's login
page, and the path closed there closes for all 15 applications. In the same way, the path that
**stays open** there stays open for all 15 applications too. This is centralization's
counterpart in this lesson: the type choice is made at a single point and its result is
multiplied across 1562 connections. The two-path difference between four and six is not two paths
in a single account — it is two paths across the entire organization.

## The Four Paths No Factor Closes

The second table is this lesson's most important result. Four of the ten paths are closed by
**none** of the six types: the backup recovery channel, use of the post-authentication session,
the registration moment being compromised, and process-based reset. All four sit outside the
factor.

The class column gives the reason. The recovery path is the backup channel that kicks in when
the factor is lost, and if that channel is weaker than the second factor, the account's strength
drops to the backup channel's strength. The post-authentication session is the object produced
after login completes; the factor has finished its job by that point, and the 72-hour session
window from the previous lesson is exactly the size of this row. The registration moment is when
the second factor **first binds** to the account: if it is bound to the wrong person at that
moment, the factor treats the wrong person as correct on every subsequent login. Process-based
reset, meanwhile, is not a technical item — it is the existence of a person who can reset the
factor.

**The strength of multi-factor authentication is the strength of its weakest recovery path.**
Factor type choice determines the first six rows; what determines the last four rows is how
registration and recovery processes are written, and that is not an item that can be bought.

Four rows share one more property: none of them show up on the login screen. The six closed
paths can be checked off and reported on an audit list; the four unclosed ones can only be found
by searching through process documents. An organization reporting its multi-factor
authentication coverage as one hundred percent means these four rows were never asked about at
all. A coverage report measures six paths at best, not ten; the four-row gap between them never
gets written into the report, and because it is unmeasured, it belongs to no one.

## This Mechanism's Trust Root

The course's first column shows up in an unexpected place here. Multi-factor authentication's
trust root is not the factor itself — it is **the registration moment where the factor binds to
the person**. At registration, the organization must hold proof that whoever is registering is
really that person; without that proof, every subsequent login works flawlessly and treats the
wrong person as correct.

The chain from the root to use is four items: identity proof at the moment of registration, the
record of the factor's binding to the account, the authority governing changes to that binding,
and the recovery record. All four sit in the directory; which means this lesson's root rests on
the integrity of the repository measured in the first lesson. Among the 646 items that must be
renewed when repository integrity breaks, these four are included, and they are the most
expensive to renew — renewing a factor binding requires physically re-recognizing the person.

This is why the second table's registration row closes with no factor at all. **A mechanism
cannot verify its own root**; the factor carries no item that would test whether it is bound to
the right person. The correctness of the registration is the output of a process outside the
mechanism, and that process rests on none of the keys this course measures.

## Two Factors Are Not Better Than One

The third table counts EI16. Using the SMS code together with the time-based code closes five
paths; the time-based code alone also closes five paths. The closed path is the same; the
registration item rises from 642 to 1070. The combination of matching approval and the
time-based code also stays at five.

The reason is simple: if two types close the same subset, their combination is that subset. No
combination in the table closes more than the widest type it contains closes on its own:
matching approval with the hardware factor closes six, and the hardware factor alone also closes
six. **Duplicating a factor does not close paths; adding a type whose closed set differs does.**

This does not mean two factors are useless. A second type keeps access going when the first cannot
be used, and lowers the number of recovery requests — that is, it gets written to the recovery
column in the first table, not the closed-path column. A decision that does not separate the two
columns doubles the cost while leaving the closed path unchanged.

## The Cost of Closing

The fourth table divides the cost per closed path. The SMS code costs 107.0 registration items
and 1.5 left-out endpoints per path closed; the hardware factor costs 142.7 registration items
and 6.8 endpoints. The left-out endpoint count for the hardware factor rises to 41, and this is
a portion of the legacy consoles and field devices counted in the first lesson.

There is one more item the table does not cover, and it belongs to coverage more than to cost. The
214 accounts counted are people's accounts; the 21 service accounts counted in the second lesson
are outside this measurement. A second factor cannot be placed in front of a service account,
because there is no one on the other side to give approval. On the organization's identity
surface, multi-factor authentication covers **interactive logins**, and every non-interactive
account stays open to the first three of the ten paths. The coverage ratio is 214/235, and each
of the 21 accounts left out represents an entire service, as measured in the second lesson.

The recovery column reads in the opposite direction. Nine requests are opened per year for the
hardware factor; thirty-one for the time-based code. The difference between them comes from how
often devices get replaced, and every request opens the recovery path from the second table one
more time. **The number of recovery requests is the measure of how many times a year the
unclosable fourth path gets used**; that is why a low recovery count matters as much as a high
closed-path count.

## Summary

- A factor type closes a subset of the ten-path set; no type closes more than 6, and password
  alone closes 0.
- The first three paths close in every type; only the origin-bound hardware factor closes the
  fourth path, because the proof it produces is bound to the address it is presented to.
- Each type opens a path from its own mechanics: approval fatigue stays open in the notification
  type, channel interception in the SMS type.
- 4 of the ten paths close with none of the six types; all four are in the registration,
  recovery, post-authentication session, and process classes.
- Using two factors that close the same set together does not increase the closed path; it raises
  the registration item from 642 to 1284, and the gain is written only to the recovery column.

## Next Step

This lesson measured login itself. What the user **can do** after login is determined by
another mechanism: the organization writes, from a single place, which setting is to apply
across thousands of machines, and the machines pull what is written at regular intervals. The gap
between what is written and what is applied directly affects a portion of the items in this
lesson's path set. The next lesson measures central enforcement in terms of propagation: how
many machines the policy reaches in how many rounds, how many items it never reaches, and how
many paths local override opens.
