Choose a sign-in stack
AuthEndpoints has three password sign-in stacks: cookie, Identity bearer, and JWT. Passkeys and GitHub or Google sign-in sit beside them and finish into one of those stacks. The right stack depends on where the client keeps its credentials.
The short answer
| Client | Stack | Why |
|---|---|---|
| Browser app on the same site as the API | Cookie (the facade default) | The browser stores an HttpOnly cookie. Script cannot read it. CSRF tokens protect the routes that change state. |
| Browser app that needs an access token for other APIs | JWT | The access token lives in memory. The refresh token stays in an HttpOnly SameSite=Strict cookie. |
| Native or mobile app | Identity bearer | The app stores the access and refresh tokens itself. It needs no cookie jar for sign-in and refresh. |
| Browser and native clients on one API | Cookie or JWT for the browser, Identity bearer for native | Map two sign-in groups. See Compose a custom auth stack. |
Why cookies are the default
Most AuthEndpoints hosts serve a browser app from the same site as the API. For that client, a cookie is the smallest attack surface. JavaScript cannot read an HttpOnly cookie, so a cross-site scripting bug cannot copy the session out of the browser. The cost is CSRF: the browser sends the cookie on every request, so every state-changing route checks an antiforgery token. AuthEndpoints adds those checks for you. Cookies also make passkeys simple, because the WebAuthn ceremony already runs in the browser.
The cookie stack ends a session cleanly. POST /identity/logout clears the application cookie, the 2FA remember-client cookie, and the ReAuth cookie.
When JWT is the better fit
Pick JWT when the browser app must send a bearer token, for example to a second API that validates the same JWT. AuthEndpoints keeps the long-lived part out of script: the refresh token is HttpOnly, SameSite=Strict, stored hashed, and rotated on each use. Reuse of an old refresh token revokes the whole token family. POST /auth/logout revokes the family too.
JWT asks more of the host. You add a migration for the refresh-token table, and you configure the signing key, issuer, and audience. The client keeps the access token in memory and calls POST /auth/refresh with a CSRF token when it expires. JWT is not a facade sign-in mode. Turn it on with Jwt.Enabled beside the cookie stack, or compose it on its own.
Why native apps use Identity bearer
A native app has no browser cookie jar to lean on and no cross-site request risk. Identity bearer returns both tokens in the response body, and POST /identity/refresh takes the refresh token in the body. That fits secure on-device storage.
Know two trade-offs. First, POST /identity/logout does not revoke Identity bearer tokens. They stay valid until they expire. Second, the bearer facade maps no /csrfToken route, but passkey ceremonies still need a CSRF token. To use passkeys on a bearer host, map an antiforgery token endpoint yourself. See Quick start.
Where passkeys and GitHub or Google fit
Passkeys and external OAuth are first-factor methods. They do not replace a stack. Each finishes into one:
- The default passkey completer sets the application cookie when the request has
useCookies=trueoruseSessionCookies=true, and returns Identity bearer tokens otherwise.JwtPasskeySignInCompleterreturns a JWT instead. - The default OAuth completer sets the application cookie.
JwtExternalLoginCompletersets the JWT refresh cookie. No completer returns Identity bearer tokens.
Both methods skip two-factor authentication. See Security model.
500. So does any other CSRF-protected endpoint called without a session. To upgrade, run dotnet add package AuthEndpoints --version 3.1.1.