Learnixo
Back to blog
AI Systemsintermediate

ASP.NET Core Identity Setup — Users, Passwords, and Stores

Configure ASP.NET Core Identity correctly: custom user entity, password hashing, EF Core store, token providers, and the identity pipeline in a Clean Architecture project.

Asma Hafeez KhanMay 16, 20265 min read
ASP.NET Core Identity.NETAuthenticationEF CoreSecurity
Share:𝕏

What ASP.NET Core Identity Provides

ASP.NET Core Identity is a membership system: user storage, password hashing, role management, token providers (email confirmation, password reset), and lockout. It is infrastructure — it belongs in the Infrastructure layer of a Clean Architecture project.

Identity provides:
  ✓ UserManager     — create, find, update, delete users
  ✓ SignInManager   — password check, lockout tracking, 2FA
  ✓ RoleManager     — role CRUD
  ✓ IPasswordHasher — PBKDF2 password hashing
  ✓ Token providers        — email confirmation, password reset, TOTP 2FA

Identity does NOT provide:
  ✗ JWT issuance — you write that
  ✗ OAuth flows — add IdentityServer/Duende or use Azure AD
  ✗ Session management — you handle that

Custom User Entity

C#
// Domain/Users/AppUser.cs
public sealed class AppUser : IdentityUser<Guid>
{
    // IdentityUser provides: Id, UserName, Email, PasswordHash,
    // PhoneNumber, LockoutEnabled, AccessFailedCount, etc.

    // Add domain-specific fields:
    public string    FirstName   { get; set; } = null!;
    public string    LastName    { get; set; } = null!;
    public string    Department  { get; set; } = null!;
    public string    LicenseNo   { get; set; } = null!;    // medical license
    public DateTime  CreatedAt   { get; set; }
    public bool      IsActive    { get; set; }
}

Registration with EF Core Store

C#
// Infrastructure/Persistence/AppDbContext.cs
public sealed class AppDbContext : IdentityDbContext<AppUser, AppRole, Guid>
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);  // Identity table configuration

        // Rename Identity tables to match your naming convention
        builder.Entity<AppUser>().ToTable("Users");
        builder.Entity<AppRole>().ToTable("Roles");
        builder.Entity<IdentityUserRole<Guid>>().ToTable("UserRoles");
        builder.Entity<IdentityUserClaim<Guid>>().ToTable("UserClaims");
        builder.Entity<IdentityUserLogin<Guid>>().ToTable("UserLogins");
        builder.Entity<IdentityUserToken<Guid>>().ToTable("UserTokens");
        builder.Entity<IdentityRoleClaim<Guid>>().ToTable("RoleClaims");
    }
}
C#
// Program.cs
builder.Services
    .AddIdentity<AppUser, AppRole>(options =>
    {
        // Password policy
        options.Password.RequiredLength         = 12;
        options.Password.RequireDigit           = true;
        options.Password.RequireUppercase       = true;
        options.Password.RequireNonAlphanumeric = true;

        // Lockout
        options.Lockout.DefaultLockoutTimeSpan  = TimeSpan.FromMinutes(15);
        options.Lockout.MaxFailedAccessAttempts = 5;

        // User settings
        options.User.RequireUniqueEmail = true;
    })
    .AddEntityFrameworkStores<AppDbContext>()
    .AddDefaultTokenProviders();

User Registration Handler

C#
// Application/Auth/Commands/RegisterUser/RegisterUserHandler.cs
public sealed class RegisterUserHandler
{
    private readonly UserManager<AppUser> _users;

    public RegisterUserHandler(UserManager<AppUser> users) => _users = users;

    public async Task<Result<Guid>> Handle(
        RegisterUserCommand cmd, CancellationToken ct)
    {
        var existing = await _users.FindByEmailAsync(cmd.Email);
        if (existing is not null)
            return Result.Failure<Guid>(AuthErrors.EmailAlreadyTaken);

        var user = new AppUser
        {
            Id         = Guid.NewGuid(),
            UserName   = cmd.Email,
            Email      = cmd.Email,
            FirstName  = cmd.FirstName,
            LastName   = cmd.LastName,
            Department = cmd.Department,
            CreatedAt  = DateTime.UtcNow,
            IsActive   = true,
        };

        var result = await _users.CreateAsync(user, cmd.Password);
        if (!result.Succeeded)
        {
            var errors = string.Join(", ", result.Errors.Select(e => e.Description));
            return Result.Failure<Guid>(Error.Validation("Identity.CreateFailed", errors));
        }

        await _users.AddToRoleAsync(user, cmd.Role);
        return Result.Success(user.Id);
    }
}

