Syed Hasibur Rahman20 min read

Passkeys in Production: The Sharp Edges Nobody Puts in the Demo

The WebAuthn demo takes an afternoon. Production takes a year, and the hard parts are nowhere near the ceremony code: an rp_id decision you cannot revise after launch, a sync fabric that quietly swaps hardware-bound keys for account-bound ones, attestation that returns 'none' for the credentials most of your users hold, and a recovery flow that reintroduces every phishable path passkeys were supposed to eliminate. This post walks the sharp edges with spec citations from WebAuthn Level 2/3 and CTAP 2.1/2.2, and closes with the security claim you can honestly make after shipping.

The registration demo takes an afternoon. You call navigator.credentials.create(), the browser draws a beautiful platform sheet, Touch ID glows, and a public key lands in your database. Someone on the team posts a screen recording in Slack with the caption “passwords are dead.” Everyone is very pleased.

Eighteen months later the company rebrands, app.oldname.com becomes newname.com, and you discover — usually in a war room, usually from a spike in support tickets — that every passkey your users ever created is scoped to a domain you are trying to walk away from, that the scoping is cryptographic rather than configurational, and that no amount of DNS cleverness will move those credentials. The demo never mentioned this. Demos never do.

This post is about the distance between the demo and production. Passkeys — discoverable FIDO2 credentials, standardized across the W3C’s WebAuthn specification and the FIDO Alliance’s CTAP2 protocol — are, in my judgment, the most important improvement to consumer authentication in twenty years. They are also a system whose sharp edges live almost entirely outside the ceremony code: in domain strategy, in the sync fabrics operated by Apple and Google, in attestation policy, and above all in account recovery, which is where the actual attacks have moved. We will take the edges one at a time, with the specs open.


The stack, precisely

Terminology first, because “passkey” is a marketing word layered over two specifications that carve the problem at different joints.

WebAuthn — Web Authentication: An API for accessing Public Key Credentials — is the W3C specification defining the browser API and the data structures a server (the Relying Party, RP) must verify. Level 2 became a W3C Recommendation on 8 April 20211. Level 3, which standardizes the passkey-era machinery — conditional UI, the backup-state flags, related origin requests — has been advancing through Candidate Recommendation drafts and, as of this writing in mid-2026, sits at Candidate Recommendation2.

CTAP2 — the Client to Authenticator Protocol — is the FIDO Alliance specification for how a client platform (browser plus OS) talks to an authenticator. CTAP 2.1 became a Proposed Standard on 15 June 2021 and brought enterprise attestation, credential management, and the credProtect extension3. CTAP 2.2, published 14 July 2025, formalized hybrid transport — the QR-code-plus-proximity flow that lets a phone act as an authenticator for a nearby laptop4.

A passkey is a WebAuthn discoverable credential (the spec’s term for what CTAP calls a resident key): a credential the authenticator can find by RP identifier alone, without the server first supplying a credential ID. Discoverability is what enables the username-less, autofill-style sign-in flow. When the private key is additionally synchronized through a vendor fabric — iCloud Keychain, Google Password Manager, or a third-party credential manager — the industry calls it a synced passkey; when it stays locked to one device’s secure hardware, a device-bound passkey. That one adjective, as we will see, moves most of the security model.

The core protocol is unusually well-studied for something so young. Barbosa, Boldyreva, Chen, and Warinschi published the first provable-security analysis of FIDO2 at CRYPTO 2021, confirming WebAuthn’s authentication security while surfacing weaknesses in CTAP2’s PIN protocol5. Bindel, Cremers, and Zhao extended the analysis to CTAP 2.1 and WebAuthn Level 2 in 2022, including post-quantum instantiations6. Keep a critical fact in view, though: these proofs cover the ceremonies. They do not — cannot — cover the sync fabric, the recovery flow, or your fallback login page. The attacks have noticed.


rp_id scoping: the decision you make once

Every WebAuthn credential is created under an RP ID — a domain string the server chooses at registration. The rules come from WebAuthn’s use of the HTML standard’s “is a registrable domain suffix of or is equal to” algorithm: the RP ID must equal the caller’s effective domain or be a registrable suffix of it1. From https://app.example.com, valid RP IDs are app.example.com and example.com. Not www.example.com (a sibling), not example.co (a different registrable domain), and never a public suffix like com or github.io — the Public Suffix List draws that line7.

