Skip to content

Passkeys: sign in without a password #320

Open

Description

@mforce

Note

Picking this up? Start here.

Do not start before the TOTP milestone's design (#1274) merges. This milestone reuses its login state machine and its AspNetUserTokens tenant guard (#1276).

Build in this order: #1282, then #1284, then #1285. #1283 runs in parallel with #1282, and its picks must be recorded before #1284 starts.

This epic holds the passkey half of #320. The TOTP half moved to epic #1273 in the "Two-factor authentication (TOTP)" milestone.

Scope decided by the maintainer: passkeys are a login method only, for now. A user signs in with a passkey instead of a password. A passkey is not a step-up factor and not a second factor, so step-up stays with the password or TOTP code from #1273. This differs from the body below, which asked for a step-up factor. That fits what .NET 10 ships: it treats a passkey as a primary factor.

Why a design slice comes first

Verified against origin/main at 497e29e and #320's survey of the .NET 10 assemblies:

  • .NET 10 Identity treats a passkey as a primary factor. Microsoft's docs say: "No built-in 2FA support: Passkeys are treated as a primary authentication factor, not as a second factor." That matches the login-only scope.
  • The entry points live on SignInManager. This repo registers AddIdentityCore without AddSignInManager() (src/Cluckwork.Api/Hosting/CluckworkIdentityServiceCollectionExtensions.cs:69-82), and login is written by hand (IdentityProvider.cs:34-161).
  • The passkey table is not mapped. IdentityPasskeyData appears only in the business-record exclusion list (src/Cluckwork.Infrastructure/Persistence/PlatformBusinessRecords.cs:23), and docs/schema/ has no passkey table.
  • Identity 10 does not check attestation statements by default. Fido2.NET (MIT, .NET Foundation) is the recommended complement, through a plugin interface Microsoft built for it.

Slices

Wave 1, design.

Wave 2, registration.

Wave 3, sign-in.

Each UI slice updates GLOSSARY, the Help page and the en/es/tl catalogs in its own PR, and attaches screenshots per AGENTS.md.


Parent: #244
Epic: #15
Severity: Medium (defense-in-depth follow-up)

Context

#308 ships the first step-up factor: current-password re-confirmation returning a short-lived, audience-limited step-up grant (bound to account+user, invalidated by security-stamp change and logout). That closes the stolen-access-token → durable-Owner path with no new infrastructure.

This issue tracks adding stronger step-up factors on top of that grant mechanism, so a compromised password alone can't satisfy step-up.

What to build (each its own slice)

  1. TOTP / authenticator app — enrollment flow, encrypted secret storage, verification. Satisfies the same step-up grant Require recent step-up authentication for privileged user administration #308 defines.
  2. WebAuthn / passkey — credential registration + attestation, assertion verification (phishing-resistant). Also satisfies the Require recent step-up authentication for privileged user administration #308 step-up grant.

Notes

Blocked by

#308 (defines the grant these factors satisfy).

