ASP.NET Core Identity Complete Guide: Authentication & Authorization
Master ASP.NET Core Identity for secure authentication and authorization.
20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.
- ✓Basic knowledge of C# and .NET
- ✓Familiarity with ASP.NET Core MVC or Razor Pages
- ✓Understanding of Entity Framework Core basics
- ✓Visual Studio or VS Code with .NET SDK installed
ASP.NET Core Identity is a membership system that adds login functionality to your app. It handles user registration, password hashing, role management, and external login providers. You integrate it by adding the Identity services, configuring a data store (usually EF Core), and using built-in UI or custom endpoints. Key features include two-factor authentication, lockout, and claims-based authorization.
Think of ASP.NET Core Identity as a bouncer for your website. It keeps a list of who is allowed in (users), what they can do (roles), and checks their ID (password or external login). It also remembers who is logged in with a special stamp (cookie or token). If someone tries the wrong password too many times, it locks them out temporarily. You can customize the bouncer's rules without building everything from scratch.
Building a web application that requires user authentication is a common but complex task. You need to handle user registration, login, password recovery, email confirmation, two-factor authentication, and role-based access control. Doing all of this securely from scratch is error-prone and time-consuming. ASP.NET Core Identity is Microsoft's built-in solution that provides all these features out of the box, with extensibility points for customization.
In this guide, you'll learn how to integrate ASP.NET Core Identity into your application, configure it for production use, and extend it with custom user stores and policies. We'll cover real-world scenarios like integrating with external login providers (Google, Facebook), implementing JWT authentication for APIs, and handling user lockout. By the end, you'll be able to build a secure authentication system that scales.
We'll also share a production incident where a misconfigured lockout policy led to a user lockout storm, and how to avoid it. Let's dive in.
Setting Up ASP.NET Core Identity
To get started, create a new ASP.NET Core project and add the required NuGet packages. The easiest way is to use the individual user accounts template. Alternatively, you can add Identity to an existing project.
First, install the Microsoft.AspNetCore.Identity.EntityFrameworkCore package if you plan to use Entity Framework Core as your data store. Then, add the Identity services in Program.cs:
```csharp builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddDefaultIdentity<IdentityUser>(options => { // Configure Identity options here }) .AddEntityFrameworkStores<ApplicationDbContext>(); ```
This registers the default IdentityUser and IdentityRole classes. You can customize these by creating your own classes that inherit from IdentityUser and IdentityRole.
Next, add the Identity middleware in the request pipeline:
``csharp app.``UseAuthentication(); app.UseAuthorization();
Finally, add the Identity UI by calling AddRazorPages() or using the Identity UI package. For a minimal setup, you can scaffold Identity pages.
Now you have a working authentication system with registration, login, and logout.
Customizing the Identity User and Role
The default IdentityUser and IdentityRole classes are often insufficient for real-world applications. You may need to add custom properties like FirstName, LastName, or ProfilePicture. To do this, create a class that inherits from IdentityUser:
``csharp public class ApplicationUser : IdentityUser { public string FirstName { get; set; } public string LastName { get; set; } public DateTime? DateOfBirth { get; set; } } ``
Similarly, create a custom role class if needed:
``csharp public class ApplicationRole : IdentityRole { public string Description { get; set; } } ``
Then update your DbContext to use these custom classes:
```csharp public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string> { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // Additional configuration } } ```
Finally, update the service registration to use your custom classes:
``csharp builder.Services.AddDefaultIdentity<ApplicationUser>(options => ...) .AddRoles<ApplicationRole>() .AddEntityFrameworkStores<ApplicationDbContext>(); ``
Now you can access custom properties via UserManager and RoleManager.
Configuring Identity Options
Identity provides many configuration options to control password policies, lockout settings, user requirements, and sign-in behavior. These are set in the AddDefaultIdentity method or via IdentityOptions.
- Password: RequireDigit, RequiredLength, RequireNonAlphanumeric, RequireUppercase, RequireLowercase
- Lockout: MaxFailedAccessAttempts, DefaultLockoutTimeSpan, AllowedForNewUsers
- User: RequireUniqueEmail, AllowedUserNameCharacters
- SignIn: RequireConfirmedAccount, RequireConfirmedEmail, RequireConfirmedPhoneNumber
Example configuration:
```csharp builder.Services.AddDefaultIdentity<ApplicationUser>(options => { // Password settings options.Password.RequireDigit = true; options.Password.RequiredLength = 8; options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = true; options.Password.RequireLowercase = true;
// Lockout settings options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); options.Lockout.AllowedForNewUsers = true;
// User settings options.User.RequireUniqueEmail = true; options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
// Sign-in settings options.SignIn.RequireConfirmedAccount = true; }) .AddRoles<ApplicationRole>() .AddEntityFrameworkStores<ApplicationDbContext>(); ```
These settings affect how Identity validates passwords and handles lockouts. Adjust them based on your security requirements.
Implementing User Registration and Login
With Identity configured, you can implement registration and login endpoints. Identity provides built-in Razor Pages, but you may want to create custom API endpoints for a SPA or mobile app.
Here's an example of a custom registration endpoint using UserManager:
```csharp [HttpPost("register")] public async Task<IActionResult> Register([FromBody] RegisterModel model) { var user = new ApplicationUser { UserName = model.Email, Email = model.Email, FirstName = model.FirstName, LastName = model.LastName };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded) { // Send confirmation email var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); var confirmationLink = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, token }, Request.Scheme); // Send email with confirmationLink
return Ok(new { message = "Registration successful. Please confirm your email." }); }
return BadRequest(result.Errors); } ```
For login, use SignInManager:
```csharp [HttpPost("login")] public async Task<IActionResult> Login([FromBody] LoginModel model) { var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
if (result.Succeeded) { return Ok(new { message = "Login successful" }); } if (result.RequiresTwoFactor) { return Ok(new { message = "Two-factor authentication required" }); } if (result.IsLockedOut) { return Unauthorized(new { message = "Account locked out" }); }
return Unauthorized(new { message = "Invalid login attempt" }); } ```
Remember to inject UserManager and SignInManager via constructor.
Role-Based Authorization
Roles allow you to group users and assign permissions. Identity supports roles out of the box. First, create roles and assign them to users:
```csharp // Create roles if (!await _roleManager.RoleExistsAsync("Admin")) { await _roleManager.CreateAsync(new ApplicationRole { Name = "Admin", Description = "Administrator" }); }
// Assign role to user await _userManager.AddToRoleAsync(user, "Admin"); ```
Then, protect your controllers or actions with the [Authorize] attribute:
``csharp [Authorize(Roles = "Admin")] public IActionResult ``AdminDashboard() { return View(); }
You can also check roles programmatically:
``csharp if (User.IsInRole("Admin")) { // Admin-specific logic } ``
For more granular control, use claims-based authorization. Claims are key-value pairs that can represent user attributes or permissions. You can add custom claims:
``csharp await _userManager.AddClaimAsync(user, new Claim("Permission", "EditArticles")); ``
Then create a policy:
``csharp builder.Services.AddAuthorization(options => { options.AddPolicy("EditArticles", policy => policy.RequireClaim("Permission", "EditArticles")); }); ``
And use it:
``csharp [Authorize(Policy = "EditArticles")] public IActionResult EditArticle(int id) { return ``View(); }
Claims are more flexible than roles because they can be fine-grained and don't require role management.
External Login Providers (Google, Facebook, etc.)
Identity supports external authentication providers like Google, Facebook, Microsoft, and Twitter. This allows users to log in with their existing accounts.
First, register your app with the external provider to get ClientId and ClientSecret. Then, add the authentication services:
``csharp builder.``Services.AddAuthentication() .AddGoogle(options => { options.ClientId = builder.Configuration["Authentication:Google:ClientId"]; options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]; }) .AddFacebook(options => { options.AppId = builder.Configuration["Authentication:Facebook:AppId"]; options.AppSecret = builder.Configuration["Authentication:Facebook:AppSecret"]; });
In the login page, add links to external providers:
``html <a asp-action="ExternalLogin" asp-route-provider="Google">Login with Google</a> ``
The ExternalLogin action redirects to the provider:
``csharp [HttpPost("external-login")] public IActionResult ExternalLogin(string provider, string returnUrl = null) { var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new { returnUrl }); var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); return Challenge(properties, provider); } ``
After the provider redirects back, handle the callback:
```csharp [HttpGet("external-login-callback")] public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null) { if (remoteError != null) return BadRequest(remoteError);
var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) return BadRequest("Error loading external login info");
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false); if (result.Succeeded) return RedirectToLocal(returnUrl);
// User does not have an account, prompt to create one var email = info.Principal.FindFirstValue(ClaimTypes.Email); return Ok(new { message = "External login successful, but account not linked. Provide additional info." }); } ```
You can also link multiple external providers to the same account.
AddAuthentication().ConfigureExternalAuthenticationProperties and handle the callback.JWT Authentication with Identity
For APIs and SPAs, you often need token-based authentication instead of cookies. Identity can be used with JWT (JSON Web Tokens).
First, install Microsoft.AspNetCore.Authentication.JwtBearer. Then, configure JWT authentication:
``csharp builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) }; }); ``
In the login endpoint, generate a JWT token upon successful authentication:
```csharp [HttpPost("login")] public async Task<IActionResult> Login([FromBody] LoginModel model) { var user = await _userManager.FindByEmailAsync(model.Email); if (user != null && await _userManager.CheckPasswordAsync(user, model.Password)) { var authClaims = new List<Claim> { new Claim(ClaimTypes.Name, user.UserName), new Claim(ClaimTypes.Email, user.Email), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), };
var userRoles = await _userManager.GetRolesAsync(user); foreach (var role in userRoles) { authClaims.Add(new Claim(ClaimTypes.Role, role)); }
var token = GenerateToken(authClaims); return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token), expiration = token.ValidTo }); } return Unauthorized(); }
private JwtSecurityToken GenerateToken(List<Claim> authClaims) { var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
var token = new JwtSecurityToken( issuer: _configuration["Jwt:Issuer"], audience: _configuration["Jwt:Audience"], expires: DateTime.Now.AddHours(3), claims: authClaims, signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256) );
return token; } ```
Now clients can include the token in the Authorization header: Bearer <token>.
The Lockout Storm: When Identity Lockout Policy Backfired
- Always set lockout duration to a short time (e.g., 5 minutes) to avoid permanent lockout.
- Reset failed attempt count on successful login to prevent accumulation.
- Use rate limiting on authentication endpoints to mitigate brute-force attacks.
- Monitor lockout events in production to detect anomalies.
- Consider using CAPTCHA after a few failed attempts instead of immediate lockout.
var user = await _userManager.FindByEmailAsync(email);var isConfirmed = await _userManager.IsEmailConfirmedAsync(user);| File | Command / Code | Purpose |
|---|---|---|
| Program.cs | var builder = WebApplication.CreateBuilder(args); | Setting Up ASP.NET Core Identity |
| ApplicationUser.cs | using Microsoft.AspNetCore.Identity; | Customizing the Identity User and Role |
| Program.cs (Identity Options) | builder.Services.AddDefaultIdentity | Configuring Identity Options |
| AccountController.cs | [ApiController] | Implementing User Registration and Login |
| RoleController.cs | [ApiController] | Role-Based Authorization |
| Program.cs (External Login) | builder.Services.AddAuthentication() | External Login Providers (Google, Facebook, etc.) |
| JwtController.cs | [ApiController] | JWT Authentication with Identity |
Key takeaways
Common mistakes to avoid
4 patternsNot resetting failed access attempt count on successful login
Using the same DbContext for Identity and business data without proper separation
Storing JWT secret key in source code or configuration files that are committed to source control
Not handling external login account linking correctly
Interview Questions on This Topic
Explain how ASP.NET Core Identity handles password hashing.
Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.
That's ASP.NET. Mark it forged?
5 min read · try the examples if you haven't