TECHNICAL PRESENTATION

Authentication
Methods

Passwords · MFA · Passkeys & WebAuthn
Argon2id TOTP FIDO2 Passkeys
know + have + are phishing-resistant

Establishing who the user is, before any authorisation happens. The complement to the OAuth/OIDC series.

Hash  ·  Verify  ·  Strengthen  ·  Replace
01

Topics

Passwords (and how to stop using them)

  • Hashing — bcrypt, Argon2id, scrypt, PBKDF2
  • Password policies that actually help
  • Breach-detection (HIBP) and credential stuffing
  • Account-lockout, rate-limiting, abuse signals

Multi-factor authentication

  • The three factor classes — what each is good for
  • TOTP (RFC 6238) and HOTP (RFC 4226) in detail
  • SMS / email codes — and why they're the weakest
  • Push, hardware OTP, smart cards

Phishing-resistant — WebAuthn / FIDO2 / Passkeys

  • The three names for the same thing
  • Authenticator types — platform vs roaming
  • Discoverable credentials & conditional UI
  • Synced passkeys vs device-bound
  • Attestation, AAGUIDs, FIDO MDS

Operational

  • Account recovery — the unsolved problem
  • Risk-based / adaptive auth
  • Step-up & CAEP shared signals
  • Anti-bot & abuse detection
  • Choosing a stack
02

Authentication vs Authorisation — Where This Deck Sits

The OAuth/OIDC decks in this series cover authorisation ("what may an app do on a user's behalf?") and identity assertions ("a JWT saying this is Alice"). Neither tells you how the user proved they were Alice in the first place. That is this deck.

User "is this really Alice?" Authentication passwords · MFA · passkeys → THIS DECK Identity Tokens OIDC ID tokens, SAML → OIDC / SAML decks Authorisation OAuth · RBAC · ABAC "Sign in with Google" packages all three; in your own systems they are separate concerns with separate failure modes.

A working definition

Authentication is the process of getting a high-confidence answer to "is the entity at the other end of this connection the human (or service) we expect?". The output of authentication is what feeds the AS in OAuth and the OP in OIDC.

03

Password Hashing — The Only Acceptable Way to Store Passwords

If you ever store a password in plaintext, reverse-encrypted, or with a fast hash (MD5, SHA-256), you have lost. Use a memory-hard password-hashing function with a per-user salt and tuned cost.

FunctionYearTypeStatus in 2026Why
MD5 / SHA-1 / SHA-256 (raw)1992+Fast hashForbiddenCrackable at billions/sec on a single GPU.
PBKDF2-HMAC-SHA2562000Iterated hashAcceptable for FIPS-onlyCPU-only, GPU-friendly. Use ≥ 600 000 iterations (OWASP 2023).
bcrypt1999AdaptiveAcceptableMemory-light (4 KB) — modern GPUs / FPGAs eat it. Use cost ≥ 12.
scrypt2009Memory-hardAcceptableMemory ≥ 64 MB, parallelism 1 — niche.
Argon2id2015Memory-hardRecommended (PHC winner, RFC 9106)Tunable memory + parallelism + time. OWASP default: m=46 MiB, t=1, p=1.

A storage record (Argon2id)

$argon2id$v=19$m=47104,t=1,p=1$
   c2FsdHNhbHRzYWx0c2FsdA$
   QthRKhMKM7wG2pfjzvVXsBcEfBLk8qUZcM…

      ↑ algorithm + version
                 ↑ memory KiB · time · parallelism
                          ↑ random salt (≥ 16 bytes, per-user)
                                   ↑ derived key

Tuning rule of thumb

Pick parameters so a single hash takes ~250–500 ms on production hardware. Update parameters as hardware improves; rehash on next successful login.

Always pepper if you can

  • A pepper is a server-side secret (in HSM / KMS / Vault) HMAC'd onto the password before hashing.
  • Stolen DB → attacker still needs the pepper to make any progress.
  • Trade-off: rotation is harder (need to rehash on login).

