Changelog
Get started

Choose a sign-in stack

Which AuthEndpoints sign-in stack fits a browser app, a native app, or both, and why.

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

ClientStackWhy
Browser app on the same site as the APICookie (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 APIsJWTThe access token lives in memory. The refresh token stays in an HttpOnly SameSite=Strict cookie.
Native or mobile appIdentity bearerThe app stores the access and refresh tokens itself. It needs no cookie jar for sign-in and refresh.
Browser and native clients on one APICookie or JWT for the browser, Identity bearer for nativeMap 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=true or useSessionCookies=true, and returns Identity bearer tokens otherwise. JwtPasskeySignInCompleter returns a JWT instead.
  • The default OAuth completer sets the application cookie. JwtExternalLoginCompleter sets the JWT refresh cookie. No completer returns Identity bearer tokens.

Both methods skip two-factor authentication. See Security model.

Use AuthEndpoints 3.1.1 or later. In 3.1.0, when JWT is not turned on, passkey registration and passkey sign-in return 500. So does any other CSRF-protected endpoint called without a session. To upgrade, run dotnet add package AuthEndpoints --version 3.1.1.