Login with Lockout

C#
// Application/Auth/Commands/Login/LoginHandler.cs
public sealed class LoginHandler
{
    private readonly UserManager<AppUser>  _users;
    private readonly SignInManager<AppUser> _signIn;
    private readonly TokenService          _tokens;

    public async Task<Result<LoginResponse>> Handle(
        LoginCommand cmd, CancellationToken ct)
    {
        var user = await _users.FindByEmailAsync(cmd.Email);
        if (user is null || !user.IsActive)
            return Result.Failure<LoginResponse>(AuthErrors.InvalidCredentials);

        // lockoutOnFailure: true — increments failed attempts, locks after threshold
        var result = await _signIn.CheckPasswordSignInAsync(
            user, cmd.Password, lockoutOnFailure: true);

        if (result.IsLockedOut)
            return Result.Failure<LoginResponse>(AuthErrors.AccountLocked);

        if (!result.Succeeded)
            return Result.Failure<LoginResponse>(AuthErrors.InvalidCredentials);

        var accessToken  = _tokens.GenerateAccessToken(user);
        var refreshToken = await _tokens.CreateRefreshTokenAsync(user.Id, cmd.IpAddress);

        return Result.Success(new LoginResponse(accessToken, refreshToken.Token));
    }
}

Password Reset Flow

C#
// Step 1: Generate reset token
var user  = await _users.FindByEmailAsync(email);
var token = await _users.GeneratePasswordResetTokenAsync(user!);
// Send token via email (URL-encode it)
await _email.SendPasswordResetAsync(user.Email, token);

// Step 2: User submits new password with token
var result = await _users.ResetPasswordAsync(user!, resetToken, newPassword);
if (!result.Succeeded)
    return Result.Failure(AuthErrors.InvalidResetToken);

Email Confirmation

C#
// After registration, send confirmation email
var token = await _users.GenerateEmailConfirmationTokenAsync(user);
var link  = $"https://app.systemforge.com/confirm-email?token={Uri.EscapeDataString(token)}&userId={user.Id}";
await _email.SendConfirmationAsync(user.Email, link);

// On confirmation endpoint
var result = await _users.ConfirmEmailAsync(user!, token);
if (!result.Succeeded)
    return Results.BadRequest("Invalid confirmation token.");

Production issue I've seen: A team did not URL-encode the email confirmation token before embedding it in the link. Tokens containing + characters (common in Base64) were decoded as spaces by the browser, making the token invalid. Uri.EscapeDataString() on the token before embedding it in a URL fixes this entirely.


Clean Architecture Layer Placement

Domain:
  AppUser extends IdentityUser
  No Identity interfaces here — just the entity

Application:
  Uses UserManager and SignInManager via DI
  Handlers: RegisterUser, Login, ChangePassword, ResetPassword
  Does NOT reference EF Core directly

Infrastructure:
  AppDbContext : IdentityDbContext
  EF Core migration for Identity tables
  Token providers configuration

API:
  Endpoints that call application handlers
  No Identity calls directly in endpoints

Red Flag / Green Answer

Red Flag: "We call _users.CreateAsync() directly from the API controller."

Identity calls in controllers bypass the application layer — no domain validation, no Result pattern, no consistent error handling. When requirements change (add department validation, enforce license number), the logic is scattered across controllers.

Green Answer:

RegisterUserHandler in Application layer. Controller calls _handler.Handle(cmd). Identity is an infrastructure detail — the application layer orchestrates it.


Key Takeaway

ASP.NET Core Identity is a complete membership system: hashed passwords, lockout, token providers. In Clean Architecture, it lives in Infrastructure (EF store) and is used by Application handlers (via UserManager/SignInManager). The Identity tables belong to your DbContext; your custom AppUser adds domain fields. JWT issuance is separate — Identity validates credentials, your TokenService issues tokens.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.