Nedo.AspNet.Authentication.Local.Passkeys
2.0.3
See the version list below for details.
dotnet add package Nedo.AspNet.Authentication.Local.Passkeys --version 2.0.3
NuGet\Install-Package Nedo.AspNet.Authentication.Local.Passkeys -Version 2.0.3
<PackageReference Include="Nedo.AspNet.Authentication.Local.Passkeys" Version="2.0.3" />
<PackageVersion Include="Nedo.AspNet.Authentication.Local.Passkeys" Version="2.0.3" />
<PackageReference Include="Nedo.AspNet.Authentication.Local.Passkeys" />
paket add Nedo.AspNet.Authentication.Local.Passkeys --version 2.0.3
#r "nuget: Nedo.AspNet.Authentication.Local.Passkeys, 2.0.3"
#:package Nedo.AspNet.Authentication.Local.Passkeys@2.0.3
#addin nuget:?package=Nedo.AspNet.Authentication.Local.Passkeys&version=2.0.3
#tool nuget:?package=Nedo.AspNet.Authentication.Local.Passkeys&version=2.0.3
Nedo.AspNet.Authentication.Local.Passkeys
Passkeys / WebAuthn (FIDO2) sign-in for Nedo.AspNet.Authentication.Local. Passwordless or step-up sign-in via Touch ID, Windows Hello, YubiKey, or phone-as-key (hybrid transport / cross-device QR).
Built on Fido2NetLib.
Install
dotnet add package Nedo.AspNet.Authentication.Local.Passkeys
Requires that Nedo.AspNet.Authentication.Local is already wired up (AddLocalAuthentication + AddLocalAuthStore).
Quickstart
using Nedo.AspNet.Authentication.Local.Passkeys;
builder.Services.AddPasskeys(opts =>
{
// Bare host — no scheme, no port. MUST match the SPA's host exactly,
// or browsers will reject every assertion as "not for this origin".
opts.ServerDomain = "app.example.com"; // dev: "localhost"
opts.ServerName = "Acme Corp"; // shown by the authenticator UI
opts.Origins = new() { "https://app.example.com" }; // dev: "https://localhost:5173"
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapLocalAuth();
app.MapLocalAccount();
app.MapPasskeyEndpoints(); // /auth/passkey/* + /account/passkeys
app.Run();
The package registers IFido2, IMemoryCache (challenge state), and PasskeyService. The nedo_passkeys table is provisioned by AddLocalAuthStore itself — the entity + EF mapping ship in Local.
Endpoints
| Method | Path | Auth | Body |
|---|---|---|---|
| POST | /auth/passkey/register/begin |
bearer | { authenticatorAttachment? } → CredentialCreateOptions JSON |
| POST | /auth/passkey/register/finish |
bearer | { attestationResponse, friendlyName? } |
| POST | /auth/passkey/assertion/begin |
anon | { username? } (omit for passwordless / discoverable) → AssertionOptions JSON |
| POST | /auth/passkey/assertion/finish |
anon | { assertionResponse, deviceInfo? } → TokenResponse |
| GET | /account/passkeys |
bearer | — → list of registered credentials |
| DELETE | /account/passkeys/{id} |
bearer | — |
Frontend recipe (passwordless / discoverable)
// Sign-in
const opts = await fetch('/auth/passkey/assertion/begin', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({}), // omit username for the discoverable flow
}).then(r => r.json());
const assertion = await navigator.credentials.get({ publicKey: decodeB64Url(opts) });
const tokens = await fetch('/auth/passkey/assertion/finish', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ assertionResponse: encodeB64Url(assertion), deviceInfo: navigator.userAgent }),
}).then(r => r.json());
// → same TokenResponse shape as /auth/login
decodeB64Url / encodeB64Url are the standard WebAuthn helpers — see the bundled React sample's webauthn.ts for a copy-pasteable pair.
Pipeline integration
FinishAssertionAsync runs the same gates as a password sign-in:
- FIDO2 signature verification + counter regression check (cloned-authenticator detection).
ILockoutPolicy.IsLockedOutAsync— passkey users lock out under the same policy.IMultiFactorChallenge.IsRequiredAsync— TOTP gating still applies.- Every
IPostLoginCheck— TOS / tenant / trial gates apply uniformly. SessionTokenIssuer.IssueNewSessionAsync— sameTokenResponseshape, same refresh-token family bookkeeping.
So a passkey login is a true peer of password sign-in, not a side door.
Pitfalls
ServerDomainmust match the SPA's host exactly (no scheme, no port). Common mistake: registering with"localhost"and signing in from"127.0.0.1".Originsmust include the scheme + port.https://app.example.com✓ —app.example.com✗.- Resident keys are required by default so credentials work in the discoverable / passwordless flow.
localhostworks for same-device WebAuthn, but cross-device hybrid (iPhone scanning desktop QR) requires HTTPS + a hostname the iPhone can reach. Usengrok https 5173for dev and updateServerDomain+Originsto match.
Full guide: docs/providers/passkeys.md.
Related
| Package | Role |
|---|---|
Nedo.AspNet.Authentication.Local |
Required peer — provides IUserStore, SessionTokenIssuer, the EF schema. |
Nedo.AspNet.Authentication.Totp |
Step-up MFA on top of passkey assertion. |
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- Fido2 (>= 3.0.1)
- Fido2.AspNet (>= 3.0.1)
- Microsoft.Extensions.Caching.Memory (>= 9.0.0)
- Nedo.AspNet.Authentication.Local (>= 2.0.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.