Changelog
Get started

Password auth: one session under /auth

What AddPasswordAuth and MapPasswordAuth register and map, and the building-block calls they expand to.

AddPasswordAuth<TUser, TContext> and MapPasswordAuth<TUser> are the password facade. One call registers Identity, password sign-up, sign-in, and recovery, account management, and one session. Cookie is the default. JWT is the other session. The facade adds no routes of its own. It expands to the building-block calls on this page.

TUser is a class with a public parameterless constructor. TContext is a DbContext. There is no role overload. Identity bearer stays on the 3.x facade. Do not call both facades in one host. Calling AddPasswordAuth twice throws. MapPasswordAuth without AddPasswordAuth throws InvalidOperationException. The message is in How composition works.

The tutorial is the quick start. Options are in Configuration options. Routes for each Add* and Map* pair are in the composition table.

builder.Services.AddPasswordAuth<AppUser, AppDbContext>();

app.UseAuthEndpoints();
app.MapPasswordAuth<AppUser>();

Every route is under /auth (PasswordAuthOptions.Path). Password sign-in is POST /auth/login. It does not use antiforgery. GET /auth/csrfToken issues the token for logout, manage POST routes, and step-up. RequireConfirmedAccount defaults to true, so login before confirm is 401.

Passkeys stay off until Passkeys.ServerDomain is set. Email-code sign-in and external sign-in are not mapped.

JWT session

Set Session to AuthEndpointsSession.Jwt and pass Jwt. The callback is required. A missing signing key fails while the host is being built.

builder.Services.AddPasswordAuth<AppUser, AppDbContext>(o =>
{
    o.Session = AuthEndpointsSession.Jwt;
    o.Jwt = jwt =>
    {
        jwt.Issuer = "https://example.com";
        jwt.Audience = "https://example.com";
        jwt.SigningOptions.SymmetricKey = key;
    };
});

Password sign-in is POST /auth/create, not POST /auth/login. Refresh transport, the refresh cookie, and body transport are in JWT. The DbContext calls UseRefreshToken(), and you add a migration before production.

Session IdentityBearer fails startup validation.

What the map includes

Cookie session maps register, POST /login, forgot-password, reset-password, logout, GET /csrfToken, confirm-email, resend, and account management (/manage/info, /manage/2fa, /manage/authMethods, /manage/password, /confirmIdentity).

JWT session uses the same management, recovery, and confirm-email routes. It maps POST /create, POST /refresh, GET /verify, and POST /logout instead of POST /login. Cookie refresh also maps GET /csrfToken. Body refresh omits that route.

When Passkeys.ServerDomain is set, the same prefix also maps passkey sign-in and passkey management, plus POST /confirmIdentity/passkeyOptions. Passkey sign-in includes POST /passkeys/register. See Passkeys.

Session, Path, and Passkeys.ServerDomain are fixed by the AddPasswordAuth callback. A later Configure<PasswordAuthOptions> that changes any of them fails validation. EmailConfirmation can still be configured later. The option table is in Configuration options.

Building-block expansion

AddPasswordAuth and MapPasswordAuth expand to these calls. Copy them when you leave the facade.

services.AddAuthEndpointsIdentity<TUser, TContext>(identity =>
{
    identity.SignIn.RequireConfirmedAccount = options.RequireConfirmedAccount;
    options.ConfigureIdentity?.Invoke(identity);
});
services.AddPasswordSignUpEndpoints();
services.AddPasswordSignInEndpoints();
services.AddPasswordRecoveryEndpoints();
services.AddAccountManagementEndpoints();

// Cookie session:
services.AddCookieSessionEndpoints();

// JWT session (options.Jwt is required).
// AddJwtEndpoints copies a non-default SimpleJwtOptions.RefreshTokenTransport.
services.AddJwtEndpoints<TUser, TContext>(options.Jwt);

// When Passkeys.ServerDomain is set:
services.AddPasskeyEndpoints<TUser>();
services.Configure<IdentityPasskeyOptions>(passkeys => passkeys.ServerDomain = options.Passkeys.ServerDomain);

// Confirm-email redirect, read by the shared handler:
services.Configure<AuthEndpointsOptions>(auth =>
{
    auth.EmailConfirmation.ConfirmEmailRedirectUri = options.EmailConfirmation.ConfirmEmailRedirectUri;
    auth.EmailConfirmation.AllowedRedirectOrigins = options.EmailConfirmation.AllowedRedirectOrigins;
});

Map under one group. Cookie session:

var group = app.MapGroup("/auth");
group.MapPasswordSignUpEndpoints<TUser>();
group.MapPasswordSignInEndpoints<TUser>(AuthEndpointsSession.Cookie);
group.MapPasswordRecoveryEndpoints<TUser>();
group.MapCookieSessionEndpoints<TUser>();
group.MapAccountManagementEndpoints<TUser>();

JWT session replaces the sign-in and session lines:

group.MapJwtPasswordEndpoints<TUser>();
group.MapJwtSessionEndpoints<TUser>();

When passkeys are on, the same group also calls:

group.MapPasskeySignInEndpoints<TUser>(session);
group.MapPasskeyManagementEndpoints<TUser>();

session is AuthEndpointsSession.Cookie or AuthEndpointsSession.Jwt. Do not also call MapPasskeyRegisterOptionsEndpoints. MapPasskeySignInEndpoints already maps register/options and register.