> "Treat your password like your toothbrush. Don't let anybody else use it, and get a new one every six months."
Prerequisites
- 3
- 4
Learning Objectives
- Explain the three authentication factors and map a login flow to NIST 800-63 authenticator assurance levels (AAL).
- Store passwords correctly using a memory-hard hash with per-user salt, and judge a password policy against modern guidance.
- Deploy multi-factor authentication and reason about its failure modes — push fatigue, SIM swap, and OTP phishing.
- Distinguish phishing-resistant MFA (FIDO2/WebAuthn, passkeys) from phishable factors, and explain the cryptographic reason it cannot be relayed.
- Detect and defend against credential stuffing, password spraying, and MFA-fatigue attacks in authentication telemetry.
In This Chapter
- Overview
- Learning Paths
- 16.1 Proving who you are
- 16.2 Passwords: storage, policy, and why they won't die
- 16.3 MFA and its failure modes: push fatigue and SIM swap
- 16.4 Phishing-resistant MFA: FIDO2, WebAuthn, and passkeys
- 16.5 Biometrics
- 16.6 Defending against credential attacks
- 16.7 Project Checkpoint
- Summary
- Spaced Review
- What's Next
Chapter 16: Authentication: Passwords, MFA, Biometrics, Passkeys, and Why Passwords Won't Die
"Treat your password like your toothbrush. Don't let anybody else use it, and get a new one every six months." — Clifford Stoll (a quip that is now half-wrong, and the half that's wrong is the interesting half)
Overview
Return to the morning that opened this book. A loan officer at Meridian Regional Bank received a phishing email that looked exactly like a DocuSign request, clicked the link, reached a pixel-perfect copy of the bank's single sign-on portal, and typed her username and her password into it. At that instant the attacker possessed a valid Meridian credential. In a bank that authenticated with passwords alone, the rest of this book would be about the breach that followed. Instead, the login simply did not complete — because the real portal demanded a cryptographic proof from a small FIDO2 hardware key that the fake portal did not know how to ask for, and that the attacker could not forge or relay. The attacker had the password and got nothing.
This chapter is the inside of that moment. We have referenced the security key that saved Meridian since Chapter 1; now we open it up and show you exactly why it worked when a password did not, why a six-digit code texted to a phone would not have saved her, and how to build authentication that survives a human doing the most human thing imaginable — handing her password to an attacker who asked nicely. Authentication is the act of proving you are who you claim to be, and it is the single most attacked control in the modern enterprise, because identity has quietly become the perimeter. The firewall that used to define "inside" has dissolved (you saw that in Part II); what remains between an attacker and your crown jewels is, more often than not, a login. Verizon's Data Breach Investigations Report has found for years that stolen or weak credentials are involved in a large share of breaches — not exotic zero-days, but logins. Attackers do not break in nearly as often as they log in.
This is also the chapter where we confront an awkward truth in the title: passwords will not die. They have been declared obsolete for two decades and they are still everywhere, and a defender who waits for them to disappear will be defending passwords for the rest of their career. So we do both jobs. We store and govern passwords the way they must be stored when you cannot avoid them, and we move the highest-value logins beyond passwords entirely, to phishing-resistant authentication that an attacker cannot phish, relay, or fatigue a user into approving. By the end you will have written Meridian's authentication standard and the next piece of the bluekit toolkit, and you will be able to look at a login flow and say precisely how strong it is and how it fails.
In this chapter, you will learn to:
- Distinguish the three authentication factors — knowledge, possession, inherence — and combine them into genuine multi-factor authentication.
- Store passwords correctly with a memory-hard, salted hash, and evaluate a password policy against current NIST guidance (which says the opposite of what you were probably taught).
- Deploy MFA and reason clearly about its failure modes: push fatigue, SIM swap, and one-time-code phishing.
- Explain why FIDO2/WebAuthn and passkeys are phishing-resistant at the protocol level, not merely "stronger," and read a WebAuthn challenge-response flow.
- Use biometrics correctly — as a local convenience factor, not a network secret — and understand false-accept/false-reject tradeoffs.
- Detect credential stuffing, password spraying, and MFA-fatigue attacks in your authentication logs, and choose the controls that stop each.
- Map any login to the NIST 800-63 authenticator assurance levels (AAL) so you can match the strength of authentication to the value of what it protects.
Learning Paths
This chapter sits at the center of Part IV — Identity and Access Management — and identity is where the most consequential security engineering of the next decade is happening. Weight your reading by role:
🛡️ SOC Analyst: §16.3 (MFA failure modes — you will triage push-fatigue and SIM-swap alerts) and §16.6 (detecting credential stuffing, spraying, and impossible-travel) are your core. The authentication-log patterns there are among the highest-value detections you will build. 🏗️ Security Engineer: This is a home chapter. §16.2 (password storage), §16.4 (FIDO2/passkeys), and §16.7 (the authentication standard) are the design decisions you will own. Read the WebAuthn flow in §16.4 until you can draw it. 📋 GRC: §16.1 (assurance levels) and the AAL mapping let you tie authentication strength to regulatory obligations (PCI-DSS, GLBA, FFIEC). The policy questions in §16.2 are board-relevant. 📜 Certification Prep: Authentication factors, MFA, biometrics (FAR/FRR/CER), and the something-you-know/have/are model are heavily tested on Security+ and CISSP. The
key-takeaways.mdfile maps each to its exam domain.
16.1 Proving who you are
Before any access decision can be made, a system has to answer one question: who is this? Authentication is the answer. It is worth being precise, because authentication is constantly confused with two of its neighbors, and the confusion causes real security failures.
Authentication is proving that an entity is who or what it claims to be. It is distinct from identification (claiming an identity — typing a username asserts who you say you are; it proves nothing) and from authorization (deciding what an authenticated entity is allowed to do, which is Chapter 17's entire subject). The sequence is always the same: you identify (here is my claimed identity), you authenticate (here is my proof), and only then does the system authorize (here is what that proven identity may do). A login screen asks for both halves of authentication at once — the username is identification, the password is the authentication proof — which is exactly why people blur them.
So how do you prove an identity? Every authentication method in existence rests on one or more of three authentication factors — categories of evidence, each grounded in a different kind of thing an attacker would have to obtain to impersonate you:
- Something you know (the knowledge factor): a password, a PIN, the answer to a security question. Its defining weakness is that knowledge can be copied without taking anything — phished, guessed, observed, or leaked in a breach — and the legitimate owner often never notices it was copied.
- Something you have (the possession factor): a phone running an authenticator app, a hardware security key, a smart card, a SIM that can receive a text. Its strength is that possession is harder to copy at scale than knowledge; its weaknesses depend entirely on how possession is proven (a code read off a screen can be relayed; a hardware key's cryptographic signature cannot).
- Something you are (the inherence factor): a biometric — fingerprint, face, iris, voice. Its strength is that it is bound to the person and hard to forget or lend; its defining weaknesses are that you cannot change it after a compromise and that, on a network, a biometric is only ever as trustworthy as the device that measured it.
A fourth and fifth category are sometimes added — somewhere you are (geolocation/IP) and something you do (behavioral patterns like typing rhythm) — but these are almost always used as signals that feed a risk decision rather than as standalone factors, and we will treat them that way. The three classic factors are the load-bearing model. Memorize them as know / have / are; you will use that triplet for the rest of your career, and the entire next idea is built on it.
🚪 Threshold Concept: Multi-factor authentication does not mean "two passwords" or "a longer login." It means combining factors from different categories, so that compromising one category does not compromise the login. A password plus a security question is one factor used twice — both are something you know, and a single breach or phishing page can capture both. A password plus a hardware key is genuinely two factors, because stealing what you know gives the attacker nothing toward what you have. The security of MFA comes entirely from the independence of the categories. Internalize this and you will never again be fooled by a system that calls a second knowledge question "multi-factor."
Multi-factor authentication (MFA) — sometimes called two-factor authentication (2FA) when there are exactly two — is therefore the practice of requiring proof from two or more different factor categories. It is the single highest-leverage authentication control in existence, and we will spend the back half of the chapter on the enormous quality gap between different kinds of MFA. But first the headline: turning on MFA of almost any kind blocks the overwhelming majority of automated credential attacks, because those attacks trade in stolen passwords (a knowledge factor) and a second factor of a different category renders a stolen password, by itself, useless.
How strong does authentication need to be? That depends on what it protects — reading a public brochure does not warrant the same login as moving a wire transfer. The U.S. government's standard for this is NIST 800-63 (specifically Special Publication 800-63B, Digital Identity Guidelines: Authentication and Lifecycle Management), and its central idea for our purposes is the AAL — the Authenticator Assurance Level, a three-tier scale describing how confident you can be that the person presenting credentials is their genuine owner:
| AAL | Roughly means | Typical authenticators | Phishing-resistant? |
|---|---|---|---|
| AAL1 | "Some confidence" — single factor permitted | A password alone, or a single other factor | No |
| AAL2 | "High confidence" — MFA required | Password + TOTP app or push; or a passkey | Not necessarily |
| AAL3 | "Very high confidence" — hardware-based, phishing-resistant MFA required | A FIDO2/WebAuthn hardware authenticator (or equivalent) proving possession of a private key | Yes (required) |
The practical discipline this gives you is matching: you decide the value and risk of each system, then require the AAL that fits. A bank's customer-facing login might target AAL2; access to the systems that move money, or to domain-administrator credentials, should target AAL3. We will make exactly this decision for Meridian in §16.7. Note the column that will dominate the chapter: AAL3 requires phishing resistance, because at the highest stakes, "the user might be fooled" is not an acceptable residual risk.
🔗 Connection: AAL is the authentication-strength dial; it pairs with the authorization models of Chapter 17 (who may do what once authenticated) and feeds the continuous, context-aware access decisions of zero-trust architecture in Chapter 32, where every request is re-evaluated and the assurance of the authentication is one input to the policy engine. Authentication strength is not a one-time gate; in a zero-trust world it is a signal you re-check.
🔄 Check Your Understanding: 1. A system requires a password and the answer to "What was your first pet's name?" The vendor's brochure calls this "two-factor authentication." Is it? Why or why not? 2. Map each to a factor category: (a) a fingerprint; (b) a six-digit code from an authenticator app; (c) a PIN; (d) a hardware security key. 3. Why does AAL3 require phishing resistance while AAL2 does not?
Answers
- No. Both a password and a security-question answer are something you know — the same factor category used twice. A single phishing page or breach can capture both, so there is no independence and thus no genuine second factor. 2. (a) inherence / something you are; (b) possession / something you have (the code proves you hold the seeded app); (c) knowledge / something you know; (d) possession / something you have. 3. AAL3 is meant for the highest-value access, where the residual risk that a user could be socially engineered into revealing or relaying a credential is unacceptable; only authenticators that cannot be phished or relayed (hardware-bound cryptographic keys) clear that bar.
16.2 Passwords: storage, policy, and why they won't die
Here is the uncomfortable place to start: despite everything that follows, you will be defending passwords for years. They are free, universally understood, require no hardware, and are embedded in tens of thousands of legacy systems that will not be re-architected on your timeline. The professional stance is not contempt for passwords but competence with them — storing them so a breach of your database is survivable, governing them so they are as strong as a human will tolerate, and steadily moving the highest-value logins off them. Let us take storage first, because storage is where the catastrophic, headline-making failures happen.
Storing passwords: never store the password
The first rule of password storage is that you do not store passwords. When a user sets a password, you store a value computed from it by a one-way function — a hash (you met hashing in Chapter 4) — so that the stored value cannot be reversed back into the password, yet can be recomputed at login time to check a match. The reason is brutal and simple: databases get breached. If you stored plaintext passwords and your database leaked, every account is compromised the instant the dump is published and, because people reuse passwords, so are those users' accounts on other services. Storing a hash means that even with the whole database in hand, an attacker still has to work to recover each password.
But naive hashing is not enough, and the history of breaches is a graveyard of teams who learned this the hard way. Two attacks shape correct password storage:
- Precomputation (rainbow tables). Fast, general-purpose hashes like a bare SHA-256 can be precomputed: an attacker builds (or downloads) a giant table mapping common passwords to their hashes, then looks up your leaked hashes instantly. The defense is a salt — a unique, random value stored alongside each user's hash and mixed into the hash input. With a per-user salt, two users who chose the same password get different stored hashes, and precomputed tables are useless because the attacker would need a separate table per salt. Salts are not secret; their job is uniqueness, not concealment.
- Brute-force speed. Even salted, a fast hash can be attacked by trying billions of candidate passwords per second on a GPU. The defense is to use a hash that is deliberately slow and resource-intensive — a password hashing function (also called a password-based key derivation function) engineered so that each guess costs meaningful time and memory. The leading choices are bcrypt, scrypt, and Argon2 (specifically Argon2id), with PBKDF2 acceptable where the others are unavailable. Argon2id and scrypt are additionally memory-hard: they require a large block of RAM per guess, which blunts the attacker's biggest advantage (cheap, massively parallel GPU and ASIC hardware that has lots of compute but limited fast memory).
So the recipe is: a salted, memory-hard, deliberately slow hash. Here is the concept in code, using Argon2id (the current first choice) at an illustrative cost setting. As with all bluekit code, this is illustrative and hand-traced — never executed during authoring.
# Illustrative password storage with Argon2id (via the argon2-cffi library).
# Real cost parameters should be tuned to your hardware; these are illustrative.
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=64 * 1024, parallelism=4) # 64 MiB
# When a user sets a password, store ONLY the encoded hash string:
stored = ph.hash("correct horse battery staple")
# stored looks like (FAKE, truncated):
# $argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ...$Q2hl...EXAMPLE
# The salt is generated automatically and embedded in the string.
def login_ok(stored_hash: str, attempt: str) -> bool:
"""Verify a login attempt against the stored Argon2id hash."""
try:
ph.verify(stored_hash, attempt) # recomputes with the embedded salt+params
return True
except Exception: # VerifyMismatchError, etc.
return False
# Expected behavior (hand-traced, not executed):
# login_ok(stored, "correct horse battery staple") -> True
# login_ok(stored, "Tr0ub4dor&3") -> False
Notice three things. The salt is generated and embedded automatically — you never manage it by hand. The cost parameters (time_cost, memory_cost, parallelism) are stored in the hash string, so you can raise them over time as hardware improves and old hashes still verify. And verification recomputes the hash; it never "decrypts" anything, because there is nothing to decrypt — the function is one-way by design.
⚠️ Common Pitfall: Hashing with a fast, general-purpose algorithm and calling it secure. A single unsalted SHA-256 or MD5 of a password is not password storage; it is an invitation to a rainbow table. If you ever see
MD5(password),SHA1(password), or evenSHA256(password)with no salt and no work factor in a code review, that is a critical finding. Worse still — and still distressingly common in legacy systems — is storing passwords reversibly "encrypted" or in plaintext, which means a single database breach or rogue administrator exposes every credential at once. The §16.6 worked example and several exercises train you to spot these.🛡️ Defender's Lens: From the attacker's side, your choice of hash sets the economics of a breach. After a database leak, an attacker runs the leaked hashes against wordlists and breach corpora. Against unsalted MD5, they crack millions of passwords in minutes and your incident is total. Against per-user-salted Argon2id at a real work factor, they may crack a handful of the weakest passwords slowly and give up on the rest — which buys you the time to force a reset before most accounts are usable. The hash function does not prevent the breach; it determines whether the breach is a catastrophe or a manageable incident. That is defense in depth applied to the database itself: assume it leaks, and make the leak survivable.
Password policy: what NIST actually says now
Now the policy question — the rules you impose on the passwords users choose — and here you must unlearn what you were taught. For two decades, "strong password policy" meant: at least 8 characters, mixed upper/lower/digit/symbol, changed every 90 days, no repeats. NIST SP 800-63B reversed much of this, because field evidence showed the old rules backfired. Composition rules pushed users toward predictable patterns (Password1!, then Password2!); forced periodic expiration pushed them toward minimal, guessable changes (Spring2024! → Summer2024!) and writing passwords down. Modern guidance, distilled:
- Length over complexity. Allow long passphrases (support at least 64 characters) and do not impose composition rules (no mandatory "must contain a symbol"). A long, memorable phrase has far more entropy — unpredictability, which we quantify below — than a short string mangled to satisfy a checkbox.
- No mandatory periodic expiration. Do not force routine password changes on a calendar. Force a change only on evidence of compromise. Routine expiration trains weak, incremental passwords and yields little benefit.
- Screen against known-bad and breached passwords. This is the high-value rule that replaces the old ones: when a user sets a password, reject it if it appears in a list of common or previously-breached passwords. A "P@ssw0rd1" that satisfies every composition rule is worthless because it is on every attacker's wordlist. We build exactly this check, privacy-preservingly, in the Project Checkpoint.
- Allow paste and password managers. Do not block pasting into password fields; it punishes the users doing the right thing with a password manager. Encourage managers — they make long, unique, random passwords per site practical, which kills password reuse (the engine of credential stuffing in §16.6).
- Rate-limit and lock out attackers, not users. Throttle and lock based on failed attempts against an account or from a source, not by forcing the legitimate user through pointless friction.
To make "length beats complexity" concrete, we need to quantify unpredictability. The entropy of a password (in bits) measures how many guesses an attacker must make on average; each bit doubles the work. For a password drawn randomly from an alphabet of $N$ symbols with length $L$, the entropy is
$$H = L \times \log_2 N \quad \text{bits.}$$
Compare two passwords. A random 8-character password over the 94 printable ASCII symbols has $H = 8 \times \log_2 94 \approx 8 \times 6.55 \approx 52$ bits. A passphrase of 5 words chosen randomly from a 7,776-word list (the classic "diceware" approach) has $H = 5 \times \log_2 7776 \approx 5 \times 12.92 \approx 65$ bits — meaningfully stronger, and far easier for a human to remember and type. This is the arithmetic behind "length over complexity," and it is why the famous comparison holds: a memorable four- or five-word phrase beats a short line of mangled symbols.
⚠️ Common Pitfall — the entropy caveat: That formula assumes the password (or each word) is chosen uniformly at random. A human-chosen password is not random — "Password1!" has far less effective entropy than its 10 characters and 94-symbol alphabet would suggest, because humans cluster around a tiny, predictable subset. This is precisely why screening against breached/common lists matters more than any composition rule: the rule reasons about the alphabet; the attacker reasons about what people actually pick. Never tell a user their human-chosen "Tr0ub4dor&3" has 50+ bits of real strength — it doesn't.
Why passwords won't die
So why, knowing all this, will passwords persist? Because their replacements each demand something passwords don't. Hardware keys cost money and must be provisioned and recovered. Biometrics require capable hardware and careful handling. Passkeys (which we will champion) need platform and ecosystem support that, while finally arriving, is not yet universal across every legacy enterprise app, every B2B partner portal, and every device a 2.5-million-customer bank must serve. Passwords are the universal solvent of authentication: they work on everything, for everyone, with no rollout. The realistic professional plan, therefore, is not "ban passwords" but "store them correctly, govern them sanely, always pair the important ones with a second factor, and migrate the highest-value logins to phishing-resistant methods first." That is the plan Meridian will adopt. With storage and policy handled, the next question is what to add to a password — and how much it actually helps.
🔄 Check Your Understanding: 1. Why is a per-user salt necessary even when you already use a slow, memory-hard hash like Argon2id? 2. NIST now discourages mandatory 90-day password expiration. Give the behavioral reason, and state the one condition under which you should force a password change. 3. A random 12-character password over 94 symbols — roughly how many bits of entropy, and is a 6-word diceware passphrase stronger? (Use $\log_2 94 \approx 6.55$, $\log_2 7776 \approx 12.92$.)
Answers
- The salt defeats precomputation (rainbow tables) and ensures identical passwords hash to different stored values; the slow hash defeats brute-force speed. They counter different attacks — you need both. Without a salt, an attacker could precompute one table and crack every account that shares a common password regardless of how slow the hash is per guess. 2. Forced periodic expiration trains users to pick weak, incrementally-changing passwords (Spring → Summer) and to write them down; you should force a change only on evidence of compromise (e.g., the password appears in a breach, or the account shows signs of takeover). 3. $12 \times 6.55 \approx 79$ bits for the random password; $6 \times 12.92 \approx 78$ bits for the passphrase — roughly equal, and the passphrase is far easier to remember and type. Both assume random selection.
16.3 MFA and its failure modes: push fatigue and SIM swap
Adding a second factor of a different category is the most cost-effective security improvement most organizations can make. A stolen password alone stops being a breach. But "MFA" spans a wide quality range, and the rest of this chapter is largely about that range — because attackers, faced with MFA everywhere, have learned to defeat the weaker forms, and a defender who thinks "we have MFA" is finished is in for a bad incident. Let us walk the common methods from weakest to strongest, and at each step show how the attacker beats it.
SMS one-time passcodes — the weakest MFA, still better than none
The most common second factor is a one-time passcode (OTP) texted to the user's phone. It is wildly better than no second factor, and you should not sneer at it for a consumer base that has nothing else. But it has two serious, well-documented weaknesses a defender must understand:
- SIM swap. The "possession" being proven is possession of a phone number, and phone numbers can be stolen. In a SIM swap attack, the adversary social-engineers (or bribes) a mobile carrier into porting the victim's number to a SIM the attacker controls — then the OTP texts arrive on the attacker's device. This has drained bank and cryptocurrency accounts repeatedly. The victim often notices only when their own phone loses service.
- OTP phishing / relay. An SMS (or app-generated) code is a secret the user can read and type, which means the user can be tricked into typing it into a fake page. A modern phishing kit shows the victim a fake login, captures username, password, and the OTP, and relays all three to the real site in real time — an adversary-in-the-middle attack. The code's brief validity window doesn't help when the relay is instantaneous.
📟 War Story: A constructed but representative incident. An attacker targets a Meridian wealth-management client by first calling the client's mobile carrier, impersonating the customer with details scraped from social media, and convincing a support agent to "replace a lost SIM." Minutes later the client's number is on the attacker's phone. The attacker triggers a password reset on the brokerage portal; the reset code arrives — to the attacker. The only reason the loss is contained is that the transfer of funds required a separate, phishing-resistant approval that the SIM swap did not grant. The lesson is the one this whole chapter circles: the strength of an authentication method is the strength of the thing it actually proves you possess, and "possession of a phone number" is weaker than it looks.
Authenticator apps (TOTP) — better, still phishable
A step up is the TOTP — Time-based One-Time Password — generated by an authenticator app (Google Authenticator, Authy, Microsoft Authenticator, and many others). At setup the server and the app share a secret seed (often via a QR code); thereafter both compute a 6-digit code from that seed and the current time, rolling over every 30 seconds, using the standard algorithm of RFC 6238. TOTP is better than SMS in one important way: there is no phone number to hijack, so SIM swap doesn't apply, and the seed lives on the device rather than traveling over the cellular network.
But TOTP shares SMS's fatal property for high-value access: the code is a secret the user reads and types, so it can be phished and relayed by exactly the adversary-in-the-middle attack above. The attacker's fake page asks for the 6 digits; the user, mid-login and unsuspecting, supplies them; the attacker replays them to the real site inside the 30-second window. TOTP defeats credential stuffing and SIM swap but not real-time phishing. Keep that distinction sharp — it is the line between "phishable MFA" and the phishing-resistant MFA of §16.4.
Push notifications and the attack that exploits being human: push fatigue
The most user-friendly second factor is the push approval: the app pops a notification — "Approve sign-in?" — and the user taps Approve. No code to type, almost frictionless. And that frictionlessness is the vulnerability. In a push fatigue attack (also called MFA fatigue or push bombing), an attacker who already has the user's password triggers login after login, firing a stream of push prompts at the victim's phone — sometimes dozens, sometimes at 3 a.m. — until the exhausted, confused, or annoyed user finally taps Approve just to make it stop. This is not hypothetical; it was a key step in several high-profile 2022 breaches of large technology companies. The attacker never broke the cryptography; they exploited a tired human and a prompt that asks for a reflexive yes.
PUSH-FATIGUE ATTACK (attacker already has the password)
Attacker Victim's phone Identity provider
│ submits password (stolen) │ │
├───────────────────────────────────────────────────────────────► │
│ │ "Approve sign-in?" #1 │
│ │ ◄───────────────────────────┤
│ submits again ... │ "Approve sign-in?" #2 │
├───────────────────────────────────────────────────────────────► │
│ ... and again, and again ... │ "Approve sign-in?" #3..n │
│ │ ◄───────────────────────────┤
│ │ (3 a.m., 14th prompt) │
│ │ user taps ✗ APPROVE │
│ ├───────────────────────────► │
│ ◄═══════════════ attacker is now signed in ════════════════════ │
Figure 16.1 — A push-fatigue attack. Note that every cryptographic check "passed"; the attacker defeated the human, not the protocol. The defenses below remove the reflexive single tap.
How do you defend the weaker MFA methods you cannot yet retire? You harden the prompt and watch the telemetry:
- Number matching. Instead of a bare Approve/Deny, the login screen shows a two-digit number the user must type into the app. A reflexive tap no longer works; the user must look at the genuine login screen to learn the number — which an attacker firing blind prompts cannot supply. This single change neutralizes most push-fatigue attacks and is now widely enabled by default.
- Additional context and rate-limiting. Show the app the location, application, and IP of the request, so "sign-in from another country to an app I'm not using" is an obvious deny. Rate-limit prompts per account and lock the account after a burst of denials — a flood of prompts is itself an indicator of compromise (§16.6).
- Move high-value access off phishable factors entirely. Number matching and context raise the bar; they do not make push phishing-resistant. For administrators and money movement, the answer is not a better prompt but a different category of authenticator, which is the subject of the next section.
🛡️ Defender's Lens: Every weak-MFA attack has a telemetry signature you can detect. A burst of MFA prompts to one user in a short window, especially followed by an approval from an unusual IP, is a near-textbook push-fatigue indicator. MFA challenges for accounts whose password was recently seen in a breach feed suggests stuffing-plus-MFA-defeat. A successful login geographically impossible relative to the user's last login ("impossible travel") suggests a relayed session or stolen token. You will build several of these detections in §16.6. The recurring lesson of Part V applies here early: the attack you cannot prevent at the door, you can often catch in the logs.
🔄 Check Your Understanding: 1. Rank SMS OTP, TOTP, and push-with-number-matching from most to least phishable, and name the single attack TOTP resists that SMS does not. 2. Explain in one sentence why "number matching" defeats a push-fatigue attack. 3. An attacker has a user's correct password and is firing push prompts every few seconds. What is the attacker's goal, and what two controls most directly stop it?
Answers
- By phishability (most to least): SMS OTP and TOTP are both readily phishable via real-time relay (roughly equal), and push-with-number-matching is the least phishable of the three because there is no code to type and the required number can't be supplied by a blind attacker — though none are truly phishing-resistant. TOTP resists SIM swap, which SMS does not, because there is no phone number to hijack. 2. Number matching requires the user to read a number off the genuine login screen and type it into the app, so a reflexive single tap — and an attacker who can't see the real screen — both fail. 3. The attacker's goal is to get the victim to tap Approve out of fatigue/annoyance (push fatigue). The two most direct controls: number matching (removes the reflexive tap) and prompt rate-limiting plus account lockout on a burst of denials (stops the flood and flags it as an incident).
16.4 Phishing-resistant MFA: FIDO2, WebAuthn, and passkeys
We have arrived at the heart of the chapter — the technology that actually saved Meridian, and the only kind of MFA NIST will accept at AAL3. Everything so far shares one weakness: the proof of the second factor is something the user can be tricked into handing over — a code to read, a tap to make. Phishing-resistant MFA removes that weakness at the protocol level. The user has nothing to type or relay; the proof is a cryptographic signature produced by a private key that never leaves the authenticator and that is cryptographically bound to the website asking for it. There is no secret for a fake site to capture, and a signature meant for one site is worthless at another. This is not "stronger MFA"; it is a different security property, and the difference is the whole game.
The standards are a family with overlapping names worth untangling:
- FIDO2 is the umbrella set of standards (from the FIDO Alliance and the W3C) for passwordless, phishing-resistant authentication using public-key cryptography.
- WebAuthn (Web Authentication) is the W3C browser API — the part that lets a website ask the browser and an authenticator to create and use a key pair. When a web app "supports security keys" or "supports passkeys," it is using WebAuthn.
- CTAP (Client to Authenticator Protocol) is the companion standard for how the browser/OS talks to an external authenticator (a USB/NFC/Bluetooth security key).
- A passkey is a FIDO2 credential — a private key bound to one website — used in place of a password. Some passkeys live on a dedicated hardware security key (a small device you tap or plug in); others are synced passkeys stored in a platform credential manager (Apple iCloud Keychain, Google Password Manager, Windows Hello / a password manager) and backed up across the user's devices. Both are phishing-resistant; they differ in portability and recovery, which we'll weigh shortly.
How it works: the challenge-response that cannot be relayed
The mechanism is public-key cryptography (Chapter 4) applied to login. It has two phases.
Registration (once per site). When you enroll, the authenticator generates a fresh key pair for that specific website. The private key stays locked inside the authenticator (often in a secure hardware element); the public key is sent to the website and stored with your account. The website never sees, and never stores, anything secret — only a public key, which is useless to a thief.
Authentication (every login). To log you in, the website sends a random challenge. The authenticator signs that challenge with the private key — but crucially, it signs it together with the origin (the website's actual domain, e.g., meridianbank.example), and the browser, not the user, supplies that origin from the address bar. The website verifies the signature with the stored public key. If the signature is valid and the origin matches, you are in.
Here is the flow as a diagram, then the reason it defeats phishing:
FIDO2 / WebAuthn AUTHENTICATION (phishing-resistant)
User + Authenticator Browser Website (Relying Party)
(holds PRIVATE key) (knows true origin) (holds PUBLIC key only)
│ │ │
│ │ 1. "Log in" request │
│ ├─────────────────────────►│
│ │ 2. random CHALLENGE │
│ │ ◄─────────────────────────┤
│ 3. browser passes │ │
│ challenge + ORIGIN │ │
│ ◄────────────────────────┤ (origin = address bar) │
│ 4. user gesture │ │
│ (tap / biometric │ │
│ unlocks the key) │ │
│ 5. SIGN(challenge, origin) with private key │
├─────────────────────────►│ │
│ │ 6. signed assertion │
│ ├─────────────────────────►│
│ │ 7. verify signature │
│ │ with public key & │
│ │ check origin match │
│ ◄══════════════════ 8. access granted ══════════════┤
Figure 16.2 — The WebAuthn challenge-response. The private key never leaves the authenticator; the signature is bound to the origin the browser supplies. There is no code, no shared secret, and nothing for a fake site to capture or replay.
Now the payoff — why a phishing page cannot defeat this. Suppose the attacker stands up meridian-bank-login.example, a perfect visual copy, and lures the loan officer to it. Three independent things break for the attacker:
- There is no secret to capture. The user types no code and approves no bare prompt; the authentication is a signature the user cannot read, write down, or be tricked into reciting. Even a flawless fake page captures nothing reusable.
- The origin won't match. If the attacker's page somehow forwards the challenge to a real authenticator, the browser binds the signature to the attacker's origin (
meridian-bank-login.example), because that is what is in the address bar. When the attacker relays that signature to the real site, the real site checks the origin embedded in the signed data against its own domain — they differ — and rejects it. The signature meant for the fake site is worthless at the real one. - The key is bound to the real domain in the first place. The authenticator will only use the private key it registered for
meridianbank.example; it has no key for the look-alike domain, so for a genuinely different domain there is no credential to offer at all.
This is what "phishing-resistant" means, concretely: not that users are trained well enough to spot the fake (they aren't, and the loan officer didn't), but that the protocol refuses to produce a usable credential for the wrong site. It also defeats credential stuffing (no reusable secret), SIM swap (no phone number), and adversary-in-the-middle relay (origin binding). The residual risks are narrower and more manageable — losing the authenticator, weak account-recovery paths, and (for synced passkeys) the security of the cloud account that holds them — and we address them in §16.7 and §16.6.
🔗 Connection: This is the same public-key machinery from Chapter 4 — a private key that signs, a public key that verifies — now binding a login to a domain instead of binding a message to a signer. If §4's digital signatures felt abstract, this is where they pay off: a FIDO2 login is a digital signature over a server-supplied challenge plus the origin. And it is the authentication substrate that makes the strict, continuous verification of zero trust (Chapter 32) trustworthy — there is no point re-checking identity continuously if the check itself can be phished.
🧩 Try It in the Lab: On a personal account that supports it (most major email, social, and developer platforms now do), enroll a passkey or a hardware security key as a second factor — in your own account only. Then watch what changes: the login asks for a tap or your device biometric instead of a code. If you have two browsers, try registering on one and note that the credential is bound to the site, not portable by copy-paste. This five-minute exercise makes §16.4 tangible: you will feel the absence of a code to type, which is exactly the absence an attacker cannot exploit.
⚠️ Common Pitfall: Treating "passwordless" and "phishing-resistant" as synonyms. They are not. A magic-link emailed to you is passwordless but phishable (an attacker can capture or relay the link). A push approval is "MFA" but phishable via fatigue. FIDO2/WebAuthn is phishing-resistant because of origin-bound public-key cryptography, not because it lacks a password. When evaluating any "passwordless" vendor claim, ask the one question that matters: is the credential cryptographically bound to the origin, with no secret the user can relay? If not, it is not phishing-resistant, whatever the marketing says.
Synced passkeys vs. hardware keys: a real tradeoff
Both forms are phishing-resistant, so the choice between them is about portability, recovery, and assurance:
| Hardware security key (device-bound) | Synced passkey (platform/cloud) | |
|---|---|---|
| Where the private key lives | On the physical key only; cannot be exported | In a cloud credential manager, synced across the user's devices |
| Recovery if lost | Need a registered backup key, or fall back to recovery | Restored from the cloud account on a new device |
| Best for | Highest-assurance accounts (admins, money movement) — clears AAL3 | Broad consumer/workforce rollout where usability and self-recovery matter |
| Key caution | Provisioning and backup-key logistics; cost | Only as strong as the cloud account securing the sync — protect that with phishing-resistant MFA too |
The practical pattern, which Meridian will adopt, is both: synced passkeys to move the broad population off passwords with minimal friction, and device-bound hardware keys for the small set of highest-value identities where you want a private key that physically cannot leave the device.
🔄 Check Your Understanding: 1. In one sentence each, give the two independent reasons a relayed FIDO2 signature from a phishing site fails at the real site. 2. A vendor advertises "passwordless login via emailed magic links" as phishing-resistant. Are they correct? Why or why not? 3. What is the principal residual risk of synced passkeys that device-bound hardware keys largely avoid, and how do you mitigate it?
Answers
- (i) There is no reusable secret — the user produces a signature, not a code, so nothing capturable is exposed. (ii) Origin binding — the browser binds the signature to the phishing site's origin, which the real site rejects because it differs from its own domain (and the authenticator has no key registered for the look-alike domain in the first place). 2. No. Magic links are passwordless but phishable: an attacker can intercept or relay the link in real time. "Passwordless" is not the same property as "phishing-resistant," which requires origin-bound public-key cryptography with no relayable secret. 3. Synced passkeys are only as secure as the cloud account that holds and syncs them; if that account is compromised, the passkeys can be misused. Mitigate by protecting the cloud/credential-manager account itself with phishing-resistant MFA and strong recovery, and by reserving device-bound hardware keys for the highest-assurance accounts.
16.5 Biometrics
The third factor — something you are — deserves careful, slightly skeptical treatment, because biometrics are simultaneously excellent and routinely misunderstood. Biometrics authenticate using a measured physical or behavioral characteristic: fingerprint, face geometry, iris pattern, voice, and so on. Their genuine strengths are real: a biometric is bound to the person, hard to forget, hard to lend, and frictionless to present. Unlocking your phone with your face is the most-used authentication on Earth precisely because it is nearly invisible.
But biometrics carry properties no password or key shares, and a defender must keep them front of mind:
- You cannot revoke a biometric. If a password leaks, you change it. If a key is lost, you revoke it and issue another. But you cannot get a new fingerprint or a new face. A biometric is permanent, which means a compromise of the stored biometric data is permanent too. This is the single most important reason biometric templates must be stored as protected, non-reversible representations on the device — never as raw images in a central database that, once breached, exposes an unchangeable trait forever.
- Biometrics are probabilistic, not exact. A password match is binary. A biometric match is a similarity score compared against a threshold, which produces two error rates you must understand: the false acceptance rate (FAR) — how often the system wrongly accepts an impostor — and the false rejection rate (FRR) — how often it wrongly rejects the legitimate user. These trade off against each other: tighten the threshold to lower FAR (more secure) and you raise FRR (more user frustration), and vice versa. The threshold where the two rates are equal is the crossover error rate (CER) — a single number useful for comparing systems (lower CER is better).
- On a network, a biometric is only as trustworthy as the device that measured it. This is the crucial architectural point. A fingerprint sensor on a phone proves a finger touched that phone; it does not, by itself, prove anything to a remote server, because the server cannot see the finger — it sees whatever the device says about the finger, which an attacker who controls the device or the channel could forge. Biometrics also face presentation attacks (spoofing) — a photograph, a mask, a lifted fingerprint — which is why serious systems add liveness detection.
These properties dictate how biometrics should and should not be used. The right pattern is the one your phone already uses, and it is worth seeing why it is well-designed: the biometric is a local gesture that unlocks a cryptographic key, and it is the key — not the biometric — that authenticates to the server. When you authorize a passkey with your fingerprint, the fingerprint never leaves the phone; it merely unlocks the FIDO2 private key, which then performs the origin-bound signature of §16.4. The server receives a cryptographic proof, not a biometric. This composition gives you the convenience of inherence and the network security of possession, while keeping the irrevocable biometric data on-device where its compromise is contained.
⚠️ Common Pitfall: Treating a biometric as a network secret — sending fingerprint or face data to a server to authenticate, or storing raw biometric images centrally. This is dangerous twice over: it creates a high-value, irrevocable database that is catastrophic if breached, and it misunderstands the trust model, because the server is trusting the device's report of the biometric, not the biometric itself. The correct architecture keeps the biometric local as an unlock gesture for an on-device key. When you see a system that uploads raw biometrics to authenticate, flag it.
⚖️ Authorization & Ethics: Biometric data is among the most sensitive data you can collect, and it is increasingly regulated — biometric-privacy laws (such as Illinois's BIPA in the U.S. and biometric provisions in GDPR) impose real obligations on collection, consent, storage, and retention. Beyond compliance, there is an ethics of permanence: you are asking a person to entrust you with a trait they can never change. The defensive and the ethical answers coincide — keep biometrics on the user's device, never in your database; use them to unlock local keys, not as credentials sent to you. The architecture that is most secure is also the one that respects the irrevocability of what you are handling.
🔄 Check Your Understanding: 1. Why is the irrevocability of biometrics a security problem, and what storage architecture mitigates it? 2. Define FAR and FRR and explain how they trade off. What does the CER let you do? 3. Your phone's fingerprint unlock authorizes a passkey login to a bank. What, exactly, does the bank's server receive — the fingerprint, or something else?
Answers
- You cannot reissue a fingerprint or face after a compromise, so a breach of stored biometric data is permanent. The mitigation is to store only protected, non-reversible templates on the user's device and use the biometric locally to unlock an on-device cryptographic key, rather than storing raw biometrics centrally. 2. FAR (false acceptance rate) is how often an impostor is wrongly accepted; FRR (false rejection rate) is how often the legitimate user is wrongly rejected. Lowering one by adjusting the match threshold raises the other. The CER (crossover error rate), where FAR equals FRR, gives a single comparison number across systems (lower is better). 3. The server receives a cryptographic signature from the FIDO2/passkey private key (an origin-bound assertion), not the fingerprint. The fingerprint only unlocked the key locally and never leaves the device.
16.6 Defending against credential attacks
Authentication is the most attacked control because credentials are the most valuable loot. We now turn fully to the defender's seat: the three attacks every authentication system faces, what each looks like in your telemetry, and the controls that stop each. The unifying theme — true to this book — is that you cannot prevent every attempt, but you can make attempts expensive, throttle them, and see them in the logs.
Credential stuffing
Credential stuffing is the automated replay of username-and-password pairs stolen from one breach against many other services, exploiting the fact that people reuse passwords. The attacker does not guess; they know a valid pair existed somewhere, and they bet the user reused it. With billions of leaked pairs circulating, stuffing is high-volume and cheap, and it is the single most common attack against consumer login pages. Its signature: many distinct usernames attempted, often each tried once or twice (the attacker has a specific password per account, not a guess list), frequently from a rotating set of IPs (botnets and proxies) to evade per-IP limits, with an unusually low success rate but enormous volume.
Consider this illustrative authentication log for Meridian's online-banking portal (source IPs in documentation range 203.0.113.0/24; all times UTC):
03:14:01 user=jlopez@ex src=203.0.113.10 result=SUCCESS
03:14:01 user=msingh@ex src=203.0.113.55 result=FAIL reason=bad_password
03:14:02 user=awong@ex src=203.0.113.91 result=FAIL reason=bad_password
03:14:02 user=dpatel@ex src=203.0.113.12 result=SUCCESS
03:14:03 user=kobrien@ex src=203.0.113.77 result=FAIL reason=bad_password
03:14:03 user=rgupta@ex src=203.0.113.41 result=SUCCESS
03:14:04 user=bcruz@ex src=203.0.113.30 result=FAIL reason=bad_password
... thousands of DISTINCT users, ~2% SUCCESS, IPs spread across /24, in minutes ...
The tell is the shape: thousands of different users, each tried a handful of times from spread-out IPs, with a small but nonzero success rate — exactly what you'd expect if an attacker is replaying a breach corpus and a couple percent of your users reused a leaked password. Contrast this with the next attack, whose shape is different.
Detecting and defending credential stuffing:
- Screen passwords against breach corpora at set-time (the §16.2 control and our Project Checkpoint) so reused-from-a-breach passwords can't be set in the first place.
- Rate-limit and add bot defenses — per-IP and global throttling, CAPTCHAs on anomalous patterns, and device/behavioral signals to distinguish humans from automation.
- Detect the shape, not just the volume: alert on a spike in distinct usernames per source or a sudden drop in your portal's overall login success rate (a stuffing run drags it down). These are SIEM detections you'll formalize in Part V.
- The decisive control is MFA — and ideally phishing-resistant MFA — because even a correct reused password yields nothing without the second factor. Stuffing's entire premise (a valid password = access) collapses under MFA.
Password spraying
Password spraying inverts the loop: instead of many passwords against one account, the attacker tries a few very common passwords against many accounts, deliberately staying under the lockout threshold for each account. It is the attacker's answer to account lockout — by spraying Winter2024! or CompanyName1 across the whole user directory, hitting each account just once or twice, they avoid tripping per-account lockouts while still landing on the unlucky users who chose a weak, common password. Its signature is the mirror image of stuffing: one (or a few) password values, attempted against many distinct accounts, low and slow, often from one or a few sources, timed to dodge lockout windows.
PASSWORD SPRAYING — same password across many users, low-and-slow
09:00:00 user=aadams@ex pw_try="Spring2024!" result=FAIL
09:02:00 user=bbaker@ex pw_try="Spring2024!" result=FAIL
09:04:00 user=cclark@ex pw_try="Spring2024!" result=SUCCESS ◄ one weak password
09:06:00 user=ddavis@ex pw_try="Spring2024!" result=FAIL
... one password value, hundreds of users, spaced minutes apart to evade lockout ...
Detecting and defending password spraying:
- Screen against common passwords so the values attackers spray (
Season+Year!,Companyname1) cannot be set by your users — this is the most effective single control, because spraying depends on someone having a sprayable password. - Detect the inverse shape: alert when many distinct accounts see a failed login with the same password hash or within a short window from one source — a per-account lockout will miss this because each account is only touched once, so you need cross-account correlation. This is a classic detection that simple lockout cannot provide.
- Smart lockout / throttling that considers source and behavior, not just per-account counts, plus MFA so a single sprayed hit still can't complete a login.
⚠️ Common Pitfall: Relying on account lockout as your defense against guessing. Lockout stops one account, many passwords (stuffing-style hammering of a single login), but password spraying is designed to slip under it by touching each account only once. Worse, aggressive lockout creates a denial-of-service: an attacker can deliberately lock out legitimate users by submitting bad passwords for their accounts. The modern answer is smart throttling (source- and behavior-aware), breach-password screening, and MFA — not a blunt "5 strikes and the account freezes."
MFA-defeat attacks and account takeover
Once MFA is widespread, attackers pivot to defeating it — the push-fatigue, SIM-swap, and real-time OTP-relay attacks of §16.3. The end goal of all of these, and of stuffing and spraying, is the same: account takeover (ATO), unauthorized control of a legitimate account. Detection for the MFA-defeat layer focuses on the anomalies a successful relay or fatigue attack leaves behind:
- Impossible travel: a successful authentication from a location geographically incompatible with the user's previous login minutes earlier — a strong indicator of a stolen session token or a relay. (One caveat: VPNs and corporate egress points cause false positives; tune accordingly.)
- MFA prompt bursts: a flood of MFA challenges to one user in a short window (the push-fatigue signature from Figure 16.1), especially ending in an approval from a new device or IP.
- New-device / new-location enrollment of an authenticator, or a password reset followed immediately by an MFA change — a common ATO finishing move where the attacker locks the real owner out.
- Session and token anomalies — reuse of a session token from a new IP can indicate the attacker stole the post-authentication session rather than the credential, which is why phishing-resistant and short-lived, bound sessions (Chapter 32) matter.
Here is the worked detection logic for the highest-value cross-account case — spraying — that simple lockout cannot catch, as a small, hand-traced function (the kind you'll formalize into SIEM rules in Part V):
# Illustrative spray detector: alert when ONE password is tried across MANY users.
# Real systems work on hashed/redacted values; this is teaching logic, not executed.
from collections import defaultdict
def spray_alerts(events, user_threshold=10, window_seconds=600):
"""events: list of (ts, user, pw_fingerprint, result).
Alert on a pw_fingerprint tried against >= user_threshold distinct users
within window_seconds. (pw_fingerprint = a salted hash, never the password.)"""
by_pw = defaultdict(list)
for ts, user, pw_fp, result in sorted(events):
by_pw[pw_fp].append((ts, user))
alerts = []
for pw_fp, hits in by_pw.items():
start = hits[0][0]
users = {u for ts, u in hits if ts - start <= window_seconds}
if len(users) >= user_threshold:
alerts.append((pw_fp, len(users)))
return alerts
# Hand-traced: with one pw_fingerprint "X" tried against users a..k (11 distinct)
# inside 600s, and threshold 10:
# spray_alerts(events) -> [("X", 11)]
# Expected output:
# [('X', 11)]
The point is the cross-account correlation — grouping by the password value to reveal a pattern that per-account lockout, which only ever looks at one account at a time, is structurally blind to.
🛡️ Defender's Lens: A useful mental model for the whole section is shape. Credential stuffing is wide in usernames, narrow in passwords-per-account (each user tried with their one leaked password). Password spraying is wide in usernames, narrow in password values (one common password across everyone). Brute force is narrow in usernames, wide in passwords (one account, many guesses). MFA fatigue is narrow in everything but loud in prompts (one user, many push challenges). Once you can name the shape from the log, the detection writes itself and the right control follows: breach-screening and MFA for stuffing, common-password screening and cross-account correlation for spraying, number-matching and rate-limits for fatigue.
🔄 Check Your Understanding: 1. Distinguish credential stuffing from password spraying by their shape in an authentication log. Which one does per-account lockout fail to catch, and why? 2. Why does enabling MFA neutralize the core premise of credential stuffing even when the stolen password is correct? 3. You see a single successful login from Lagos at 14:02 for a user whose previous login was from Chicago at 13:58. Name the detection and one benign cause that could trigger a false positive.
Answers
- Credential stuffing is many distinct usernames, each tried with one (leaked) password, often from many IPs with a low success rate. Password spraying is one (or a few) common passwords tried against many distinct accounts, low-and-slow to dodge lockout. Per-account lockout fails to catch spraying, because each account is touched only once or twice — under the threshold — so detection requires cross-account correlation by password value. 2. Stuffing's premise is "a valid password equals access." MFA breaks that: a correct password from the breach corpus still lacks the second, different-category factor, so the login does not complete. 3. The detection is impossible travel (geographically incompatible successful logins minutes apart). A benign cause: the user is on a VPN or corporate egress that makes them appear in another region — so the rule must be tuned for known VPN/egress IPs to limit false positives.
16.7 Project Checkpoint
This chapter's increment advances both Meridian's security program (an authentication standard) and the bluekit toolkit (authn.py).
Program increment — Meridian's authentication standard. The phishing near-miss that opened this book proved a control choice right; this chapter turns that single win into policy. Working from the AAL model (§16.1) and the credential-attack analysis (§16.6), the team — Sam Whitfield drafting, Elena Vasquez mapping to GLBA/PCI-DSS/FFIEC, Dana Okafor sponsoring to the board — writes a one- to two-page authentication standard. Its spine is AAL-by-asset-tier:
- Tier 0 — public/low-value (marketing site, read-only info): AAL1; password permitted; breach-password screening on.
- Tier 1 — workforce productivity (email, M365, general apps): AAL2 minimum; password + MFA, number matching and context required on any push; phasing toward passkeys.
- Tier 2 — customer online/mobile banking: AAL2, moving customers to passkeys; SMS OTP retained only as a fallback while phasing out, never as the sole high-value factor; smart lockout and breach-screening on.
- Tier 3 — money movement, privileged/admin access, and the systems that touch the core or the cardholder data environment: AAL3 — phishing-resistant FIDO2/hardware-key MFA required, no exceptions. This is the tier the loan-officer incident lives in, and the standard now mandates the very control that saved Meridian for everyone with comparable access.
Storage and policy clauses round it out: passwords stored with Argon2id (per-user salt, tuned work factor), no mandatory periodic expiration, length over composition, breach-corpus screening at set-time, password managers and paste explicitly allowed. The standard names residual risks honestly — account-recovery paths become the new soft target (an attacker who can't phish the login attacks the help desk and the reset flow instead), synced-passkey cloud accounts must themselves be hardened, and AAL3 hardware logistics (provisioning, backup keys) need a process. Recovery and the help desk are flagged for the identity-governance work ahead (Chapter 18) and the access controls of Chapter 17.
bluekit increment — authn.py. We add two functions a defender actually uses: a password-strength check that reasons about real strength (length, character classes, and a common-password screen) rather than a naive composition rule, and a breach-prefix helper that demonstrates the k-anonymity technique behind privacy-preserving "have I been breached?" checks — sending only the first five characters of a password's SHA-1 hash so the full hash (and thus the password) is never revealed to the service. As always, illustrative and hand-traced — never executed.
# bluekit/authn.py — Chapter 16 increment
"""Authentication helpers: password strength and breach-prefix (k-anonymity)."""
COMMON = {"password", "123456", "qwerty", "letmein", "password1", "iloveyou"}
def password_strength(pw: str) -> str:
"""Crude strength band from length + variety + a common-password screen.
Returns 'reject' | 'weak' | 'ok' | 'strong'. Screening beats composition rules."""
if pw.lower() in COMMON or len(pw) < 8:
return "reject"
classes = sum(bool(s) for s in (
any(c.islower() for c in pw), any(c.isupper() for c in pw),
any(c.isdigit() for c in pw), any(not c.isalnum() for c in pw)))
if len(pw) >= 16 or (len(pw) >= 12 and classes >= 3):
return "strong"
return "ok" if len(pw) >= 12 or classes >= 3 else "weak"
def breached_prefix(sha1_hex: str) -> str:
"""k-anonymity concept: you send only the first 5 hex chars of SHA-1(password)
to a breach service, which returns all suffixes for that prefix to match LOCALLY.
The full hash (and password) never leaves your side. (No network here.)"""
return sha1_hex[:5].upper()
if __name__ == "__main__":
for pw in ("password1", "hunter2", "correct horse battery", "Tr0ub4dor&3xtra!"):
print(f"{pw!r:28} -> {password_strength(pw)}")
# Real use: hashlib.sha1(pw.encode()).hexdigest(); send only the prefix.
# Pass an ILLUSTRATIVE (fake) digest so the trace is deterministic, no hashing run:
print("send-prefix:", breached_prefix("abf0f1d2e3c4b5a6978899aabbccddeeff001122"))
# Expected output:
# 'password1' -> reject
# 'hunter2' -> reject
# 'correct horse battery' -> strong
# 'Tr0ub4dor&3xtra!' -> strong
# send-prefix: ABF0F
Trace it: "password1" is in COMMON → reject; "hunter2" is 7 characters, under 8 → reject; "correct horse battery" is 21 characters (≥ 16) → strong; "Tr0ub4dor&3xtra!" is 16 characters → strong. The breach helper returns the uppercase first five hex characters of a SHA-1 digest — the only thing you'd transmit — so the service can answer "has this been breached?" without ever learning the password. (We feed it a fake, illustrative digest rather than running a real hash, so the ABF0F prefix is deterministic for teaching and is not a verified digest of any real password — the concept, not the constant, is the lesson; the real check matches returned suffixes locally.) Both functions are deliberately small and conservative; in production you'd screen against a far larger corpus and tune the bands, but the shape — screen against what people actually pick, and never reveal the secret — is exactly right, and it is the same shape as NIST's modern guidance and the real "have I been pwned" range API.
Summary
This chapter built the authentication layer of the defender's mental model and Meridian's program.
- Authentication proves an identity (distinct from identification, the claim, and authorization, the permissions). It rests on three authentication factors: something you know (knowledge), something you have (possession), something you are (inherence).
- Multi-factor authentication (MFA) combines factors from different categories; its security comes from the independence of the categories, not from using one category twice. A password plus a security question is not MFA.
- NIST 800-63B defines authenticator assurance levels — AAL1 (single factor), AAL2 (MFA), AAL3 (phishing-resistant, hardware-based MFA, required for the highest stakes). Match the AAL to the value of what is protected.
- Password storage: never store the password — store a per-user-salted, memory-hard, deliberately slow hash (Argon2id preferred; bcrypt/scrypt/PBKDF2 acceptable). The salt defeats rainbow tables; the slow hash defeats brute-force speed. Both are required.
- Password policy (modern NIST): length over complexity, no mandatory periodic expiration, screen against breached/common lists, allow paste and password managers, throttle attackers not users. Entropy $H = L \times \log_2 N$ — but only for random choices; human-chosen passwords need breach-screening, not composition rules.
- MFA failure modes: SMS OTP is vulnerable to SIM swap and relay; TOTP resists SIM swap but is still phishable by real-time relay; push is vulnerable to push fatigue — defended by number matching, context, and rate-limiting, but not made phishing-resistant by them.
- Phishing-resistant MFA — FIDO2/WebAuthn/passkeys: an origin-bound public-key challenge-response. The private key never leaves the authenticator; the signature is bound to the real domain; there is no relayable secret. This defeats phishing, stuffing, SIM swap, and AITM relay — and is what saved Meridian.
- Biometrics are convenient and bound to the person but irrevocable, probabilistic (FAR/FRR/CER), and only as trustworthy as the measuring device. Use them as a local gesture to unlock an on-device key, never as a network secret or a central database of raw images.
- Credential attacks and their shapes: credential stuffing (many users × leaked passwords — defeat with breach-screening + MFA), password spraying (many users × few common passwords, under lockout — defeat with common-password screening + cross-account correlation), and MFA-defeat/ATO (detect via impossible travel, prompt bursts, recovery-flow abuse). Lockout alone catches neither stuffing-at-scale well nor spraying at all.
- Program + toolkit: Meridian's authentication standard (AAL-by-asset-tier; AAL3/phishing-resistant for money movement and admin) and
bluekit'sauthn.py(password_strength,breached_prefixwith k-anonymity).
Spaced Review
Before moving on, retrieve from earlier chapters without scrolling back:
- (Chapter 4) Password storage uses a hash, and FIDO2 login uses a digital signature. In one sentence each, what security property does a cryptographic hash provide, and what does a digital signature provide — and why does authentication need both in different places?
- (Chapter 4) A salt makes two identical passwords store as different hashes. What was the analogous role of a nonce/IV in encryption, and what general principle do salts and IVs share?
- (Chapter 3) MFA is a control. Classify it by function (preventive / detective / corrective) and by type (administrative / technical / physical), and name one detective control from this chapter that complements it.
- (Chapter 3) This chapter required phishing-resistant MFA for admin and money-movement access while allowing weaker MFA elsewhere. Which Chapter 3 principle does "stronger authentication for higher-value access" most directly express?
Answers
1. A hash is one-way (you cannot recover the input) and is used to *store* a password so a database leak doesn't expose it; a digital signature proves *authenticity and integrity* — that a specific private-key holder produced this exact data — which is what a FIDO2 login proves to the server. Authentication uses the hash at rest (storage) and the signature in transit (proving possession of the key). 2. A nonce/IV ensured that encrypting the same plaintext twice produced different ciphertext; salts and IVs share the principle that *randomizing per-use input defeats precomputation and pattern analysis* — never reuse, never need to keep them secret, just unique. 3. MFA is a *preventive*, *technical* control. A complementary *detective* control from this chapter: impossible-travel detection, MFA-prompt-burst alerting, or the cross-account spray detector. 4. Least privilege / matching the strength of a control to the value and risk of what it protects (defense in depth also applies — multiple independent layers — but the *graduated strength by value* idea is the least-privilege-of-assurance expression).What's Next
You can now prove who someone is, to a chosen level of assurance, and defend that proof against the attacks that target it. But proving identity only answers half the access question. Once Meridian knows a verified user is the loan officer — or the teller, or the domain administrator — it must decide what that person is allowed to do: which accounts they can view, which transactions they can initiate, whether they can move a wire. That is authorization, the subject of Chapter 17, where we build the access-control models (DAC, MAC, RBAC, ABAC), design roles that scale to a 1,800-person bank without collapsing into chaos, and confront privilege creep and the toxic combinations that separation of duties exists to prevent. Authentication got the right person through the door; authorization decides which rooms they may enter — and the two together are the foundation the rest of identity and access management is built on.