Security model
AuthEndpoints adds protection in layers on top of ASP.NET Core Identity. This page explains what each layer covers, and where the library makes a trade-off that you should know about.
Responses do not reveal which accounts exist
An attacker who can tell whether an email is registered can target that account. AuthEndpoints returns the same answer whether or not an account exists:
- Password registration returns
200for a new email and for a duplicate. - Passkey registration options come back for any valid email. Registration with a taken email fails with the same generic message as a failed attestation.
POST /identity/forgotPasswordalways returns200.POST /identity/resetPasswordreturnsInvalidTokenfor an unknown email, an unconfirmed email, and a bad code alike.- Login returns
Invalid credentials.for a wrong password, a lockout, and an unconfirmed email.
One exception is deliberate. Identifier-first passkey sign-in (requestOptions with an email) can reveal through allowCredentials that an account has passkeys. Omit the email to use discoverable credentials and avoid that.
CSRF checks protect cookie sessions
A browser sends cookies on every request, including requests that another site starts. AuthEndpoints adds an antiforgery filter to every route that changes state for a signed-in cookie user, and to every passkey ceremony. The filter skips the check when a bearer scheme authenticated the request and no application or external cookie is present, because a cross-site page cannot attach a bearer header. Login, register, forgot password, and reset password have no check. They do not act on an existing session. See Antiforgery (CSRF) rules.
Rate limits and lockout slow guessing
Login routes share a token bucket per IP. Registration and password-reset routes share a fixed window per IP. Passkey options and passkey registration have tighter limits. Identity lockout is on for every password check (lockoutOnFailure: true), including the password proof in step-up. See Rate-limit policies.
Identity bearer POST /identity/refresh has no rate limit. A refresh token is a long random value, so guessing is not practical, but the route does accept unlimited attempts.
Step-up guards the changes that matter most
A stolen session should not be enough to take over an account. Changing the email, the password, the 2FA settings, or the passkeys requires a fresh ReAuth proof that expires after 5 minutes by default. Unlinking an external login requires one too. The ReAuth cookie is SameSite=Strict and does not slide. See Require step-up before sensitive actions.
Two-factor is a password add-on
AuthEndpoints treats 2FA as a second factor for password sign-in only. Passkey sign-in and GitHub or Google sign-in do not ask for a 2FA code, even when the user turned 2FA on. The passkey completers and the OAuth completers sign the user in directly.
For passkeys, this is a reasonable default. A passkey is phishing-resistant and is usually unlocked with a device biometric or PIN, so it already combines two factors. For OAuth, the reasoning is weaker. The account is as strong as the user's GitHub or Google account, which may or may not use 2FA. If your threat model requires 2FA on every path, register a custom IExternalLoginCompleter<TUser> or IPasskeySignInCompleter<TUser> that checks it.
A related trade-off: a persistent cookie login with a valid authenticator code sets Identity's remember-client cookie. Later password logins from that browser skip 2FA until the user sends forgetMachine or logs out. Session logins and recovery-code logins never set it.
Email confirmation gates sign-in
RequireConfirmedAccount is true by default. Until a user confirms the email, password, passkey, and JWT sign-in all fail. This blocks an attacker from claiming an address they do not own.
The cost is that resend needs a session. POST /identity/resendConfirmationEmail requires a signed-in user, and an unconfirmed user cannot sign in. A user who loses the first email cannot get another one through AuthEndpoints today.
GitHub and Google accounts skip the confirmation email. EmailConfirmed comes from the provider's verified flag, and RequireVerifiedEmail refuses unverified emails by default.
Startup checks catch unsafe Production settings
In Production, startup fails when the host has no real email sender, no passkey domain, or the default JWT issuer or audience. In every environment, startup fails for a missing or short symmetric JWT key and for an empty GitHub or Google client id or secret. See Prepare for production.
What the host owns
AuthEndpoints does not configure HTTPS, CORS, or the security of the client. Serve the API over HTTPS. Allow credentials in CORS only for the exact origins you trust. Keep Identity bearer tokens in secure device storage.