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
- Mental Model: What ASP.NET Core Identity Actually Is
- Project Setup for .NET 10 (Minimal but Realistic)
- Identity Building Blocks: Users, Roles, Claims, Tokens
- Configuring Identity Like a Security Engineer
- Authentication Cookies, Schemes and the Auth Pipeline
- Authorization: From Roles to Policy‑Based Access Control
- Identity with Minimal APIs and SPAs
- Extending the Identity Model Without Pain
- Production Concerns: Password Resets, 2FA, Email, Lockout
- 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:
-
Persistence Layer (Entity Framework Core)
- Tables like
AspNetUsers,AspNetRoles,AspNetUserClaims,AspNetUserLogins… -
UserManager<TUser>andRoleManager<TRole>orchestrate reads/writes.
- Tables like
-
Authentication Layer (Cookies / Tokens)
- Issues authentication cookies (or bearer tokens) after successful login.
- Uses ASP.NET Core authentication handlers (cookie, JWT bearer, etc.).
-
Authorization Layer (Roles, Claims, Policies)
- Applies
[Authorize]and policies to control what authenticated users may do.
- Applies
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
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();
ApplicationDbContext
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace IdentityNet10Demo.Data;
public sealed class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
Add a connection string in appsettings.json, then run:
dotnet ef migrations add InitialIdentitySchema
dotnet ef database update
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);
}
}
-
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 = FinanceandIsManager = 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>();
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!;
}
Remember to update the DbContext generics:
public sealed class ApplicationDbContext
: IdentityDbContext<ApplicationUser, IdentityRole, string>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }
}
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);
});
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")]
…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")));
});
Use them in controllers, Razor Pages or minimal APIs:
[Authorize(Policy = "FinanceManager")]
public IActionResult FinancialDashboard() => View();
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;
}
}
Register:
builder.Services.AddSingleton<IAuthorizationHandler, SameTenantHandler>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("SameTenantOnly", policy =>
policy.AddRequirements(new SameTenantRequirement()));
});
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
});
});
If the frontend shares the same origin and cookie, this just works.
7.2 JWT bearer + Identity
If you prefer tokens:
- Use Identity for user management.
- Issue JWTs from a custom endpoint using
JwtSecurityTokenHandler. - 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
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);
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)
- User submits email on “Forgot Password” page.
- 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>.");
- On the reset page, you consume the token:
var result = await _userManager.ResetPasswordAsync(user, token, model.NewPassword);
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:
-
AccessFailedCountincrements each failed attempt. - Once it reaches
MaxFailedAccessAttempts, the account is locked untilLockoutEnd.
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, appropriateSameSite.
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.IsInRolechecks 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)