Common mistakes

  • Using password.equals(stored) — timing leak. Use constant_time_eq.
  • Truncating passwords to fit a column. Allow ≥ 64 chars.
  • Storing the hash and a "challenge" copy for SSO — defeats the hash.
  • Salting per-app instead of per-user.
04

Password Policy — What Actually Helps

Decades of "must contain a number, a symbol and the chief executive's mother's maiden name" gave us "Password1!". NIST SP 800-63B (current) and BS 8484 agree on a much shorter list.

Do

  • Minimum length 8 for low-risk; 12+ for normal accounts; 15+ for admin.
  • Allow length up to at least 64 characters; allow all printable Unicode + spaces.
  • Block known-breached passwords (HIBP Pwned Passwords API) — k-anonymity prefix lookup, not raw upload.
  • Block trivial dictionary words and the username itself.
  • Show the user a strength meter (zxcvbn) tied to the same logic the server uses.

Don't

  • Force scheduled rotation. Rotate on suspicion of compromise; otherwise leave alone.
  • Force composition rules (1 upper / 1 number / 1 symbol). They reduce entropy in practice.
  • Block paste — that's how password managers work.
  • Truncate or strip "special" characters silently.
  • Send the password back via email "for confirmation".

HIBP integration in five lines

const sha = sha1(password).toUpperCase();
const prefix = sha.slice(0, 5);
const suffix = sha.slice(5);
const list = await fetch(
  `https://api.pwnedpasswords.com/range/${prefix}`).then(r=>r.text());
if (list.split('\n').some(l => l.startsWith(suffix))) reject();

Server only ever sees the first 5 chars of a SHA-1 hash; the API returns ~500 candidates; you check locally.

05

Rate-Limiting, Lockout & Credential Stuffing

The single most consequential authentication API is /login. Every credential-stuffing campaign first finds it.

Three signals to rate-limit on

  • Per-account — exponential back-off after N failures for the same username. Protects each user.
  • Per-IP — caps brute-force from a single client. Easily defeated by botnets but raises cost.
  • Per-network / ASN — catches botnets concentrated on a hosting provider.

Lockout — the trade-off

  • Soft lockout — auto-unlock after N minutes. Default for consumer apps.
  • Hard lockout — admin re-enable. For high-value accounts; doubles as a self-serve DoS surface.
  • Always notify the user after the fact, never block silently.

Credential stuffing — what it actually looks like

  • Attacker has a list of (email, password) pairs from another site's breach.
  • They try each one against your /login at low rate per IP, high diversity of IPs.
  • Per-IP limits don't catch it. Per-account limits do — but only if you're prepared for users to be locked out by accident.
  • Best defence: HIBP-block known-breached passwords at signup and at login, plus risk-based MFA challenges on suspicious sessions.

Username enumeration

  • "Wrong password" vs "user not found" lets an attacker discover valid emails. Always return the same string and the same response time.
  • Same on signup ("email already registered") — use a generic "we've sent you a link" instead.
  • Same on password-reset.
06

The Three Factor Classes

Something you know

  • Password / passphrase
  • PIN
  • "Security questions"

Cheap, universal, phishable. The factor everyone has and the factor every attacker is best at extracting.

Something you have

  • Phone (TOTP app, push, SMS)
  • Hardware token (YubiKey, Titan, Feitian)
  • Smart card / PIV
  • Recovery codes printed on paper

Adds a physical constraint — attacker must possess the device. Susceptible to SIM-swap (SMS) and theft.

Something you are

  • Fingerprint, face, voice
  • Behavioural biometrics (typing rhythm)

Convenient, not a primary remote factor — biometric data leaks and you can't rotate your face. Used as a local unlock for a stronger factor.

"MFA" really means "two factors from different classes"

Password + security questions = one factor (both "know"). Password + TOTP = two factors. Password + face-unlock-of-passkey = two factors (the face unlocks the have). The class boundary, not the count, is what matters.

NIST AAL ladder (SP 800-63B)

  • AAL1 — single factor. Passwords alone live here.
  • AAL2 — two factors, one cryptographic. TOTP / push / hardware token + password.
  • AAL3 — hardware-bound cryptographic authenticator + verifier impersonation resistance. FIDO2 / PIV.
07