Activity

  1. mforce commented on Sep 12, 2026

    @mforce
    OwnerAuthor

    Verified against the restored .NET 10 assemblies: both factors are substantially cheaper than this issue assumes, and one has a prerequisite the body does not mention.

    Checked the actual ref-pack XML docs at Microsoft.AspNetCore.App.Ref/10.0.12 — not recalled, not from release notes. Everything below is already on this repo's compile graph with zero new package references, because Microsoft.AspNetCore.Identity, Identity.Core and Identity.Stores are in-box shared framework for net10.0 and both projects already carry FrameworkReference Microsoft.AspNetCore.App. So the #684/#146 packaging tax for either slice is zero.

    Slice 1 — TOTP: confirmed present, not greenfield

    • AuthenticatorTokenProvider<TUser> with GenerateAsync/ValidateAsync
    • UserManager.GenerateNewAuthenticatorKey()
    • UserManager.VerifyTwoFactorTokenAsync(user, provider, token)
    • UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, number) — recovery codes are in-box too

    This repo calls none of them, and TwoFactorEnabled exists only as an unused EF column inherited from IdentityUser. So this slice is an enrolment UI, a QR code, and a verify endpoint that mints the existing #308 grant — not the "enrollment flow, encrypted secret storage, verification" build the body implies.

    One prerequisite the body does not name: Identity's token providers are data-protector-backed, and this repo has no Data Protection key ring configured at all — no AddDataProtection(), no PersistKeysTo*, nothing. With the framework default (ephemeral/local keys), TOTP secrets would not survive a restart or a second replica, which matters given #271/#543. Persisting the key ring is a prerequisite for this slice, and it is arguably a latent bug already, since AddDefaultTokenProviders() is registered and GeneratePasswordResetTokenAsync is called today.

    Slice 2 — Passkeys: present in .NET 10, but NOT usable as a second factor as written

    Confirmed present: IUserPasskeyStore<TUser>, UserPasskeyInfo, UserManager.{AddOrUpdatePasskeyAsync, GetPasskeysAsync, FindByPasskeyIdAsync, RemovePasskeyAsync}, IdentityPasskeyOptions, IPasskeyHandler<TUser>, PasskeyHandler<TUser>.

    Two problems for this issue specifically:

    1. Microsoft's own docs say passkeys are a primary factor, not a second one — verbatim: "No built-in 2FA support: Passkeys are treated as a primary authentication factor, not as a second factor." This issue wants passkeys as a step-up factor. That is not what ships; adapting a primary-factor implementation into a second-factor grant is the actual work of this slice.
    2. The entry points live on SignInManager, and this repo uses AddIdentityCore without AddSignInManager() — SignInManager is never registered and login is hand-rolled in IdentityProvider.LoginAsync. So this slice must first wire SignInManager into a login path that deliberately does not use it.

    Also worth knowing: Fido2.NET is still the recommended complement, not obsolete. Its README has a section titled ".NET 10 Passkeys vs this library?" written by a maintainer who worked with Microsoft on the in-box implementation — Identity 10 does not validate attestation statements by default, and Microsoft built a plugin interface so the two combine. MIT, .NET Foundation, actively maintained.

    Why this matters now

    #788 builds an OAuth authorization server where approving an assistant requires step-up. Every factor added here strengthens that approval, because #308's grant is the shared contract. TOTP being nearly free makes slice 1 look considerably more attractive than its tier-3 priority suggests; slice 2 is genuinely more work than the body implies, not less.

    Suggest splitting the two slices' priorities rather than carrying one label for both.

  2. added and removed
    priority:tier3Real product weight, real cost
    on Sep 13, 2026
  3. added
    epicPhase-level tracking issue
    epic-320Passkeys (WebAuthn) as an authentication factor (#320)
    and removed
    sliceThin vertical work item
    on Oct 12, 2026
  4. added this to the Passkeys milestone on Oct 12, 2026
  5. changed the title [-]Add TOTP and WebAuthn/passkey as step-up factors (follow-up to #308)[/-] [+]Passkeys: sign in without a password[/+] on Oct 12, 2026
  6. mforce commented on Oct 12, 2026

    @mforce
    OwnerAuthor

    Important

    Amended 2026-10-12. This issue is now split across two milestones. The body stays as written, for history.

    Moved out. The TOTP half (item 1 of "What to build") is now epic #1273 in the "Two-factor authentication (TOTP)" milestone. The maintainer widened it: TOTP is both a step-up factor that mints the #308 grant and a second factor at login. Its slices are #1274 to #1281.

    Stays here. The WebAuthn/passkey half (item 2) is in the "Passkeys" milestone. This issue is now that milestone's epic, and the checklist above the original body tracks its slices. The maintainer narrowed it: a passkey is a login method only, for now, not a step-up factor. That matches .NET 10, which treats passkeys as a primary factor. A design slice, #1282, comes first, because the entry points sit on SignInManager, which this repo does not register. The survey comment above has the details.

    Prerequisite done. The Data Protection key ring the survey named shipped in #794.

    Not met as written. The body's "encrypted secret storage" becomes a decision in #1274, not an assumption. The maintainer resolved "Owner should be able to require a specific factor" this way: #1281 makes two-factor mandatory for every Owner. A setting that requires it for Managers is deferred.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:apiAPI/endpoint layerarea:frontendReact/Vite web clientepicPhase-level tracking issueepic-320Passkeys (WebAuthn) as an authentication factor (#320)priority:tier4Deferred or speculative

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions