DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

ASP.NET Core Identity in .NET 10 — From “Login Page” to Production‑Grade Security

ASP.NET Core Identity in .NET 10 — From “Login Page” to Production‑Grade Security<br>

ASP.NET Core Identity in .NET 10 — From “Login Page” to Production‑Grade Security

Most .NET developers meet ASP.NET Core Identity through a a template:

“Press F5, click **Register, it magically creates users in the database.”

That’s a nice demo.

But in real products you quickly hit harder questions:

  • How do I harden Identity for production (cookies, lockout, password policies, 2FA)?
  • How do I integrate Identity with minimal APIs and SPA frontends?
  • How do roles, claims and policies really work under the hood?
  • How do I extend the schema without breaking migrations or security?
  • How do I prepare for multi‑tenant, external IdPs, and zero‑trust flavored backends?

This post is a .NET 10‑ready guide that goes beyond scaffolding: we’ll take the familiar Identity stack and shape it into something you’d be comfortable running in production.

We’ll assume you’re targeting .NET 10 (but everything here works with .NET 8+ unless stated otherwise).


Table of Contents

  1. Mental Model: What ASP.NET Core Identity Actually Is
  2. Project Setup for .NET 10 (Minimal but Realistic)
  3. Identity Building Blocks: Users, Roles, Claims, Tokens
  4. Configuring Identity Like a Security Engineer
  5. Authentication Cookies, Schemes and the Auth Pipeline
  6. Authorization: From Roles to Policy‑Based Access Control
  7. Identity with Minimal APIs and SPAs
  8. Extending the Identity Model Without Pain
  9. Production Concerns: Password Resets, 2FA, Email, Lockout
  10. Checklist: Hardening ASP.NET Core Identity in .NET 10

1. Mental Model: What ASP.NET Core Identity Actually Is

Identity is not just “login pages”. Think of it as three layers:

  1. Persistence Layer (Entity Framework Core)

    • Tables like AspNetUsers, AspNetRoles, AspNetUserClaims, AspNetUserLogins
    • UserManager<TUser> and RoleManager<TRole> orchestrate reads/writes.
  2. Authentication Layer (Cookies / Tokens)

    • Issues authentication cookies (or bearer tokens) after successful login.
    • Uses ASP.NET Core authentication handlers (cookie, JWT bearer, etc.).
  3. Authorization Layer (Roles, Claims, Policies)

    • Applies [Authorize] and policies to control what authenticated users may do.

When you use the template, you get all three with conservative defaults. As a senior dev you:

  • Decide which features to turn on/off (2FA, lockout, external providers).
  • Shape the user model.
  • Integrate Identity with your app architecture (MVC, Razor Pages, minimal APIs, SPA).

2. Project Setup for .NET 10 (Minimal but Realistic)

Let’s start from a minimal ASP.NET Core app using Identity and EF Core.

dotnet new webapp -n IdentityNet10Demo
cd IdentityNet10Demo

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
Enter fullscreen mode Exit fullscreen mode

Program.cs

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using IdentityNet10Demo.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
    ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));

builder.Services
    .AddDefaultIdentity<IdentityUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = true;
    })
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();
Enter fullscreen mode Exit fullscreen mode

ApplicationDbContext

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace IdentityNet10Demo.Data;

public sealed class ApplicationDbContext : IdentityDbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

Add a connection string in appsettings.json, then run:

dotnet ef migrations add InitialIdentitySchema
dotnet ef database update
Enter fullscreen mode Exit fullscreen mode

You now have a working .NET app with Identity tables and a basic login/registration flow. From here we’ll elevate it to a production‑grade design.


3. Identity Building Blocks: Users, Roles, Claims, Tokens

At runtime Identity revolves around a few core types:

3.1 UserManager & SignInManager

public class AccountController : Controller
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly SignInManager<IdentityUser> _signInManager;

    public AccountController(
        UserManager<IdentityUser> userManager,
        SignInManager<IdentityUser> signInManager)
    {
        _userManager = userManager;
        _signInManager = signInManager;
    }

    public async Task<IActionResult> Login(LoginViewModel model)
    {
        if (!ModelState.IsValid)
            return View(model);

        var result = await _signInManager.PasswordSignInAsync(
            model.Email,
            model.Password,
            isPersistent: model.RememberMe,
            lockoutOnFailure: true);

        if (result.Succeeded)
            return RedirectToAction("Index", "Home");

        if (result.IsLockedOut)
            return View("Lockout");

        ModelState.AddModelError(string.Empty, "Invalid login attempt.");
        return View(model);
    }
}
Enter fullscreen mode Exit fullscreen mode
  • UserManager<TUser> handles user lifecycle: create, update, password hashing, email confirmation, 2FA, tokens.
  • SignInManager<TUser> handles sign‑in workflow: cookie issuance, lockout, 2FA, external providers.

3.2 Roles vs Claims vs Policies

  • Roles: coarse‑grained labels (Admin, Moderator, Customer).
  • Claims: key/value pairs that travel with the user ("department" = "Finance", "subscription" = "Pro").
  • Policies: rules that inspect roles and/or claims (e.g. “User must have department = Finance and IsManager = true”).

In .NET 10‑style apps, you’ll often keep roles minimal and do most fine‑grained authorization through claims + policies.


4. Configuring Identity Like a Security Engineer

Templates pick safe defaults, but production systems deserve explicit configuration.

builder.Services.AddDefaultIdentity<ApplicationUser>(options =>
    {
        // Password
        options.Password.RequiredLength = 12;
        options.Password.RequireNonAlphanumeric = true;
        options.Password.RequireUppercase = true;
        options.Password.RequireLowercase = true;
        options.Password.RequireDigit = true;

        // Lockout
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
        options.Lockout.MaxFailedAccessAttempts = 5;
        options.Lockout.AllowedForNewUsers = true;

        // User
        options.User.RequireUniqueEmail = true;

        // Sign‑in
        options.SignIn.RequireConfirmedAccount = true;
        options.SignIn.RequireConfirmedEmail = true;
    })
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();
Enter fullscreen mode Exit fullscreen mode

Security‑critical fields you should know

Identity’s default IdentityUser includes fields that are easy to ignore but very important:

  • SecurityStamp – invalidates auth cookies when it changes (password reset, 2FA change, etc.).
  • ConcurrencyStamp – prevents lost updates at the DB level.
  • LockoutEnd, AccessFailedCount – drive lockout logic.
  • TwoFactorEnabled – controls whether 2FA is required on sign‑in.

When you extend Identity, don’t remove these; extend using inheritance instead.

public sealed class ApplicationUser : IdentityUser
{
    public string FullName { get; set; } = default!;
    public DateTime RegisteredAtUtc { get; init; } = DateTime.UtcNow;

    // Example of a simple multi‑tenant flag
    public string TenantId { get; set; } = default!;
}
Enter fullscreen mode Exit fullscreen mode

Remember to update the DbContext generics:

public sealed class ApplicationDbContext
    : IdentityDbContext<ApplicationUser, IdentityRole, string>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }
}
Enter fullscreen mode Exit fullscreen mode

5. Authentication Cookies, Schemes and the Auth Pipeline

Identity uses the standard ASP.NET Core authentication system under the hood.

A typical .NET 10 app will rely on cookie authentication for server‑rendered UI, and possibly JWT bearer for APIs.

5.1 Cookie scheme anatomy

Identity registers a cookie scheme named (by default) Identity.Application. The cookie:

  • Contains the user principal (name, roles, claims).
  • Is encrypted and signed using ASP.NET Core Data Protection keys.
  • Has an expiration / sliding expiration configuration.

You can customize it:

builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.Name = ".IdentityNet10.Auth";
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Lax; // or Strict for pure server‑rendered sites

    options.LoginPath = "/Account/Login";
    options.LogoutPath = "/Account/Logout";
    options.AccessDeniedPath = "/Account/AccessDenied";

    options.SlidingExpiration = true;
    options.ExpireTimeSpan = TimeSpan.FromHours(8);
});
Enter fullscreen mode Exit fullscreen mode

For SPAs with separate frontends you’ll often combine:

  • Cookie auth for the human user hitting Razor Pages / BFF.
  • Access tokens (AddJwtBearer) for API calls.

Identity doesn’t care which handler you use, as long as you properly configure schemes and sign‑in logic.


6. Authorization: From Roles to Policy‑Based Access Control

Let’s move from:

[Authorize(Roles = "Admin")]
Enter fullscreen mode Exit fullscreen mode