TOTP & HOTP — How "Authenticator App" Codes Work

HOTP (RFC 4226, 2005): HMAC-based one-time password from a counter. TOTP (RFC 6238, 2011): use the current Unix time / 30 s window as the counter. Both standards are open; the server and the app share a per-user secret only.

The whole algorithm

function totp(secret, t = floor(time()/30)) {
  let h = hmac_sha1(secret, int_to_8bytes(t));
  let off = h[19] & 0x0f;
  let bin = ((h[off]&0x7f)<<24)|(h[off+1]<<16)
          |(h[off+2]<<8)|h[off+3];
  return (bin % 1_000_000).toString().padStart(6,'0');
}

Server checks the current + previous + next windows to allow ~30 s of clock skew.

The "QR code" enrolment

otpauth://totp/Acme:alice@example.com?
  secret=JBSWY3DPEHPK3PXP&
  issuer=Acme&
  algorithm=SHA1&digits=6&period=30

Display this URI as a QR; the user's app (Authy, Google Authenticator, 1Password, Bitwarden, Aegis) decodes it.

Implementation notes

  • Secret length: 160 bits (20 bytes), random.
  • Store the secret encrypted at rest; treat as more sensitive than the password hash.
  • Burn the just-used code (replay protection inside the 30 s window).
  • Generate recovery codes at enrolment and store hashed (10 codes, single-use).

Why TOTP is no longer enough on its own

  • Phishable — a fake login site asks for the code, which the attacker forwards to the real one in 30 s.
  • Modern phishing kits (EvilProxy, Tycoon) automate exactly this.
  • Useful as a step up from password-only, but the future is phishing-resistant (FIDO2/passkeys).
08

SMS, Email & Push — The Weakest "Strong" Factors

SMS one-time codes

  • SIM-swap attacks shift the user's number to the attacker's SIM.
  • SS7 / SMS gateway interception is well-documented.
  • Phishable identically to TOTP.
  • NIST 800-63B-3 deprecated SMS as a factor in 2017; many regulators followed.

Still the most-deployed second factor in the world because every phone has SMS. Use only if you literally cannot ship anything else.

Email magic links

  • Equivalent strength to "control of the inbox" — usually one password away.
  • Susceptible to email-account takeover, link harvesting in shared inboxes.
  • Convenient for low-risk consumer flows ("sign in to read my newsletter").

Acceptable as a passwordless first factor for low-stakes apps; not as MFA for anything serious.

Push notifications (Microsoft/Duo style)

  • App on the user's phone receives a "did you just try to log in?" prompt.
  • Better UX than typing a code; bound to the registered device.
  • Vulnerable to MFA fatigue — bombard the user until they hit Approve.
  • Mitigation: number matching — show a 2-digit code on the login page; user must type it into the push.

A reasonable 2026 ladder

  1. Passkey / FIDO2 hardware key — phishing-resistant, the gold standard.
  2. TOTP / push-with-number-matching — when the user can't or won't enrol a passkey.
  3. SMS — last resort, for users with neither smartphone nor security key.

Always offer two methods, so loss of one doesn't lock the user out (recovery codes count).

09

WebAuthn / FIDO2 / Passkeys — Three Names, One Thing

The vocabulary, untangled

  • FIDO2 = the umbrella spec from the FIDO Alliance (since 2018).
  • WebAuthn = the W3C browser API half of FIDO2 — what your JS calls.
  • CTAP2 = the FIDO Alliance protocol between the browser and the authenticator (USB / NFC / BLE).
  • Passkey = a marketing term (Apple/Google/Microsoft, 2022) for a FIDO2 credential that's discoverable and (usually) synced across the user's devices.

Why it matters

  • Phishing-resistant by design — the authenticator only signs for the actual origin it was registered for, period.
  • No shared secret to steal from your DB; you store only the user's public key.
  • No code to type, no SMS to intercept.
  • Cross-platform: same API on every modern browser.

How the cryptography binds to the origin

# registration (one-off)
public_key, credential_id = authenticator.create({
  rp: { id: "acme.com", name: "Acme" },
  user: { id, name, displayName },
  challenge: random_bytes(32),
  pubKeyCredParams: [{type:"public-key", alg:-7}]
})
# server stores (user_id → public_key, credential_id, sign_count)

