Changelog
Modules

Passkeys

WebAuthn passwordless register/login and credential management endpoints.

Passkey (WebAuthn) endpoints for passwordless register/login and managing credentials on an existing account.

DI

builder.Services.AddPasskeyEndpoints<AppUser>();

This registers passkey rate limits/ReAuth and the default IdentityPasskeySignInCompleter<AppUser>. Prefer the generic overload for compose hosts so register/login can resolve IPasskeySignInCompleter<TUser>.

To choose the user id minted during passwordless register:

builder.Services.AddPasskeyUserIdFactory(() => Ulid.NewUlid().ToString());

Or register an IPasskeyUserIdFactory implementation. When no custom factory is registered, the id is Guid.NewGuid() (UUID v4).

Facade: passkeys are enabled by default; set Passkeys.ServerDomain (required in Production).

Map

app.MapGroup("/account").MapPasskeyEndpoints<AppUser>();

Routes are mapped under {prefix}/passkeys (facade default /account/passkeys).

Routes

MethodPathAuth / extras
POST/passkeys/creationOptionsAuth + ReAuth + CSRF + rate limit
POST/passkeys/requestOptionsCSRF + rate limit; optional ?username=
POST/passkeys/register/optionsCSRF; body { email }
POST/passkeys/registerCSRF; passwordless create + completer sign-in
POST/passkeys/loginCSRF; completer sign-in (useCookies / useSessionCookies for Identity completer)
POST/passkeys/Add passkey (auth + ReAuth + CSRF)
GET/passkeys/List passkeys (auth)
PATCH/passkeys/Rename (auth + ReAuth + CSRF)
DELETE/passkeys/{credentialIdUrl}Remove (auth + ReAuth + CSRF)

Passwordless register flow

  1. POST /account/passkeys/register/options with { "email": "..." }
  2. Browser navigator.credentials.create(...)
  3. POST /account/passkeys/register with { "email": "...", "credentialJson": "..." }

A successful user create sends the same confirmation email as password POST /register. The account is not marked confirmed. Completers still skip a session when CanSignInAsync fails (for example RequireConfirmedAccount). Duplicate-email and failed-attestation paths do not send that mail. Passwordless register never attaches a passkey to an existing user id; add a credential to an existing account with authenticated POST /passkeys/creationOptions then POST /passkeys/.

SPA password and passkey signup with a shared check-email screen: Register a confirmed account.

Sign-in completer

After a successful register/login ceremony, AuthEndpoints calls IPasskeySignInCompleter<TUser>.

Default: Identity

IdentityPasskeySignInCompleter honors the same query flags as Identity bearer Login (MapBearerAuthEndpoints):

  • ?useCookies=true → persistent application cookie
  • ?useSessionCookies=true → session application cookie
  • neither → Identity bearer token (AccessTokenResponse)
The default completer follows bearer Login flags, not facade LoginCookie. ?useCookies=true has no effect on POST /identity/login under MapAuthEndpoints. See Cookie auth.

Register also returns PasskeyCredentialResponse (credential id). Completers only establish a session when CanSignInAsync succeeds (same confirmed-account / lockout policy as Identity login and OAuth).

When CanSignInAsync fails (unconfirmed account, lockout):

KindIdentity completerJWT completer
RegisterPasskeyCredentialResponse (credential id); no sessionempty 200
Login401 Invalid credentials401 Invalid credentials

Simple JWT

Register JwtPasskeySignInCompleter instead (requires AddJwtEndpoints):

builder.Services.AddPasskeyEndpoints<AppUser>();
builder.Services.AddJwtEndpoints<AppUser, AppDbContext>(o =>
{
    o.Issuer = "https://example.com";
    o.Audience = "https://example.com";
    o.SigningOptions.SymmetricKey = builder.Configuration["Jwt:SymmetricKey"];
});
builder.Services.AddPasskeySignInCompleter<AppUser, JwtPasskeySignInCompleter<AppUser>>();

Successful passkey register/login then returns the same shape as JWT /create: access token in JSON + HttpOnly refresh cookie. Cookie query flags are ignored for this completer.

Facade JWT opt-in does not auto-select the JWT completer — register it explicitly when you want Simple JWT after passkeys.

Constraints

  • Passwordless register needs IdentityUser with string or Guid key. The default user id is Guid.NewGuid() (UUID v4). Apps can override the minted id by registering IPasskeyUserIdFactory.
  • CSRF is required for WebAuthn ceremonies.
  • Sensitive credential mutations require ReAuth.