Enrollment Is the Weakest Link in Passwordless
The ticket had been reopened three times, which is what made it interesting.
A finance user's account had been compromised in February. The response was textbook and fast: password reset, all sessions revoked, TOTP seed invalidated and re-enrolled, mailbox rules audited, forwarding removed. Two weeks later the same account exfiltrated a payroll export again, from a residential IP in a country the user had never visited. Same response, executed faster. Three weeks after that, again.
The fourth investigation started from a different question — not "how did they get in this time" but "what does this account actually have on it" — and the answer was five credentials. The user knew about two of them. One of the other three had been registered at 04:12 on a Sunday in February, eleven minutes after the original compromise, from the attacker's session, before anyone knew there was an incident. It was a passkey. It was synced to a credential store that the attacker controlled. It was origin-bound, non-replayable and phishing-resistant, and it survived every remediation the team had performed, because every runbook they owned was written in the vocabulary of passwords and one-time codes. Nothing in "reset the password and revoke the sessions" removes a WebAuthn credential.
The attacker got in once, using a password from a stuffing list. They left holding a stronger credential than the legitimate user had — one that could not be phished away from them, could not be guessed, could not be found in a breach corpus, and would not be evicted by any control the organisation had built.
Phishing resistance is a property of the authentication ceremony, not of the account. WebAuthn guarantees that a credential, once bound, cannot be used at the wrong origin or replayed elsewhere — and says nothing whatsoever about how it came to be bound in the first place. Every passkey deployment therefore has a second, quieter authentication path running alongside the strong one: the path by which new authenticators get attached to accounts. That path is almost always built out of email links, existing passwords, one-time codes and support agents, and it is the actual security boundary of the account. The credential's assurance is capped by the channel that provisioned it, and most deployments have never measured, logged, or even named that channel.
This piece is about that channel. What the bootstrap paths actually are and how each one fails in ways specific to enrollment rather than login; why an attacker who enrolls is in a categorically better position than an attacker who merely logs in; why the trust chain behind a synced passkey ends somewhere you don't control; what enterprise deployments can do that consumer ones can't; and a way to decide, per path, what you are entitled to claim.
Two neighbours to place first, because I'm building on them rather than repeating them. Passkey Account Recovery Is the Whole Problem covers the case where the user has lost their credential and needs a way back — the evidence tiers, the delay-plus-notification control, the scoped recovery session. The Hardest Problem in Identity Isn't Authentication. It's Recovery. generalises that to every credential type. Recovery is a subset of what I'm describing: it is enrollment triggered by loss. The larger set includes day-one signup, workforce onboarding, the second device, the new laptop, the silent addition of a credential to an account that already had three, and the helpdesk call that isn't about loss at all. Those share a threat model that recovery-focused thinking misses, because they don't feel like security events. They feel like setup.
The scoping error in "phishing-resistant"
Here is the claim as it appears in a security questionnaire: authentication to this system is phishing-resistant. Here is what WebAuthn actually provides, stated precisely: an assertion is bound to the relying-party ID by the authenticator, and the client refuses to produce one for an origin that doesn't match, so a credential registered for bank.example cannot be exercised by bank-example.co. That is a strong and genuinely novel property. It is also scoped to a single operation — proving possession of a credential that already exists.
Nothing in the specification addresses the antecedent question of whether that credential should exist on that account. navigator.credentials.create() is called inside a session your application has already decided to trust. The whole ceremony is downstream of an authorisation decision your code made before the WebAuthn API was ever invoked.
So the honest formulation is a chain, not a property:
An account's resistance to takeover is the minimum over every path that can result in an authenticator being bound to it.
Which reads like the familiar min(credential, recovery) formula, and is deliberately broader. Recovery is one such path. So is initial signup. So is "add another device" in settings. So is the admin console where an operator can attach a credential on a user's behalf. So is the SCIM-provisioned account that arrives pre-created and waits to be claimed by whoever clicks the invitation link first. Enumerate those paths on a whiteboard for a real system and the count is usually five to eight, several of which no one has looked at since they were written, at least one of which nobody present knew existed.
There is a second scoping error nested inside the first, and it is the one I see catch experienced engineers.
User verification at registration proves something local and nothing remote. When a user registers a passkey, the authenticator performs user verification — a device PIN, Touch ID, Windows Hello — and sets the UV bit in authenticatorData. It is enormously tempting to read that as "a human, verified, did this deliberately." What it actually asserts is: the entity in physical control of this authenticator satisfied the authenticator's own local verification policy. If the attacker is holding their own phone and enrolling their own biometric against your session, UV is 1. It is always 1. The bit tells you the authenticator was unlocked; it tells you nothing about whose account it was unlocked against. The full mechanics of the flag are in Under the Hood of WebAuthn; the point here is narrower and behavioural — UV=1 on a registration response is not evidence of legitimacy, and any risk logic that treats it as such is reading a local fact as a remote one.
The same applies to excludeCredentials. It prevents a user from accidentally registering a duplicate on an authenticator they've already used. It does absolutely nothing against an attacker registering a new authenticator, because a new authenticator is precisely what the exclusion list doesn't contain.
The bootstrap paths, and how each one fails at enrollment specifically
Every one of these is discussed somewhere as a login mechanism or a recovery mechanism. What follows is the enrollment-specific failure mode for each — the way it breaks differently when its output is a permanent credential rather than a session.
The emailed link
The default. It inherits mailbox security, which has been said often enough. Three things about it are specific to enrollment and much less discussed.
Enrollment links have longer lifetimes than login links, because onboarding is slow. A magic-link login is valid for ten minutes because the user is standing at the keyboard. An invitation to join a workspace is valid for seven days, because the recipient might be on holiday and a 24-hour expiry generates support tickets. Both are correct, and the result is a bearer token with account-creation authority sitting in a mailbox for a week — in the sender's sent folder, in the corporate archive, in whatever backup the mail platform keeps, and in the URL history of any intermediary that touched it.
Link URLs get followed by things that aren't people. Mail security products detonate URLs in a sandbox before delivery. Corporate proxies prefetch. Chat clients unfurl previews. If your enrollment link performs a state change on GET — consuming the token, establishing a session, marking the invitation accepted — then some fraction of your enrollments are being initiated from a datacentre by an automated scanner, before the user has seen the mail. The benign version is a support ticket that says "the link says it's already been used." The non-benign version is that for a window, an authenticated session for a brand-new account exists somewhere that is not the user's browser. This is the same generator of weirdness that fills impossible-travel alert queues with logins from Ashburn, Virginia, and it is much more consequential when what's on the other end of the link is the ability to bind a credential rather than a session. The mitigation is unglamorous and effective: the link lands on a page, the state change happens on a POST that requires an interaction, and the token is consumed only at that point.
Forwardability is a feature of email and a hole in your enrollment. "Can you set this up for me" is a sentence real people say to their spouse, their assistant, their IT-literate nephew, and — with a little help — to a stranger claiming to be from the helpdesk. The credential gets bound to whoever completed the ceremony, and the system records that as the account owner. A shared password could at least be changed. A bound authenticator held by someone else is a durable co-owner of the account.
An existing password
Extremely common as the passkey upgrade path: the user logs in with their password, sees a prompt, enrols a passkey. Convenient, high-converting, and it imports the exact weakness the project exists to retire.
The damage isn't symmetric with a normal password login, though, and this is the part worth internalising. A password login yields a session — bounded, revocable, and visible in a session list. A password login that enrols a passkey yields persistence. The password can now be reset by anyone, including the legitimate owner during an unrelated spring clean, and the attacker's access is unaffected. Credential stuffing is an economics game played at scale for low per-account value (the arithmetic is here); the ability to convert one successful stuffed login into a permanent, phishing-resistant foothold changes the value of a hit substantially, and it does so silently.
If you offer password-to-passkey upgrade — and you should, it's the highest-yield adoption path there is — then the enrollment must be gated on more than the password. A second factor, a recognised device, or both. Upgrading a credential is not a routine operation performed with the weak credential; it is the moment the weak credential's authority is being permanently extended.
SMS and email one-time codes
The failure mode is the well-known one and I won't re-derive it — Why SMS OTP Won't Die gives the balanced version, including the cases where it genuinely beats the alternative of nothing.
The enrollment-specific wrinkle is social transferability under a pretext that makes sense. Users have been trained, correctly, to be suspicious of "read me the code so I can access your account." They have not been trained to be suspicious of "we're setting up your new secure sign-in, I'll walk you through it, read me the code and then approve the prompt on your phone." The second script is describing the thing that is genuinely about to happen. The user is not being asked to bypass security; they are being asked to participate in a security improvement. Enrollment pretexts have a legitimacy that recovery pretexts don't, and support-desk and vishing operators discovered this some time ago.
An existing session
This is the one that produced the story at the top, and I think it's now the most important bootstrap path in the consumer and workforce internet, because of where attackers have moved.
Most applications treat "you have a valid session" as sufficient authorisation to add a credential. It is the path of least friction, it is what the user expects, and it is a transitive trust chain: the session was created by some earlier authentication, whose evidence you may no longer be recording, possibly weeks ago, possibly by a mechanism you have since deprecated. Trust on first use, chained forward indefinitely. Every credential in the system eventually traces back through a graph of these to something — and if you can't reconstruct that graph, you can't state your own security posture.
The reason this matters more each year is infostealer malware. Modern commodity stealers do not primarily hunt for passwords; they exfiltrate session cookies and token stores, precisely because a live session bypasses whatever authentication was in front of it, MFA included. The attacker replays the session, lands inside an authenticated context, and the single highest-value action available to them is not data exfiltration — data can be re-secured. It is enrollment. One navigator.credentials.create() converts a stolen cookie with a finite lifetime into a credential with none.
sequenceDiagram
participant A as "Attacker"
participant V as "Victim device"
participant RP as "Your app"
participant IR as "Incident response"
A->>V: "Infostealer exfiltrates<br/>session cookie"
A->>RP: "Replay session"
RP-->>A: "Authenticated context"
A->>RP: "Register new passkey<br/>(UV=1, own authenticator)"
RP-->>A: "Credential bound, no notification"
IR->>RP: "Reset password"
IR->>RP: "Revoke all sessions"
IR->>RP: "Re-enroll TOTP"
Note over RP: "Attacker credential untouched"
A->>RP: "Sign in with passkey"
RP-->>A: "Phishing-resistant login,<br/>no anomaly, no alert"
Read the bottom half of that diagram as a statement about your runbook rather than about WebAuthn. The technical failure is that credential enumeration and removal is missing from the containment steps. The structural failure is that the enrollment event produced no record anyone could act on: no notification to the user, no distinct audit entry, no provenance metadata tying the new credential to the session that created it. When the fourth investigation finally found it, it was found by eye.
The cross-device QR flow, and a distinction people get wrong
This one deserves care because there are two different things wearing the same visual language, and conflating them produces bad security decisions in both directions.
The FIDO hybrid transport — what happens when you scan a QR code shown by a desktop browser using your phone, to use a passkey stored on that phone — is not what it looks like. It looks like the OAuth device-authorisation flow, where a code displayed on one screen is entered on another and a remote attacker can relay it (the failure mode is worked through here). Hybrid is architecturally different: the QR code carries key material, and the phone, having scanned it, transmits a Bluetooth Low Energy advertisement that the desktop client must actually receive before the encrypted tunnel is established. That BLE step is a proximity requirement enforced by the client platform, and it is the reason a remote attacker cannot simply send you a QR code and have you scan it from another continent. Relaying a hybrid QR does not work the way relaying a device code does. This is a genuinely good piece of design and it is underappreciated, mostly because it is invisible.
A "scan this to add a device" QR code that your product implemented itself — a deep link containing an enrollment token — has none of those properties. It is a bearer token rendered as a picture: screenshottable, photographable over a shoulder, relayable over a video call, embeddable in a phishing page. The user experience is identical to the hybrid flow, and users have been thoroughly trained by the hybrid flow that scanning a QR code during sign-in is normal and safe.
Use the platform's hybrid transport where it exists rather than inventing a QR-shaped token, and if you must invent one, be clear-eyed that you have built a bearer credential whose ergonomics encourage exactly the behaviour that makes it exploitable.
The helpdesk
The recovery article treats the support agent as a system component with a threat model, and that analysis applies unchanged, so I'll add only the enrollment-specific part.
An agent-assisted enrollment usually produces a credential indistinguishable from a self-service one. The credential table records a credential ID, a public key, a signature counter, maybe an AAGUID. It does not record "this was bound during ticket 44192 by agent K. under the manager-approval exception." So the enrollment path — the thing that determines what the credential is actually worth — is discarded at the moment of creation, and every subsequent risk decision treats all credentials as equally trustworthy. When you later want to ask "how many live credentials in this estate were bootstrapped by a human agent overriding the normal flow," the data does not exist.
Enterprise pre-enrollment
The strongest of the set, and the reason enterprise deployments can reach a place consumer ones structurally cannot. A managed device carries a certificate or a device-management identity that the enrollment endpoint can verify; a one-time enrollment code can be delivered to that device rather than to a mailbox; a sponsor with an HR record can vouch in a system that already knows both parties; a person can walk to a desk. Attestation exists for the case where you need to enforce which hardware is being bound, though for most organisations the collect-don't-gate posture is the right one, and the BE flag answers "is this credential device-bound" far more cheaply than a metadata pipeline does.
The failure mode here isn't the mechanism. It's the exception path. Every enterprise pre-enrollment scheme has a branch for the contractor with no managed device, the executive whose laptop is in transit, the new hire starting remotely from another country, and the person whose enrollment failed for a reason nobody can diagnose. That branch is an emailed link and a phone call, it handles single-digit percentages of enrollments, and it is where a targeted attacker will go, because it is the only door with a human on the other side of it. Your enrollment assurance is the assurance of the exception path, weighted by nothing, because an attacker chooses.
Why enrollment deserves stricter treatment than authentication
The instinct in most products is the reverse: authentication is hardened because it's the front door and it's under constant automated attack; enrollment is treated as configuration, part of setup, a settings screen. Three properties invert that.
It's permanent, and authentication isn't. A successful authentication produces a session with a lifetime, a revocation mechanism, and a visible entry in a list the user can review. A successful enrollment produces a credential that persists until someone deliberately removes it, and the population of people who periodically audit their registered authenticators is approximately the population who read privacy policies.
It's rare, which makes it cheap to gate and easy to monitor. Users authenticate hundreds of times a year and enroll perhaps twice. Any friction you add to enrollment is amortised over almost nothing, and any anomaly detection you build on enrollment events runs against a tiny, high-value stream instead of the firehose. This is the single best cost-benefit ratio available anywhere in an identity system, and it is routinely spent on making the flow smoother instead.
It's asymmetric in the attacker's favour by default. Adding an authenticator does not remove the old ones, so nothing breaks for the legitimate user, so nothing prompts them to investigate. The victim's experience of a successful attacker enrollment is no experience at all. Compare a password change, which locks the user out and generates a support call within a day. Silence is the attacker's whole advantage, and silence is a default we chose.
Which gives the design principle, and it's a short one: any operation that binds a new authenticator should require at least the assurance level of the strongest authenticator already on the account, and should be loud. This is essentially the position NIST's digital identity guidance arrives at from a different direction — that binding an additional authenticator is an event requiring authentication at the account's existing level and a notification to the subscriber over an independent channel. It is one of the least-implemented requirements I know of in a widely-cited standard, and it is not hard.
Four concrete mechanisms follow from it.
Step up before you enroll, using what the account already has. If the account holds a passkey, adding another should require exercising the existing one, not a password or an emailed code. This sounds obvious and is frequently not what happens, because the "add a device" flow was written before the account had strong credentials on it and nobody revisited the ordering. Step-up authentication is the mechanism; the enrollment-specific rule is that the step-up must clear the maximum bar the account can currently satisfy, not a fixed one.
Notify on every binding, over a channel the enrolling session cannot change. And notify with content that permits action: the AAGUID's friendly name if you have it, whether the credential is syncable, the approximate location and device of the enrolling session, and a one-click path to revoke it. "A new sign-in method was added to your account" with no detail and no action is a notification that trains users to ignore notifications. The independence requirement is the load-bearing part — if the same session that just enrolled a credential can also change the notification address, the attacker's first move is to cut the wire, and the sequence is: enroll, redirect notifications, wait.
Season the credential before you trust it. This is the mechanism I'd most like to see become standard, and it is distinct from the scoped-recovery-session idea in the recovery article: that scopes a session, this scopes the credential, permanently attaching a maturity to it. A newly bound authenticator can sign you in. For some window — 24 hours, seven days, chosen against your stakes — it cannot authorise the operations that make takeover profitable: changing the recovery email, removing other authenticators, adding a payout destination, exporting data, elevating privileges. Banks have done exactly this with new payees for twenty years and nobody finds it outrageous. It costs the legitimate user nothing in the overwhelmingly common case, because the legitimate user enrolled a second device in order to sign in with it, not in order to immediately delete their other one.
Note the second item in that list carefully. The most important thing a probationary credential must not be able to do is evict the credentials that came before it. An attacker who enrolls and then immediately removes the victim's authenticators has converted a shared account into an exclusively-held one, and has done so using the very credential whose legitimacy is least established. Ordering matters enormously here: a system that allows credential removal on the authority of the newest credential has no defence in depth at all, while a system that requires an older credential — or a delay plus notification — to remove any credential turns the attacker's fastest path into their slowest.
Treat enrollment velocity and enrollment mix as security signals. Three credentials bound to one account in ten minutes is interesting. A credential bound within minutes of a session that itself began with an anomalous login is interesting. A tenant where the ratio of agent-assisted to self-service enrollments jumps in a week is very interesting. None of these are computable if enrollment isn't a distinct, typed, first-class event in your data model — which brings us to the thing most systems are missing.
The credential inventory problem
Ask a running system: how many authenticators exist on this account, what kind are they, who bound each one, on what evidence, and from where?
The recovery article makes the point that users cannot answer the first part of that question. The more awkward finding is that most relying parties cannot answer the rest of it either, because the schema was designed around what WebAuthn verification needs rather than what security operations needs. A typical credentials table holds the credential ID, the public key, the signature counter, a nickname, and a creation timestamp. That is sufficient to verify assertions forever and insufficient to answer a single question during an incident.
The minimum useful enrollment record is not large:
| Field | Why it earns its place |
|---|---|
| Bootstrap path | Which of your enumerated paths bound this — password step-up, existing passkey, invitation link, agent-assisted, device-managed. The single most important field and the one nobody stores. |
| Authorising evidence | The session ID and, transitively, the authentication event that created it; what factors that authentication used. Lets you replay the trust chain rather than guess at it. |
| Enrolling context | IP, ASN, user agent, device fingerprint at the moment of binding. Not for real-time blocking — for the investigation six weeks later. |
| AAGUID | Recorded, not enforced. Gives you a model name for notifications and a distribution you can spot anomalies in. |
| BE / BS flags | Whether the credential is syncable and currently synced. This determines what the credential's loss and theft models actually are, and BS changes over time, so re-record it on each assertion. |
| Transports | internal, hybrid, usb, nfc — coarse, free, and useful for distinguishing "this account's credentials all live on one phone" from "these are three independent devices." |
| Approver, where applicable | For agent-assisted or delegated enrollment: who, under what policy, with what ticket. |
Two design consequences fall out of storing this.
The first is that you can finally compute the number that describes your deployment's real security posture, which is not passkey adoption. It's the enrollment mix: what fraction of currently-live credentials were bootstrapped by each path. A system where 94% of live passkeys trace back to an emailed link is a system whose authentication assurance is mailbox assurance, no matter what the login screen does. That number is knowable, it is a single query once the field exists, and I have never seen it on a dashboard.
The second is that credential removal becomes a manageable operation rather than an archaeological one. Containment for a compromised account should include enumerating authenticators, presenting them to a human with enough context to recognise which are foreign, and revoking with the same ceremony as any other privileged action. Do that as a designed capability and the story at the top of this article ends at the first investigation instead of the fourth.
This is also the point at which enrollment stops being a branch inside a controller and becomes what it should have been: a first-class, permissioned, audited operation in the identity model, with the same treatment as any other administrative action — the case for which is made more generally in delegated administration and in immutable audit logs as an engineering feature. Both of those arguments are usually made about operators acting on other people's accounts. The observation here is that a user binding a credential to their own account is the same class of operation, and modelling it as such gets you the audit trail, the permission check, and the notification for free.
The chain doesn't stop at your boundary
There's a further step that most threat models omit, and it's the one that makes "phishing-resistant" hardest to defend honestly.
A synced passkey does not live on a device. It lives in a credential store — iCloud Keychain, Google Password Manager, a third-party manager — which is itself an account, with its own authentication, its own enrollment, and its own recovery. Your user's ability to authenticate to you is downstream of all three. So is an attacker's.
flowchart TD
ACC["Your account"] --> PK["Passkey<br/>(BE=1, BS=1)"]
PK --> STORE["Platform credential store"]
STORE --> PA["Platform account"]
PA --> DEV["Device unlock<br/>(6-digit passcode)"]
PA --> PREC["Platform account recovery"]
PREC --> MAIL["Recovery mailbox"]
PREC --> SUP["Platform support process"]
ACC --> ENR["Your enrollment paths"]
ENR --> LINK["Emailed link"]
ENR --> PWD["Existing password"]
ENR --> HD["Helpdesk"]
LINK --> MAIL
Every leaf on that graph is a way in, and the account's real strength is the weakest of them. The two branches are different in kind, though, and the difference is the actionable part: the left branch you inherit and cannot improve; the right branch you own entirely.
On the inherited side, be accurate rather than alarmed. A synced credential behind a six-digit device passcode is not the disaster it sounds like, because that passcode is enforced by hardware with an attempt counter and escalating delays — the reason a PIN is a perfectly reasonable memorised secret while an eight-character password is a bad one, an argument made properly in Stop Asking Users to Remember Secrets. Rate limiting in silicon changes the arithmetic completely. The exposure isn't the passcode; it's the recovery path around it, which is a process operated by a vendor's support organisation at a scale that guarantees some error rate, and which no amount of engineering on your side can strengthen. Apple and Google have invested more in that problem than you will, and it is still a dependency you accepted without a contract.
The practical response is not to ban synced credentials — for consumer products they are the best thing that has happened to device loss, and refusing them means refusing the population that has no other option. It is to know which credentials are synced, using the BE and BS flags, and let that inform policy where policy matters: device-bound credentials for administrative roles, synced credentials welcome everywhere else, and a written acknowledgement that for the synced population, the account's ceiling includes a platform vendor's recovery desk. That belongs in the questionnaire answer, not a footnote.
Enterprise and consumer are different problems, and only one of them is solvable
The gap here is wider than for almost any other identity question, and the mistake teams make is applying consumer patterns inside the enterprise, where much stronger options are available and sitting unused. The general shape of the divide is covered in Enterprise Identity Is Not Consumer Authentication; this is the enrollment-specific version.
In the enterprise, you have three assets a consumer product doesn't. An authoritative HR record that exists before the account does — the person was hired, there is a start date, a manager, a role. A device fleet you own, enrolled in management, capable of proving its own identity to your endpoints. And a physical option: a desk, a colleague, a face, an office with a spare hardware key in a drawer.
Those turn enrollment into a provisioning problem with a known answer. The person's identity was proven at hire, by a process involving documents and a human, which is stronger evidence than anything an authentication system can generate. The correct design carries that proofing forward: bind the first credential on a managed device, using an enrollment secret delivered through a channel tied to the employment relationship rather than to an email address, with the manager or a sponsor as the approver of record. Then bind a second one immediately, in the same session, because the moment you have a verified human in front of a working credential is the cheapest moment you will ever have to eliminate the single-credential problem — and the second credential is what converts every future device-loss event from a recovery incident into an ordinary login.
Note what this does to the recovery question, and it's the observation I'd most want an enterprise architect to take away: onboarding enrollment and post-loss re-enrollment are the same operation with different triggers. If your platform models "a permissioned operator, or a policy-approved self-service flow, binds a credential to a user, audited" as one thing, you have built recovery for free, with the delegation boundary and the audit trail already attached. Most enterprise IAM builds these as two unrelated features with two unrelated levels of scrutiny, and the weaker of the two sets the account's real assurance.
In a consumer product, you have an email address and nothing else. There is no prior proofing to carry forward, because there was never a moment when anyone established who this person is. This is worth being blunt about, because it bounds what's achievable: you cannot identity-proof someone back into an account that never knew who they were. Document verification, the strongest consumer option, is answering the question "is this the person in this passport" — which only helps if you captured the passport at signup, which almost no consumer product does, and which carries a real cost, a real vendor privacy footprint, and a false-rejection rate that lands hardest on the people least able to escalate.
So the achievable consumer goal is different in kind. It isn't "make enrollment as strong as the credential" — you can't, not from an email address. It's make enrollment visible, slow when weak, and reversible: every binding notified over an independent channel, weak-evidence bindings delayed and probationary, no new credential able to evict an old one, and a genuine effort to get a second strong credential onto the account at the one moment the user is provably holding two devices. That is a materially different security posture from where most consumer passkey deployments sit today, and none of it requires proofing anyone.
What you can honestly claim, and how to decide
The counterpoint I owe you is that none of this makes enrollment safe, and a piece that ends with a checklist implying otherwise would be dishonest. You cannot make the bootstrap perfect, because the bootstrap is where a system that knows nothing about a person has to start knowing something, and no cryptography addresses that. What you can do is close the gap between what the channel is worth and what you say it's worth, and make an explicit decision about the residual.
The decision is per path, and it has three parts: what evidence does this path require, what is it therefore worth, and what may a credential bound through it do?
| Bootstrap path | Assurance ceiling it sets | What that entitles you to claim | Reasonable posture for the credential it produces |
|---|---|---|---|
| Existing passkey on the account | Equal to the credential itself | Phishing-resistant, end to end | Full authority immediately |
| Managed-device enrollment with sponsor approval | Employment proofing plus device identity | Phishing-resistant, with a named exception path | Full authority; audit the sponsor |
| In-person / video with a known colleague | As strong as your onboarding process | Phishing-resistant for the workforce population | Full authority |
| Password plus a second factor | The second factor's strength | Strong, not phishing-resistant | Probationary window; cannot remove other credentials |
| Existing session | The authentication that created it — which you must be able to name | Whatever that was, aged | Probationary; require re-authentication if the session is older than a threshold |
| Emailed link or code | Mailbox security, plus the mail provider's own recovery | "Removes credential stuffing and reuse" — not phishing resistance | Probationary, delayed, notified, cannot evict |
| SMS code | Carrier account security | The weakest thing on this list; sometimes still the right answer for reach | Probationary and delayed; never sole evidence for eviction |
| Agent-assisted | The agent's verification script, socially engineerable | Nothing, unless the script is genuinely strong and audited | Probationary, second approver above a value threshold, recorded provenance |
Read that table as a constraint on your marketing copy and your questionnaire answers as much as on your architecture. If the bottom two rows account for most of your live credentials, then the accurate sentence is "login is phishing-resistant; the account is protected at the level of the user's mailbox" — which is still a large improvement over where you were, because it eliminates credential stuffing, reuse, and offline cracking. Claim that. It's true, it's substantial, and it survives contact with an auditor who reads the enrollment flow.
The sequence I'd actually follow, given a running system:
Enumerate the paths first, on a whiteboard, from the code rather than the documentation. Grep for every call site that creates a credential record. The count will be higher than the team's estimate, and the surprise entries — an internal admin tool, a partner-provisioning endpoint, a migration script that still runs — are disproportionately likely to be the weak ones, because they were written for a deadline and never reviewed as authentication surface.
Then instrument, before changing anything. Add the bootstrap-path field and backfill what you can. Compute the enrollment mix. This is a week of work and it converts an argument about risk appetite into a number, which is the only thing that reliably moves a roadmap.
Then close the loop that costs nothing: notify on every binding, over an independent channel, with revoke-in-one-click and enough detail to act on. No user-visible friction, no conversion cost, and it's the control that would have caught the incident in the opening paragraphs on the same night rather than in the fourth week.
Then order the paths and gate accordingly. Step-up to the account's strongest existing factor before any enrollment. Probation for anything bootstrapped below that bar. And the ordering rule that matters most: no credential may remove another credential during its own probation.
Then, and only then, argue about the exception path — because that is where an attacker who has read this far will go, and it is the one place where the honest answer is that you are trading availability against security with no clever option available. Decide where you sit, write it down, audit it.
The uncomfortable summary is that passwordless didn't remove the weak credential. It made the weak credential rare enough to stop thinking about, which is different, and more dangerous, because a path exercised twice per user per lifetime attracts approximately no scrutiny while carrying the full authority of the account. Phishing resistance is real and worth having. It just isn't a property you can attach to an account. It's a property of a ceremony, and every ceremony has a first one.
Which is the question worth leaving with, and it's cheap to ask of any system: what is the weakest thing that can result in a new authenticator on this account — and would you know, tomorrow morning, if it had happened last night?