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 body { email } |
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) |
Identifier-first requestOptions with email may reveal passkey presence via allowCredentials. Omit email for usernameless/discoverable login.
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, the library runs lockout and CanSignInAsync. It calls IPasskeySignInCompleter<TUser> only when sign-in is allowed.
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 (credentialId; displayName and createdAt when present). If sign-in is not allowed, register still returns that credential id with no session. Login returns 401 Invalid credentials. A custom completer cannot issue a cookie or token on those paths.
List and add return the same shape. POST /passkeys/ accepts optional name (trimmed, max 200). Identity stores that as UserPasskeyInfo.Name. JSON still uses displayName.
When the library gate denies sign-in (unconfirmed account, lockout):
| Kind | Response |
|---|---|
| Register | PasskeyCredentialResponse (credential id); no session or tokens |
| Login | 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. MapPasskeyEndpointsthrows at map time if the user store lacks passkey or email support. The module is email-keyed.- CSRF is required for WebAuthn ceremonies.
- Sensitive credential mutations require ReAuth.