The consequence engineers miss is that the RP ID is baked into the credential cryptographically, twice over. At creation, the authenticator stores the credential indexed under the RP ID. At every assertion, the authenticator data the client signs begins with rpIdHash — the SHA-256 of the RP ID — and your server MUST verify it matches the RP ID it expects1. There is no rename operation. There is no migration ceremony. A passkey created for oldname.com will answer for oldname.com until the credential is deleted.

This produces two production traps.

Trap one: scoping too narrow. If you register credentials under app.example.com and later launch admin.example.com or m.example.com, the existing passkeys are invisible there. The defensive default is to scope to the organization’s registrable domain (example.com) unless you have a genuine multi-product isolation requirement — a subdomain that hosts user content, for instance, must never be able to exercise the apex’s credentials, which is exactly why the spec lets you scope narrow.

Trap two: the domain migration. Rebrands, acquisitions, country-code domain splits (example.com, example.co.uk, example.de) all collide with the same wall: credentials cannot cross registrable domains. WebAuthn Level 3’s answer is Related Origin Requests: the RP ID’s origin serves a JSON document at https://<rpid>/.well-known/webauthn listing the origins allowed to use that RP ID8:

{
  "origins": [
    "https://example.co.uk",
    "https://example.de",
    "https://example-newbrand.com"
  ]
}

Chrome shipped support in version 128 and Safari in version 18, both in 2024; clients cap the number of distinct registrable labels (five, in the shipping implementations), so this is a bridge for a handful of domains, not a wildcard9. Two operational realities follow. First, the old domain must stay alive, on TLS, serving that file, indefinitely — your rebrand now has a permanent dependency on the brand you retired. Second, ROR arrived with Level 3; older clients simply fail. Every team I have watched go through this ends in the same place: keep the old RP ID, serve the well-known file, and re-register new credentials under the new RP ID opportunistically as users sign in, retiring the old scope over quarters, not weeks.

Choose your RP ID the way you choose a database primary key. It is the least revisable decision in the whole deployment.


Platform, roaming, and the hybrid in between

WebAuthn divides authenticators by attachment. A platform authenticator is built into the client device — Touch ID and Face ID backed by the Secure Enclave, Windows Hello backed by the TPM, Android’s hardware-backed keystore. A roaming (cross-platform) authenticator travels between devices — a YubiKey or similar security key speaking CTAP2 over USB, NFC, or BLE1.

The passkey era added a third shape that is neither: hybrid transport, formalized in CTAP 2.24. The laptop’s browser displays a QR code; the phone scans it; the two devices prove physical proximity over BLE advertisements; then the actual CTAP traffic runs through a vendor-operated tunnel service. The phone — with its synced passkeys — becomes a roaming authenticator for the laptop, without the laptop trusting the network path. The BLE proximity check is the anti-phishing load-bearing wall here: a remote attacker who convinces a victim to scan a QR code still cannot complete the ceremony from another city, because the BLE advertisement never arrives.

What the demo hides is how different the guarantees are across these shapes:

PropertyPlatform (device-bound)Platform (synced passkey)Security keyHybrid
Private key extractableNo (secure hardware)Leaves device via encrypted syncNoDepends on phone’s credential
Survives device lossNoYes (fabric recovery)No (buy two)Follows the phone
Attestation availableSometimesEffectively noYesRarely
Root of trustDevice hardwareVendor accountKey hardwareVendor account
Phishing-resistantYesYesYesYes (BLE proximity)

Read the second row carefully. Every cell in the phishing-resistance row is a win — that part of the passkey pitch is true across the board. But the “root of trust” row is where your threat model actually lives now, and for the majority of consumer credentials it says vendor account, not hardware. Hold that thought for two sections.

Your server can observe some of this. WebAuthn Level 3 defines two flags in the authenticator data: BE (backup eligible, bit 3) and BS (backup state, bit 4)2. A credential with BE=1 can be synced; BS=1 means it currently is. A bank that wants to treat device-bound and synced credentials differently — stepping up verification for high-value transfers when BS=1, say — has the signal to do it. Most RPs should record these flags even if they do not yet act on them; you cannot retrofit the data.

One more thing the demo hides: the signature counter is dead. Classic WebAuthn used a monotonic signCount to detect cloned authenticators. Synced passkeys are legitimately cloned across devices by design, so the fabrics return a counter of zero, permanently. If your verification library hard-fails on non-increasing counters — many 2019-era ones do — synced passkeys will break in ways that look like user error.


Attestation: mostly a memory

Attestation is WebAuthn’s mechanism for proving what kind of authenticator created a credential: the authenticator signs the new public key with a device-class key whose certificate chains to the manufacturer, and your server validates the chain against metadata — canonically the FIDO Alliance’s Metadata Service, keyed by the authenticator’s AAGUID10. CTAP 2.1 even added enterprise attestation, which lets managed deployments get uniquely identifying attestation from provisioned devices3.

Here is the production reality: for passkeys, you mostly get nothing. Apple’s platform authenticator returns attestation format none for synced passkeys; Google Password Manager likewise. The vendors’ position — defensible, if inconvenient — is that attestation was designed for certifiable hardware, and a credential that lives in a sync fabric has no single hardware to certify. The AAGUID still arrives and identifies the provider (there is a community-maintained registry mapping AAGUIDs to iCloud Keychain, Google Password Manager, 1Password, and the rest11), but it is unsigned metadata in the synced case — a hint, not a proof.

So set your attestation conveyance to "none" unless you have a regulatory requirement, and if you do have one — payment services directives, government IAL levels, enterprise device policy — understand what you are actually buying: an allowlist policy that admits security keys and managed device-bound credentials and rejects the synced passkeys most of your users hold. Attestation-gated deployments are real (banking, PIV-adjacent government), but they are choosing a smaller, harder population on purpose. For everyone else, attestation ceremony code is complexity without a decision behind it. Delete it.


The sync fabric: what you outsourced without signing anything

When a user creates a passkey on an iPhone, the private key is generated on-device and synchronized through iCloud Keychain, end-to-end encrypted, with recovery mediated by escrowed keys in Apple’s HSM fleet, rate-limited and bound to the device passcode12. Google Password Manager does the analogous thing: passkey private keys are backed up in encrypted form, decryptable only on the user’s devices, protected by the device screen-lock factor13. Both designs are genuinely good. Neither vendor can trivially read your users’ private keys, and both published enough detail to evaluate the claim.

But notice what happened to your security model. With a device-bound credential, the sentence “the private key never leaves the secure element” is a hardware claim. With a synced passkey, the honest sentence is: the private key is available on every device that can authenticate to the user’s Apple or Google account, under that account’s recovery policy. Your authentication is now transitively as strong as a consumer account-recovery flow you did not design, cannot audit, and will never see the logs of. A 2025 comparative evaluation of device-bound versus synced passkeys makes the point formally: syncing shifts the trust anchor from tamper-resistant hardware to the credential provider’s account security14.

Is that bad? Mostly no — for consumer authentication it is the correct trade, because the alternative to recoverable credentials is not “users with hardware keys,” it is “users locked out, falling back to passwords.” Apple’s and Google’s account security teams are better resourced than yours. But it has two consequences you must engineer for:

  1. Cross-ecosystem reality. Users live in mixed fleets — iPhone plus Windows laptop, Android plus iPad. A passkey in iCloud Keychain does not natively appear in Google Password Manager. Hybrid transport papers over this at sign-in time (scan the QR, use the phone), but the friction is real, and it is why the FIDO Alliance published the Credential Exchange Protocol and Credential Exchange Format working drafts in October 2024 — an HPKE-protected scheme for moving credentials between providers, with Apple, Google, Microsoft, 1Password, Bitwarden, and Dashlane at the table15. Portability is arriving; Apple began shipping credential-transfer machinery in its 2025 OS cycle. Until it is universal, expect a meaningful fraction of users to hold two or three passkeys per account, one per ecosystem, and design your account-security page to show and name them (this is what the AAGUID registry is for).

  2. The fabric is now in your attack tree. Which brings us to the section this post has been building toward.


Account recovery is the actual attack surface

In August 2023, attackers SMS-phished an employee of Retool, deepfaked a colleague’s voice on a phone call to extract one additional OTP, and enrolled their own device into the victim’s Okta account. The pivot that made the breach catastrophic was Google Authenticator’s then-new cloud sync: compromising the employee’s Google account handed the attacker every TOTP seed the employee had, because the seeds now synced through the account16. Twenty-seven downstream customers were compromised; one chain of victims lost roughly $15 million.

No passkeys were involved, and that is exactly why the incident matters here: it is the cleanest public demonstration of the synced-credential failure mode. The credential was strong. The fabric carrying it was subordinated to an account, and the account was subordinated to a recovery flow, and the recovery flow was subordinated to a human on a phone call. Every synced passkey deployment inherits this shape. The attacker’s optimal path is no longer “phish the credential” — the BLE proximity check and origin binding genuinely killed that — it is “become the user to the recovery system,” whether that recovery system is Apple’s, Google’s, or yours.

And yours is the soft one. Consider what a typical service ships around its shiny passkey login:

  • password fallback (“in case you lose access”)
  • email magic links (account takeover = mailbox takeover)
  • SMS OTP fallback (SIM swap)
  • support-desk manual recovery (the Retool vector, and the MGM vector before it)

NIST’s Digital Identity Guidelines, revision 4 — finalized in July 2025 — are blunt about the framing: an account’s assurance level is determined by the weakest authentication and recovery path that can produce an authenticated session, not the strongest17. A passkey login next to an SMS-recoverable password is not phishing-resistant authentication. It is phishing-resistant decoration on a phishable account.

The engineering discipline that follows:

  1. Enumerate every path to an authenticated session — logins, recovery, support tooling, OAuth connections, session-extension endpoints. The passkey is one edge in that graph. Attackers run Dijkstra on the whole thing.
  2. Make recovery slow and loud. Time-delayed recovery with notification to all enrolled devices (“your account will be recoverable by X in 72 hours — not you? tap here”) converts recovery from an instant bypass into a contested window. Apple’s own iCloud recovery leans on exactly this pattern; copy it.
  3. Recovery should re-prove identity, not re-check possession. For anything high-value, recovery is an identity-proofing event (document verification, existing-device approval), not a “click the email link” event.
  4. Train support to be un-phishable, or remove their power. If a support agent can reset authentication factors from a phone call, your AAL is “phone call.”

The fallback, and what it does to the security claim

Every team ships a fallback. The honest reasons: users on browsers without WebAuthn, users who cleared their platform account, enterprises with unmanaged fleets, and the brutal funnel math that says a sign-in failure is a churn event. So the password stays, or the OTP stays, and the deployment becomes “passkeys plus everything we already had.”

Understand precisely what that does to the claim. Phishing resistance in WebAuthn comes from the client binding the ceremony to the origin: the browser writes the actual origin into clientDataJSON, the authenticator scopes the credential by RP ID, and a look-alike domain gets signatures that verify for the wrong origin and are rejected1. That property is per-ceremony, not per-account. An attacker facing a passkey-plus-password account doesn’t attack the passkey; their phishing kit simply renders the password form. The user, trained by years of fallback flows, complies. This is a downgrade attack, and the entire fallback surface is the downgrade.

The realistic mitigation path, in deployment order:

  1. Ship passkeys as an addition. Accept the unchanged security claim; you are buying UX and building inventory of enrolled credentials.
  2. Suppress, then remove, fallbacks per-account. Once an account has ≥2 passkeys (or one synced passkey plus a verified recovery path) and N consecutive passkey sign-ins, stop offering the password, then let the user delete it. Big providers converged on exactly this “skip the password when a passkey exists” ratchet through 2023–202418.
  3. Gate the fallback with risk. Where the password must survive, wrap it: new device + password + no passkey attempt = step-up or delay. The fallback becomes an exception path with monitoring, not a parallel front door.
  4. Only then say “phishing-resistant.” The claim attaches to accounts whose every path to a session is origin-bound or proofed — and to nothing weaker.

Verification code: the checklist that actually bites

The registration and assertion verification steps are enumerated in WebAuthn §7.1 and §7.21; libraries like SimpleWebAuthn and python-fido2 implement them faithfully. Production incidents cluster on a handful of steps, so here is the assertion-side shape with the sharp edges annotated:

// Assertion verification — the parts that page you at 3am are marked.
async function verifyAssertion(assertion, session, storedCredential) {
  const clientData = JSON.parse(utf8(assertion.response.clientDataJSON));

  // 1. Ceremony type and one-shot challenge. The challenge MUST come from
  //    server-side session state and MUST be consumed on first use.
  assert(clientData.type === "webauthn.get");
  assert(timingSafeEqual(b64url(clientData.challenge), session.challenge));
  await consumeChallenge(session.challenge);        // replay = incident

  // 2. Origin. Exact-match against an allowlist — with Related Origin
  //    Requests you may have several. Never suffix-match.
  assert(ALLOWED_ORIGINS.has(clientData.origin));

  const authData = parseAuthenticatorData(assertion.response.authenticatorData);

  // 3. rpIdHash — SHA-256 of the RP ID you *intended*. Wrong constant here
  //    "works in dev" (localhost) and fails only in production.
  assert(timingSafeEqual(authData.rpIdHash, sha256(RP_ID)));

  // 4. Flags. UP required; UV per your policy. Record BE/BS — synced vs
  //    device-bound is a risk signal you'll want history for.
  assert(authData.flags.up === true);
  if (POLICY.requireUV) assert(authData.flags.uv === true);
  await recordBackupFlags(storedCredential.id, authData.flags.be, authData.flags.bs);

  // 5. Signature over authData || SHA-256(clientDataJSON), with the stored
  //    public key — never a key supplied in the request.
  assert(verifySignature(storedCredential.publicKey,
    concat(assertion.response.authenticatorData, sha256(assertion.response.clientDataJSONRaw)),
    assertion.response.signature));

  // 6. signCount: synced passkeys legitimately report 0 forever.
  //    Treat a *regression* on a device-bound credential as a clone signal
  //    to flag — do not hard-fail the fleet on zeros.
  if (authData.signCount !== 0 || storedCredential.signCount !== 0) {
    if (authData.signCount <= storedCredential.signCount) flagPossibleClone(storedCredential.id);
  }
  await updateSignCount(storedCredential.id, authData.signCount);
}

Two registration-side notes of the same flavor. Pass excludeCredentials with the user’s existing credential IDs, or users will mint duplicate passkeys per device per ecosystem and your support team will drown in “which passkey is this” tickets. And use conditional UI (mediation: "conditional" with autocomplete="username webauthn") so passkeys surface in the autofill flow rather than behind a button nobody clicks — it is the single largest adoption lever in the Level 3 feature set2.


What the formal analyses do and don’t buy you

It is worth being precise about what “provably secure” means here, because the phrase gets deployed as marketing. The CRYPTO 2021 analysis models WebAuthn as a passwordless authentication protocol and proves it achieves its authentication goals under standard assumptions — and separately shows CTAP2’s PIN protocol achieves only a weak form of its goal, which CTAP 2.1 subsequently improved5. The 2022 follow-up covers CTAP 2.1 and WebAuthn Level 2 and even establishes post-quantum instantiability of the ceremonies6.

What sits outside every one of these models: the sync fabric’s escrow and recovery, the vendor account’s authentication, your fallback stack, your support desk, and the Related-Origins JSON file your intern can edit. The proofs are real, and the protocol deserves them. But the composed system — protocol plus fabric plus recovery plus fallback — has no proof, and the Retool incident is what its counterexamples look like. Security architecture for passkeys is the discipline of not letting the unproven 80% embarrass the proven 20%.


What to actually do

If you are taking passkeys to production this year, the compressed prescription:

  1. Scope the RP ID to your registrable domain, decide it like a primary key, and stand up /.well-known/webauthn on day one even if it lists nothing — the file’s existence is your future migration hatch8.
  2. Accept synced passkeys; record BE/BS. Do not gate on attestation unless a regulator makes you, and if one does, budget for the excluded population.
  3. Request attestation: "none", residentKey: "required", UV per risk tier. Delete the attestation-chain code you were not going to act on.
  4. Fix the counter logic before launch. Zeros are normal now.
  5. Treat recovery as the perimeter. Slow it down, notify loudly, re-prove identity, and defang the support desk. This is where your next incident is.
  6. Ratchet the fallback off per-account, and only claim phishing resistance for accounts where the ratchet has fully engaged.
  7. Show users their credentials by provider name (AAGUID registry), with creation dates and last-used timestamps. Deletable. Auditable.

Closing: the claim you can honestly make

Passkeys keep their core promise. Origin-bound, proximity-checked, cryptographic sign-in genuinely ends the credential-phishing era for the ceremonies it covers — the formal analyses hold, the ecosystem shipped, and the UX, post conditional-UI, is finally better than passwords rather than merely safer. Teams that deploy them are not buying hype.

But the demo-grade claim — “we’re phishing-resistant now” — is not purchasable with a credentials.create() call. The RP ID will outlive your brand strategy. The sync fabrics traded hardware roots of trust for account roots of trust, mostly wisely, entirely without asking you. Attestation quietly exited the consumer stage. And the attacker moved to the one flow every deployment keeps and no specification covers: recovery. WebAuthn Level 3 and CTAP 2.2 hardened every wall of the house and left the same side door — because the side door is yours, not theirs.

The teams that get this right in 2026 are not the ones with the prettiest passkey sheet. They are the ones who read rpIdHash semantics before choosing a domain, who log backup-state flags they don’t yet act on, and who spent the sprint after launch making account recovery slow, loud, and identity-proofed. Passkeys move authentication’s hard problem; they do not remove it. It now lives in recovery — go meet it there before someone else does.


References


  1. Hodges, J., Jones, J.C., Jones, M., Kumar, A., and Lundberg, E. (eds.), “Web Authentication: An API for accessing Public Key Credentials — Level 2”, W3C Recommendation, 8 April 2021. https://www.w3.org/TR/webauthn-2/ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Bradley, J., Jones, J.C., Kumar, A., Liao, R., Lundberg, E., Sung, N. (eds.), “Web Authentication: An API for accessing Public Key Credentials — Level 3”, W3C Candidate Recommendation. https://www.w3.org/TR/webauthn-3/ ↩︎ ↩︎ ↩︎

  3. FIDO Alliance, “Client to Authenticator Protocol (CTAP)”, Proposed Standard, 15 June 2021. https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html ↩︎ ↩︎

  4. FIDO Alliance, “Client to Authenticator Protocol (CTAP) 2.2”, Proposed Standard, 14 July 2025. https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.pdf ↩︎ ↩︎

  5. Barbosa, M., Boldyreva, A., Chen, S., and Warinschi, B., “Provable Security Analysis of FIDO2”, Advances in Cryptology — CRYPTO 2021. https://eprint.iacr.org/2020/756 ↩︎ ↩︎

  6. Bindel, N., Cremers, C., and Zhao, M., “FIDO2, CTAP 2.1, and WebAuthn 2: Provable Security and Post-Quantum Instantiation”, IACR ePrint 2022/1029. https://eprint.iacr.org/2022/1029 ↩︎ ↩︎

  7. Mozilla Foundation, “Public Suffix List”. https://publicsuffix.org/ ↩︎

  8. W3C Web Authentication Working Group, “Related Origin Requests”, in WebAuthn Level 3. https://www.w3.org/TR/webauthn-3/#sctn-related-origins ↩︎ ↩︎

  9. Google Chrome team, “Allow passkey reuse across your sites with Related Origin Requests”, web.dev, 2024. https://web.dev/articles/webauthn-related-origin-requests ↩︎

  10. FIDO Alliance, “Metadata Service”. https://fidoalliance.org/metadata/ ↩︎

  11. Passkey Developer Community, “Passkey Provider AAGUIDs”, GitHub. https://github.com/passkeydeveloper/passkey-authenticator-aaguids ↩︎

  12. Apple Inc., “About the security of passkeys”, Apple Support; and “Apple Platform Security Guide — iCloud Keychain secure recovery”. https://support.apple.com/en-us/102195 ↩︎

  13. Birgisson, A., “Security of Passkeys in the Google Password Manager”, Google Online Security Blog, 12 October 2022. https://security.googleblog.com/2022/10/SecurityofPasskeysintheGooglePasswordManager.html ↩︎

  14. Schwartz, et al., “Device-Bound vs. Synced Credentials: A Comparative Evaluation of Passkey Authentication”, arXiv:2501.07380, January 2025. https://arxiv.org/abs/2501.07380 ↩︎

  15. FIDO Alliance, “FIDO Alliance Publishes New Specifications to Promote User Choice and Enhanced UX for Passkeys” (Credential Exchange Protocol and Credential Exchange Format working drafts), October 2024. https://fidoalliance.org/fido-alliance-publishes-new-specifications-to-promote-user-choice-and-enhanced-ux-for-passkeys/ ↩︎

  16. Toulas, B., “Retool blames breach on Google Authenticator MFA cloud sync feature”, BleepingComputer, September 2023. https://www.bleepingcomputer.com/news/security/retool-blames-breach-on-google-authenticator-mfa-cloud-sync-feature/ ↩︎

  17. National Institute of Standards and Technology, “Digital Identity Guidelines”, NIST SP 800-63-4 (and companion SP 800-63B-4, Authentication and Authenticator Management), July 2025. https://pages.nist.gov/800-63-4/ ↩︎

  18. Google, “So long passwords, thanks for all the phish”, Google Online Security Blog, May 2023. https://security.googleblog.com/2023/05/so-long-passwords-thanks-for-all-phish.html ↩︎