Repository navigation
Description
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 registersAddIdentityCorewithoutAddSignInManager()(src/Cluckwork.Api/Hosting/CluckworkIdentityServiceCollectionExtensions.cs:69-82), and login is written by hand (IdentityProvider.cs:34-161). - The passkey table is not mapped.
IdentityPasskeyDataappears only in the business-record exclusion list (src/Cluckwork.Infrastructure/Persistence/PlatformBusinessRecords.cs:23), anddocs/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.
- Passkey slice 1: design record for passkey sign-in #1282 · L decision record for passkey sign-in:
SignInManageror not, attestation policy, Fido2.NET or not, and how passkey sign-in meets the TOTP rules. Needs 2FA slice 1: design record for TOTP at login and as a step-up factor #1274. - Passkey slice 2: mockups for passkey registration and sign-in #1283 · M mockups. Can start with Passkey slice 1: design record for passkey sign-in #1282.
Wave 2, registration.
- Passkey slice 3: register, name and remove passkeys, with Owner removal and recover-admin #1284 · L register, name and remove passkeys, with the passkey table and its tenant guard, Owner removal, and
recover-adminclearing them. Needs Passkey slice 1: design record for passkey sign-in #1282, Passkey slice 2: mockups for passkey registration and sign-in #1283, 2FA slice 3: tenant guard on AspNetUserTokens and TOTP secret storage #1276, 2FA slice 4: TOTP enrolment on the Account page, with recovery codes #1277, 2FA slice 7: turn off and reset TOTP, by the user, an Owner, or recover-admin #1280.
Wave 3, sign-in.
- Passkey slice 4: sign in with a passkey instead of a password #1285 · L sign in with a passkey instead of a password. Needs Passkey slice 3: register, name and remove passkeys, with Owner removal and recover-admin #1284, 2FA slice 5: TOTP code as a second step at login #1278.
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)
- 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.
- 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
- Reuse Require recent step-up authentication for privileged user administration #308's step-up grant contract (short-lived, audience-limited, account/user-bound, stamp/ logout invalidation) — these are new factors that mint the same grant, not a parallel mechanism.
- Keep break-glass recovery separate (per Require recent step-up authentication for privileged user administration #308/Deploy: sole-Owner account recovery has no supported path (break-glass procedure) #265).
- Owner should be able to require a specific factor for privileged user-admin once >1 factor exists.
Blocked by
#308 (defines the grant these factors satisfy).
Activity
mforce commentedon Sep 12, 2026
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, becauseMicrosoft.AspNetCore.Identity,Identity.CoreandIdentity.Storesare in-box shared framework fornet10.0and both projects already carryFrameworkReference Microsoft.AspNetCore.App. So the #684/#146 packaging tax for either slice is zero.Slice 1 — TOTP: confirmed present, not greenfield
AuthenticatorTokenProvider<TUser>withGenerateAsync/ValidateAsyncUserManager.GenerateNewAuthenticatorKey()UserManager.VerifyTwoFactorTokenAsync(user, provider, token)UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, number)— recovery codes are in-box tooThis repo calls none of them, and
TwoFactorEnabledexists only as an unused EF column inherited fromIdentityUser. 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(), noPersistKeysTo*, 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, sinceAddDefaultTokenProviders()is registered andGeneratePasswordResetTokenAsyncis 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:
SignInManager, and this repo usesAddIdentityCorewithoutAddSignInManager()—SignInManageris never registered and login is hand-rolled inIdentityProvider.LoginAsync. So this slice must first wireSignInManagerinto a login path that deliberately does not use it.Also worth knowing:
Fido2.NETis 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.
docs(auth): record the OAuth 2.1 decision for MCP authentication (#801)
[-]Add TOTP and WebAuthn/passkey as step-up factors (follow-up to #308)[/-][+]Passkeys: sign in without a password[/+]mforce commentedon Oct 12, 2026
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.