Passkeys
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
| Method | Path | Auth / extras |
|---|---|---|
POST | /passkeys/creationOptions | Auth + ReAuth + CSRF + rate limit |
POST | /passkeys/requestOptions | CSRF + rate limit; optional ?username= |
POST | /passkeys/register/options | CSRF; body { email } |
POST | /passkeys/register | CSRF; passwordless create + completer sign-in |
POST | /passkeys/login | CSRF; 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
POST /account/passkeys/register/optionswith{ "email": "..." }- Browser
navigator.credentials.create(...) POST /account/passkeys/registerwith{ "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)
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):
| Kind | Identity completer | JWT completer |
|---|---|---|
| Register | PasskeyCredentialResponse (credential id); no session | empty 200 |
| Login | 401 Invalid credentials | 401 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
IdentityUserwithstringorGuidkey. The default user id isGuid.NewGuid()(UUID v4). Apps can override the minted id by registeringIPasskeyUserIdFactory. - CSRF is required for WebAuthn ceremonies.
- Sensitive credential mutations require ReAuth.