# login
signed = authenticator.get({
  rpId: "acme.com",
  challenge: random_bytes(32),
  allowCredentials: [{ id: credential_id }]
})
# server verifies: signature, origin = "https://acme.com",
#                  rpIdHash matches, sign_count increased

The phishing-resistance proof

The clientDataJSON the authenticator signs over includes the actual origin of the page making the request. A phish at acme.evil.com can never produce a signature the real acme.com server would accept — because the authenticator only ever signs for what it sees in the address bar.

10

Authenticator Types — Platform vs Roaming, Synced vs Bound

 PlatformRoaming
What it is Built into the device (Touch ID, Face ID, Windows Hello) Plugged in / tapped (YubiKey, Titan, Feitian)
Transport Internal — Secure Enclave, TPM, StrongBox USB-A/C, NFC, BLE
User experience Touch the sensor / look at the camera Tap the key, enter PIN if set
Where the key lives On this device only (or synced — see below) On the physical token, never leaves it
Best for Everyday consumer login, low-friction MFA High-assurance, shared devices, hardware-attested compliance

Synced passkeys

  • Apple iCloud Keychain, Google Password Manager, 1Password, Bitwarden, Microsoft (Edge / Authenticator).
  • Private key replicated, end-to-end encrypted with the user's account password / device PIN, across all the user's devices.
  • Lose your phone? Sign in on a new one with your iCloud / Google account, your passkeys are there.
  • The default passkey type for consumer apps in 2026.

Device-bound credentials

  • Stay on a single device; cannot be exported.
  • Hardware tokens are always device-bound. Platform authenticators can be configured this way.
  • Required by some regulators (eIDAS High, FIDO L3 attestation, defence).
  • Operationally heavier: every device needs its own enrolment, and a lost device = a recovery flow.

The synced-vs-bound debate

Consumer security teams: "synced is fine, the real risk is account takeover, and synced passkeys still beat passwords by an order of magnitude". Regulated security teams: "if it can leave the device, it doesn't satisfy hardware-bound assurance". Both are right for their threat models.

11

Discoverable Credentials & Conditional UI

The two features that make passkeys feel better than passwords, not just safer.

Discoverable credentials (a.k.a. resident keys)

  • Old (non-discoverable): server must hand the authenticator the credential ID first; the user must type a username.
  • New (discoverable): the credential carries the username with it, on the authenticator. The login page can ask "any passkey for this site?".
  • This is what enables username-less sign-in.

Conditional UI (autofill)

// in your login form
<input type="text" name="username"
       autocomplete="username webauthn">

// JS at page load
navigator.credentials.get({
  mediation: "conditional",
  publicKey: { challenge, rpId, userVerification: "preferred" }
});

Browser shows passkeys in the same dropdown as saved passwords. User taps one → done. The flow that finally beats password manager autofill.

A 2026-grade login UX

  1. Page renders an empty username field with autocomplete="username webauthn".
  2. Conditional navigator.credentials.get() fires in the background.
  3. If the user has a synced passkey for this site, it appears in the autofill bubble.
  4. One tap (Touch ID / Face ID) and they're in. No username, no password, no MFA prompt.
  5. If no passkey: form falls back to password + (TOTP / push) flow.

Best practice

Always present the passkey path and a fall-back. Don't force migration; nudge it: "We just signed you in with your password. Want to add a passkey for next time?" Conversion rates rise sharply when you ask in-context.

12

Attestation, AAGUIDs & the FIDO Metadata Service

For regulated deployments you need to know what kind of authenticator the user just enrolled — was it a YubiKey 5, a synced iCloud passkey, a software-only fake?

Attestation in one paragraph

During registration the authenticator can include a signed attestation statement proving its make/model. Attached to it is the AAGUID — a 128-bit identifier for the authenticator model.

{
  "fmt": "packed",
  "attStmt": { "alg": -7, "sig": "…",
               "x5c": [ "MIIEx…device-cert" ] },
  "authData": { "aaguid": "ee882879-…",
                "credentialPublicKey": … }
}

The FIDO Metadata Service (MDS)

  • FIDO Alliance publishes a signed JSON list of every certified authenticator AAGUID with its capabilities, certifications and revocation status.
  • Your RP fetches MDS regularly, indexed by AAGUID.
  • Lets you say "only accept FIDO L2 hardware authenticators for admin enrolment".

Attestation conveyance

attestationMeaning
noneRP doesn't want it; default for consumer apps. Privacy-preserving.
indirectBrowser may anonymise / batch-attest. Compromise.
directReal attestation; required for regulated / enterprise enrolment.
enterprisePermits per-device serial-number attestation; only allowed for whitelisted RPs.

Privacy trade-off

Direct attestation can let the RP track which physical YubiKey the user used across services. Browsers may rewrite attestation to prevent this; consumer apps should just use none.

13

Account Recovery — The Unsolved Problem

The strongest authenticator in the world is undone by the recovery flow. Whatever path the legitimate user takes when they lose access is the same path an attacker takes when they don't have any.

Common recovery paths, ranked by safety

  1. A second registered authenticator — second YubiKey, second device's passkey. Best.
  2. Single-use printed recovery codes, generated at enrolment, stored offline.
  3. In-person verification at a retail counter (banks). Slow, expensive, hard to phish.
  4. Synced-credential recovery (iCloud / Google) — strong if the underlying account is locked down.
  5. Email + previous-password challenge — moderate.
  6. "Identity verification" (photo of passport, selfie) — phishable; depends on a vendor's KYC quality.
  7. Security questions — please no.

Patterns that work in practice

  • Encourage two passkeys at enrolment ("set up a backup now?"). Most users won't, but the prompt halves recovery volume.
  • Cool-down on recovery — recovered accounts spend 24–72 h in a low-trust mode (no high-value actions, alerts to the original email).
  • Notify aggressively — recovery initiated, recovery completed, all to every channel you have on file.
  • Make the recovery path obvious in the UI — hidden recovery becomes social-engineering scope for support staff.

If you do nothing else

Decide upfront whether your security model is "we never recover an account, lost = lost" (e.g. crypto wallets) or "we always recover, with friction proportional to risk" (every consumer app). Trying to do both produces the worst of each.

14

Risk-Based / Adaptive Authentication & CAEP

Risk-based auth — the idea

Don't ask the user for MFA on every login. Score each session for risk and step up only when the score warrants.

  • New device / fingerprint?
  • New IP geolocation, ASN, or impossible-travel from the previous session?
  • Time of day vs the user's pattern?
  • Browser / OS user-agent shift?
  • Logins for many distinct accounts from the same IP (credential stuffing)?
  • HIBP-flagged password just attempted (yes, even if correct)?

Step-up — what to ask for

  • Low risk: nothing.
  • Medium: extra factor for this session only.
  • High: re-auth + email confirmation; lock account on second failure.

If you're using OIDC, request the step-up via acr_values + prompt=login; verify the returned acr and amr.

CAEP & SSF — Continuous Access Evaluation

  • SSF = Shared Signals Framework (OpenID Foundation): a publish/subscribe channel for security events between IdPs and SPs.
  • CAEP = the profile of SSF for "this user just changed their password / had their session revoked / failed step-up".
  • Lets an SP react to a signal from another SP / IdP during a session, not only at login.
  • Already deployed by Google, Microsoft, Okta, Cisco; growing in 2025–2026.

Practical pattern

Subscribe your high-value SaaS apps to the workforce IdP's CAEP stream. When the IdP sees a suspicious sign-in, fires token-claims-change or session-revoked; every downstream app drops the session within seconds, not hours.

15

Anti-Bot & Abuse Detection

Authentication endpoints are also the primary surface for non-credential attacks: account creation abuse, scraping, low-and-slow stuffing.

Tools that work

  • Privacy-preserving CAPTCHAs — Cloudflare Turnstile, hCaptcha, reCAPTCHA Enterprise. Invisible by default.
  • Private Access Tokens (Apple, Cloudflare, Fastly) — device attestation without per-site tracking.
  • Rate-limit at the edge (Cloudflare, Akamai, Fastly) — drop the obvious botnet traffic before it touches your origin.
  • JS challenges — cheap puzzles that headless browsers find expensive.

Fingerprinting (with caution)

  • Device fingerprint (canvas, WebGL, fonts, audio context) is useful for risk scoring but ages quickly and conflicts with privacy regulation.
  • Browser-issued device tokens (FedCM, Private Access Tokens) are the privacy-preserving replacement.

Don't

  • Use IP address as a primary identity signal — mobile carriers, CGNAT, and corporate VPNs make this unreliable.
  • Block a country / region without an audited business reason — you'll quietly lock out diaspora users.
  • Show a different page based on User-Agent — every scraper rotates UAs.
16

Choosing a Stack — Build vs Buy

If you're...Likely bestWhy
A startup, B2C, want a passkey-first UX in two weeks Stytch / Clerk / Auth0 Hosted passkey + magic-link + social out of the box; passkey UX they iterate on faster than you will.
A B2B SaaS that needs to sell to enterprise WorkOS + your own login UI Adds SSO/SCIM the moment your first big customer asks; passkeys for end users on top.
An enterprise with existing AD / Entra Microsoft Entra ID + Authenticator Conditional access policies, Authenticator app, FIDO2 keys all in one tenant; integrates with everything.
Self-hosting, mature ops Keycloak + WebAuthn extension, or ZITADEL Full control; both ship modern WebAuthn / passkey support, conditional UI works, FAPI-conformant.
A government / eIDAS-regulated portal National eID + national IdP End users already have the credential (BankID, eID card, EUDI Wallet); reuse, don't reissue.
A tiny internal tool / homelab Authelia or Authentik + a hardware key Light, OIDC-aware reverse-proxy auth; covers WebAuthn for everything behind the proxy.
A high-security workload, no off-the-shelf option Build on libraries: passport-fido2-webauthn · SimpleWebAuthn · fido2-net-lib · webauthn-rs Each is well-maintained, conformant, and small. Don't write the protocol yourself.
17

Summary & References

What we covered

  • Where AuthN sits relative to OAuth/OIDC
  • Password hashing (Argon2id is the default), peppering, common mistakes
  • NIST 800-63B password policy — what to do, what to drop
  • Rate-limiting, lockout trade-offs, credential stuffing, username enumeration
  • The three factor classes and the AAL ladder
  • TOTP/HOTP in detail; SMS / email / push pros and cons
  • WebAuthn / FIDO2 / Passkeys — three names, one phishing-resistant primitive
  • Platform vs roaming; synced vs device-bound
  • Discoverable credentials + conditional UI = the 2026 login UX
  • Attestation, AAGUIDs, FIDO MDS
  • Account recovery — patterns that work
  • Risk-based auth, step-up, CAEP / SSF
  • Anti-bot & abuse
  • Build vs buy

Three take-aways

  1. Argon2id with per-user salt + server pepper is the only acceptable password storage in 2026.
  2. All non-WebAuthn second factors are phishable. They're a step up from password-only, but the destination is FIDO2 / passkeys.
  3. Recovery is the security model. Whatever the legitimate-user-who-lost-everything path is, that is also the attacker path. Design it deliberately.

Companion decks

OAuth — A Gentle Primer · Introduction to OAuth · OAuth for MCP Servers · Introduction / Advanced OpenID Connect · SAML 2.0 & SCIM · Authorization Models.

References

NIST SP 800-63B-3 (Digital Identity Guidelines, Authenticator & Lifecycle) · OWASP ASVS v4 §2 (Authentication) · OWASP Password Storage Cheat Sheet · RFC 9106 (Argon2) · RFC 4226 (HOTP) · RFC 6238 (TOTP) · W3C WebAuthn Level 3 · FIDO Alliance — fidoalliance.org · FIDO Metadata Service v3 · haveibeenpwned.com / Pwned Passwords · OpenID SSF / CAEP · passkeys.dev

One-line takeaway

Stop treating "MFA" as the goal. The goal is phishing-resistant authentication; passkeys are the realistic path there for the next decade.