Changelog
Guides

Run an app with no public sign-up

Map password sign-in, recovery, and account management without a public register route.

Create accounts in your own admin code. Do not map a public register route.

AddPasswordAuth always maps POST /register. There is no option to turn that route off. Compose sign-in instead.

Map sign-in without register

  1. Call AddAuthEndpointsIdentity. It sets RequireConfirmedAccount to true, then runs your callback.
  2. Register password sign-in, recovery, account management, and the cookie session. Do not call AddPasswordSignUpEndpoints.
  3. Map those four groups on one prefix. Do not call MapPasswordSignUpEndpoints.
  4. Invite each user from your admin code. A confirmation link confirms the email and sets no password. See Invite a user.
using AuthEndpoints;
using AuthEndpoints.Identity;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite("Data Source=app.db"));

builder.Services.AddAuthEndpointsIdentity<AppUser, AppDbContext>(options =>
{
    options.User.RequireUniqueEmail = true;
});
builder.Services.AddPasswordSignInEndpoints();
builder.Services.AddPasswordRecoveryEndpoints();
builder.Services.AddAccountManagementEndpoints();
builder.Services.AddCookieSessionEndpoints();
builder.Services.AddTransient<IEmailSender<AppUser>, QueuedIdentityEmailSender>();

var app = builder.Build();
app.UseAuthEndpoints();

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

app.Run();

public class AppUser : IdentityUser { }

public class AppDbContext(DbContextOptions<AppDbContext> options) : IdentityDbContext<AppUser>(options) { }

public sealed class QueuedIdentityEmailSender : IEmailSender<AppUser>
{
    public Task SendConfirmationLinkAsync(AppUser user, string email, string confirmationLink)
        => Task.CompletedTask;

    public Task SendPasswordResetLinkAsync(AppUser user, string email, string resetLink)
        => Task.CompletedTask;

    public Task SendPasswordResetCodeAsync(AppUser user, string email, string resetCode)
        => Task.CompletedTask;
}

POST /auth/login signs an existing user in. POST /auth/forgotPassword and POST /auth/resetPassword stay available. POST /auth/register is not mapped.

POST /auth/manage/password can set a first password after sign-in and step-up. It returns 404 when password sign-in is not mapped. This host maps password sign-in, so the route is present.

Invite a user

Create the account with no password, then send your own set-password link. GET /auth/confirmEmail confirms the email. It does not set a password.

  1. Call UserManager<TUser>.CreateAsync(user) with no password. Set EmailConfirmed to true before that call. An unconfirmed account cannot sign in, and it cannot reset a password.
  2. Call GeneratePasswordResetTokenAsync. Encode the token with WebEncoders.Base64UrlEncode of the UTF-8 bytes. POST /auth/resetPassword decodes resetCode that way.
  3. Send the encoded value to your own set-password screen. That screen posts { "email", "resetCode", "newPassword" } to POST /auth/resetPassword.
var user = new AppUser { UserName = email, Email = email, EmailConfirmed = true };
await userManager.CreateAsync(user);

var token = await userManager.GeneratePasswordResetTokenAsync(user);
var resetCode = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(token));

WebEncoders is in Microsoft.AspNetCore.WebUtilities.

Leave these maps off

  • External sign-in. AutoLinkByEmail defaults to false. Leaving it off does not stop user creation. External sign-in still creates an account for a verified email when no local user has that email. See External OAuth. MapExternalAccountEndpoints alone never creates a user. That linked login cannot sign the user in until you also map MapExternalAuthProvider or MapExternalAuthEndpoints.
  • Email-code sign-up. AllowSignUp defaults to false. Leave it false if you map email-code sign-in. See Sign in with an emailed code.
  • Passkey register. For passkey login and management without that register route, call AddPasskeySignInEndpoints<AppUser>() and AddPasskeyManagementEndpoints(). Do not call AddPasskeyEndpoints or AddPasskeySignUpEndpoints. Map MapSignInEndpoints<AppUser>(AuthEndpointsSession.Cookie) and MapPasskeyManagementEndpoints<AppUser>(). MapPasskeySignInEndpoints always maps POST /passkeys/register. Do not call it. In this recipe, MapSignInEndpoints replaces MapPasswordSignInEndpoints and MapCookieSessionEndpoints. Do not map those two as well. See Passkeys.