…to policy‑based authorization, which is more flexible and easier to evolve.

6.1 Registering policies

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdmin", policy =>
        policy.RequireRole("Admin"));

    options.AddPolicy("PaidUser", policy =>
        policy.RequireClaim("subscription", "Pro", "Business"));

    options.AddPolicy("FinanceManager", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim("department", "Finance") &&
            context.User.HasClaim("is_manager", "true")));
});
Enter fullscreen mode Exit fullscreen mode

Use them in controllers, Razor Pages or minimal APIs:

[Authorize(Policy = "FinanceManager")]
public IActionResult FinancialDashboard() => View();
Enter fullscreen mode Exit fullscreen mode

6.2 Custom requirement & handler

For more complex rules (e.g., user must own the resource), build a custom requirement:

public sealed class SameTenantRequirement : IAuthorizationRequirement { }

public sealed class SameTenantHandler : AuthorizationHandler<SameTenantRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        SameTenantRequirement requirement)
    {
        var userTenant = context.User.FindFirst("tenant_id")?.Value;
        var resourceTenant = (context.Resource as ITenantScopedResource)?.TenantId;

        if (userTenant is not null &&
            resourceTenant is not null &&
            userTenant == resourceTenant)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

Register:

builder.Services.AddSingleton<IAuthorizationHandler, SameTenantHandler>();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("SameTenantOnly", policy =>
        policy.AddRequirements(new SameTenantRequirement()));
});
Enter fullscreen mode Exit fullscreen mode

Now your controllers/minimal APIs can enforce multi‑tenant boundaries with a single attribute.


7. Identity with Minimal APIs and SPAs

In .NET 8+ (and therefore .NET 10), many teams build minimal API backends with React/Angular/Blazor or mobile clients.

7.1 Protecting minimal APIs with Identity cookies

var api = app.MapGroup("/api")
    .RequireAuthorization(); // default policy

api.MapGet("/me", async (UserManager<ApplicationUser> users, ClaimsPrincipal user) =>
{
    var appUser = await users.GetUserAsync(user);
    return appUser is null
        ? Results.Unauthorized()
        : Results.Ok(new
        {
            appUser.Email,
            appUser.FullName,
            appUser.RegisteredAtUtc
        });
});
Enter fullscreen mode Exit fullscreen mode

If the frontend shares the same origin and cookie, this just works.

7.2 JWT bearer + Identity

If you prefer tokens:

  1. Use Identity for user management.
  2. Issue JWTs from a custom endpoint using JwtSecurityTokenHandler.
  3. Protect APIs with AddAuthentication().AddJwtBearer(...).

Identity doesn’t force cookies; it gives you a user store and security primitives that you can surface as you like.


8. Extending the Identity Model Without Pain

Sooner or later you’ll need extra fields on the user: subscription level, preferences, tenant, etc.

8.1 Extending the user

We already saw ApplicationUser : IdentityUser. Add your fields there and re‑run migrations:

dotnet ef migrations add AddTenantToApplicationUser
dotnet ef database update
Enter fullscreen mode Exit fullscreen mode

Avoid:

  • Editing the default Identity tables manually outside migrations.
  • Using a separate “Profile” table for basic fields that are almost always needed. It usually complicates queries.

8.2 Querying extended users

public sealed class UsersController : Controller
{
    private readonly UserManager<ApplicationUser> _userManager;

    public UsersController(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    [Authorize(Policy = "RequireAdmin")]
    public async Task<IActionResult> Index()
    {
        var users = await _userManager.Users
            .OrderBy(u => u.Email)
            .Select(u => new UserListItemDto(
                u.Id,
                u.Email!,
                u.FullName,
                u.TenantId,
                u.RegisteredAtUtc))
            .ToListAsync();

        return View(users);
    }
}

public sealed record UserListItemDto(
    string Id,
    string Email,
    string FullName,
    string TenantId,
    DateTime RegisteredAtUtc);
Enter fullscreen mode Exit fullscreen mode

Because the Identity tables are just EF Core entities, you can project them to DTOs like any other data.


9. Production Concerns: Password Resets, 2FA, Email, Lockout

Identity comes with a rich token system for password reset, email confirmation, change email, etc.

9.1 Password reset flow (high level)

  1. User submits email on “Forgot Password” page.
  2. You generate a token:
   var user = await _userManager.FindByEmailAsync(model.Email);
   if (user == null) return; // don’t reveal user existence

   var token = await _userManager.GeneratePasswordResetTokenAsync(user);
   var callbackUrl = Url.Page(
       "/Account/ResetPassword",
       values: new { userId = user.Id, token });

   await _emailSender.SendAsync(user.Email!, "Reset Password",
       $"Reset your password <a href='{callbackUrl}'>here</a>.");
Enter fullscreen mode Exit fullscreen mode
  1. On the reset page, you consume the token:
   var result = await _userManager.ResetPasswordAsync(user, token, model.NewPassword);
Enter fullscreen mode Exit fullscreen mode

Tokens are short‑lived, signed and protected by Data Protection keys.

9.2 Two‑Factor Authentication (2FA)

Identity supports:

  • TOTP (authenticator apps like Microsoft Authenticator, Google Authenticator).
  • SMS codes (if you configure an SMS provider).
  • Email codes (weaker, but better than single‑factor).

Once enabled, successful password sign‑in triggers an additional verification step, typically via SignInManager<TUser>.TwoFactorAuthenticatorSignInAsync(...) flows.

9.3 Lockout and brute‑force protection

With lockout enabled:

  • AccessFailedCount increments each failed attempt.
  • Once it reaches MaxFailedAccessAttempts, the account is locked until LockoutEnd.

You should always enable lockout for password logins, especially when exposed to the public internet.

9.4 Email delivery and background jobs

In real systems, you don’t want sign‑up flows to depend synchronously on SMTP.

A robust architecture:

  • Controllers or Razor Pages enqueue an email command to a queue (e.g., Azure Queue, RabbitMQ, background worker).
  • A background service sends emails and logs failures separately.

Identity doesn’t force any of this; you plug in your own IEmailSender implementation.


10. Checklist: Hardening ASP.NET Core Identity in .NET 10

When you ship a real app, walk through this checklist:

10.1 Basics

  • [ ] Require unique emails for users.
  • [ ] Enforce strong password policy (length, complexity, history if needed).
  • [ ] Use HTTPS everywhere. Redirect HTTP to HTTPS.
  • [ ] Configure cookie security: SecurePolicy = Always, HttpOnly = true, appropriate SameSite.

10.2 Account lifecycle

  • [ ] Require confirmed email before login (or before sensitive actions).
  • [ ] Enable lockout and choose reasonable lockout duration.
  • [ ] Implement password reset flow and test it end‑to‑end.
  • [ ] Provide account deletion / deactivation paths compliant with your privacy rules.

10.3 Authorization design

  • [ ] Keep roles small and stable (Admin, Support, Customer, …).
  • [ ] Use claims + policies for dynamic rules (subscription, tenant_id, feature flags).
  • [ ] Centralize policy registration; don’t sprinkle ad‑hoc User.IsInRole checks everywhere.

10.4 Multi‑environment, multi‑tenant

  • [ ] Store TenantId (or equivalent) in the user and/or claims.
  • [ ] Enforce tenant boundaries with policies/handlers (e.g. SameTenantRequirement).
  • [ ] Don’t share Data Protection keys across unrelated tenants or environments.

10.5 Observability & operations

  • [ ] Log sign‑in failures, lockouts, password reset attempts (without leaking secrets).
  • [ ] Monitor unusual patterns (many failed logins from a small IP range, etc.).
  • [ ] Regularly rotate Data Protection keys and email/OTP secrets according to your security policy.

Final Thoughts

ASP.NET Core Identity is much more than a checkbox that says “Individual Accounts” when you create a project.

When you treat it as a security subsystem instead of “scaffolding magic”, you can:

  • Model rich user profiles and tenants.
  • Enforce fine‑grained policies with claims and custom handlers.
  • Combine cookies, tokens and external IdPs in a controlled way.
  • Ship features like 2FA, password reset and lockout with confidence.

If you’re starting a new .NET 10 app today, resist the temptation to leave the defaults untouched. Spend a few hours designing:

  • your user model,
  • your roles/claims/policies,
  • your cookie/token story, and
  • your operational practices around Identity.

Those decisions will pay off every single day your system is in production.

Happy coding — and may your identities always be authenticated, authorized, and properly logged. 🔐🚀

Top comments (0)