ASP.NET Core Identity is the built-in authentication system for ASP.NET Core. It stores users, passwords, roles, and claims in your own database, protects passwords with modern hashing, and integrates directly with the authorization middleware. This complete guide walks every piece you need for a production MVC application on .NET 10 — setup, registration, login, roles, claims, two-factor authentication, and lockout.
Setting Up ASP.NET Core Identity with EF Core
Install the Microsoft.AspNetCore.Identity.EntityFrameworkCore package and register Identity in Program.cs. The AddIdentity extension wires the user store, sign-in manager, and role manager, while AddEntityFrameworkStores points them at your AppDbContext:
builder.Services.AddDbContext<AppDbContext>(o =>
o.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services
.AddIdentity<AppUser, IdentityRole>(options =>
{
options.Password.RequiredLength = 10;
options.Password.RequireDigit = true;
options.Password.RequireUppercase = true;
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
The AppUser class extends IdentityUser, which lets you add custom columns such as FullName, AvatarUrl, or a TenantId for multi-tenant apps. Run a migration and the Identity tables — AspNetUsers, AspNetRoles, AspNetUserClaims, and friends — appear in your database.
Registration: Creating Users with UserManager
Registration uses UserManager<AppUser>. After a successful create, you can assign a default role and sign the user in:
public class AccountController : Controller
{
private readonly UserManager<AppUser> _userManager;
private readonly SignInManager<AppUser> _signInManager;
public AccountController(UserManager<AppUser> users, SignInManager<AppUser> signIn)
=> (_userManager, _signInManager) = (users, signIn);
[HttpPost]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (!ModelState.IsValid) return View(model);
var user = new AppUser { UserName = model.Email, Email = model.Email, FullName = model.FullName };
var result = await _userManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
foreach (var error in result.Errors)
ModelState.AddModelError(string.Empty, error.Description);
return View(model);
}
await _userManager.AddToRoleAsync(user, "Member");
await _signInManager.SignInAsync(user, isPersistent: true);
return RedirectToAction(nameof(HomeController.Index), "Home");
}
}
Note that CreateAsync(user, password) hashes and stores the password for you — you never write a password to the database. The default role assignment keeps new users on the least-privileged path, which is exactly how the MVC EDevKit Basic starter seeds its Guest, Member, and Admin roles.
Login and Logout with SignInManager
Login validates credentials, respects lockout, and sets the authentication cookie. Logout clears it:
[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (!ModelState.IsValid) return View(model);
var result = await _signInManager.PasswordSignInAsync(
model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
if (result.Succeeded) return RedirectToAction(nameof(HomeController.Index), "Home");
if (result.IsLockedOut) return View("Lockout");
if (result.RequiresTwoFactor) return RedirectToAction(nameof(LoginTwoFactor));
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
[HttpPost]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
return RedirectToAction(nameof(HomeController.Index), "Home");
}
Notice how the sign-in result tells you exactly what happened — success, lockout, or a required second factor — so the UI can respond correctly instead of showing a generic error.
Roles: Seeding Admin and Enforcing Role-Based Access
Seed roles and an admin user at startup, then protect controllers with [Authorize(Roles = "Admin")]:
public static async Task SeedAsync(IServiceProvider services)
{
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
var userManager = services.GetRequiredService<UserManager<AppUser>>();
foreach (var role in new[] { "Guest", "Member", "Admin" })
if (!await roleManager.RoleExistsAsync(role))
await roleManager.CreateAsync(new IdentityRole(role));
var admin = await userManager.FindByEmailAsync("admin@root.com");
if (admin is null)
{
admin = new AppUser { UserName = "admin@root.com", Email = "admin@root.com" };
await userManager.CreateAsync(admin, "Admin@123456");
}
if (!await userManager.IsInRoleAsync(admin, "Admin"))
await userManager.AddToRoleAsync(admin, "Admin");
}
Role attributes give you quick, coarse-grained control: [Authorize] for any signed-in user, [Authorize(Roles = "Admin")] for admins only, and [Authorize(Roles = "Admin,Manager")] for a list of roles. For fine-grained rules, move up to claims and policies.
Claims and Policy-Based Authorization
Claims are facts about a user — such as Department = "Finance" — and policies evaluate claims at runtime:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("FinanceOnly", policy =>
policy.RequireClaim("Department", "Finance"));
options.AddPolicy("CanApprove", policy =>
policy.RequireAssertion(ctx =>
ctx.User.IsInRole("Admin") ||
ctx.User.HasClaim(c => c.Type == "CanApprove" && c.Value == "true")));
});
Apply with [Authorize(Policy = "FinanceOnly")] on a controller or action. Claims come from the user profile, from the database, or from an external provider, and you can inject them at login time with UserClaimsPrincipalFactory.
Two-Factor Authentication and Lockout
Identity supports authenticator-app and email-based 2FA out of the box. After login you redirect to a code-verification step:
[HttpPost]
public async Task<IActionResult> LoginTwoFactor(string code)
{
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if (user is null) return RedirectToAction(nameof(Login));
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(
code, rememberMe: true, rememberClient: false);
if (result.Succeeded) return RedirectToAction(nameof(HomeController.Index), "Home");
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
return View();
}
Lockout is enforced automatically when lockoutOnFailure: true is used with PasswordSignInAsync. Combined with the lockout options set earlier, five failed attempts blocks the account for fifteen minutes — a simple, effective brute-force defense.
Key Takeaways
AddIdentity+AddEntityFrameworkStoresgives you users, roles, and claims backed by your own databaseUserManagercreates users and hashes passwords;SignInManagerhandles login, lockout, and 2FA- Seed Guest, Member, and Admin roles and assign new users to the least-privileged role
- Combine
[Authorize(Roles=...)]with claims-based policies for coarse and fine-grained access - Password policies, lockout, and two-factor authentication are configuration, not custom code
FAQ
What is the difference between UserManager and SignInManager? UserManager performs user management — create, update, roles, claims, password reset. SignInManager handles the sign-in flow itself — password validation, cookies, lockout, and two-factor verification.
Does ASP.NET Core Identity store passwords in plain text? No. Identity hashes passwords with PBKDF2-style hashing (ASP.NET Core version 2+, ASP.NET Core Identity v3+ uses PBKDF2) and you never store or read the raw password.
Can I use both cookie and JWT authentication with Identity? Yes. Cookie authentication serves the MVC UI while JWT bearer authentication protects API endpoints, sharing the same Identity user store.
How do I add custom fields to the Identity user? Create a class that inherits from IdentityUser, add your properties, register that class in AddIdentity<AppUser, IdentityRole>(), and add a migration.