Home C# / .NET ASP.NET Core Identity Complete Guide: Authentication & Authorization
Advanced 5 min · July 13, 2026

ASP.NET Core Identity Complete Guide: Authentication & Authorization

Master ASP.NET Core Identity for secure authentication and authorization.

N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 13, 2026
last updated
2,043
articles · all by Naren
Before you start⏱ 20-25 min read
  • 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
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

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.

✦ Definition~90s read
What is ASP.NET Core Identity Complete?

ASP.NET Core Identity is a membership system that adds authentication and authorization to your ASP.NET Core applications, handling user registration, login, password management, roles, claims, and external login providers.

Think of ASP.NET Core Identity as a bouncer for your website.
Plain-English First

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.

``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.

Program.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
    options.SignIn.RequireConfirmedAccount = true;
    options.Password.RequireDigit = true;
    options.Password.RequiredLength = 8;
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
})
.AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

var app = builder.Build();

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

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

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

app.MapRazorPages();

app.Run();
💡Use Individual User Accounts Template
📊 Production Insight
Always require confirmed email in production to prevent spam accounts. Set lockout duration to a short time (5 minutes) to avoid permanent lockouts.
🎯 Key Takeaway
Add Identity services with AddDefaultIdentity and configure options for passwords, lockout, and sign-in requirements.

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; } } ``

``csharp public class ApplicationRole : IdentityRole { public string Description { get; set; } } ``

```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.

ApplicationUser.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using Microsoft.AspNetCore.Identity;

public class ApplicationUser : IdentityUser
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime? DateOfBirth { get; set; }
}

public class ApplicationRole : IdentityRole
{
    public string Description { get; set; }
}

public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
    }
}
🔥IdentityDbContext Generic Parameters
📊 Production Insight
Avoid storing sensitive data like social security numbers in the user table. Use separate tables with encryption if needed.
🎯 Key Takeaway
Extend IdentityUser and IdentityRole to add custom properties, and update DbContext accordingly.

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.

Common options include
  • Password: RequireDigit, RequiredLength, RequireNonAlphanumeric, RequireUppercase, RequireLowercase
  • Lockout: MaxFailedAccessAttempts, DefaultLockoutTimeSpan, AllowedForNewUsers
  • User: RequireUniqueEmail, AllowedUserNameCharacters
  • SignIn: RequireConfirmedAccount, RequireConfirmedEmail, RequireConfirmedPhoneNumber

```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.

Program.cs (Identity Options)CSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
builder.Services.AddDefaultIdentity<ApplicationUser>(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequiredLength = 8;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequireUppercase = true;
    options.Password.RequireLowercase = true;
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
    options.Lockout.AllowedForNewUsers = true;
    options.User.RequireUniqueEmail = true;
    options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
    options.SignIn.RequireConfirmedAccount = true;
})
.AddRoles<ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
⚠ Password Complexity vs Usability
📊 Production Insight
In production, set RequireConfirmedAccount to true to prevent unverified accounts from logging in.
🎯 Key Takeaway
Configure Identity options to enforce password complexity, lockout policies, and user 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); } ```

```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.

AccountController.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
[ApiController]
[Route("api/[controller]")]
public class AccountController : ControllerBase
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;

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

    [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)
        {
            var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
            var confirmationLink = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, token }, Request.Scheme);
            // Send email logic here

            return Ok(new { message = "Registration successful. Please confirm your email." });
        }

        return BadRequest(result.Errors);
    }

    [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" });
    }

    [HttpGet("confirm-email")]
    public async Task<IActionResult> ConfirmEmail(string userId, string token)
    {
        if (userId == null || token == null)
            return BadRequest();

        var user = await _userManager.FindByIdAsync(userId);
        if (user == null)
            return NotFound();

        var result = await _userManager.ConfirmEmailAsync(user, token);
        if (result.Succeeded)
            return Ok(new { message = "Email confirmed" });

        return BadRequest(result.Errors);
    }
}
💡Use SignInManager for Login
📊 Production Insight
Always set lockoutOnFailure to true to prevent brute-force attacks. Send confirmation email immediately after registration.
🎯 Key Takeaway
Use UserManager for user creation and management, and SignInManager for login/logout operations.

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(); } ``

``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")); ``

``csharp builder.Services.AddAuthorization(options => { options.AddPolicy("EditArticles", policy => policy.RequireClaim("Permission", "EditArticles")); }); ``

``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.

RoleController.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
[ApiController]
[Route("api/[controller]")]
public class RoleController : ControllerBase
{
    private readonly RoleManager<ApplicationRole> _roleManager;
    private readonly UserManager<ApplicationUser> _userManager;

    public RoleController(RoleManager<ApplicationRole> roleManager, UserManager<ApplicationUser> userManager)
    {
        _roleManager = roleManager;
        _userManager = userManager;
    }

    [HttpPost("create")]
    public async Task<IActionResult> CreateRole([FromBody] string roleName)
    {
        if (!await _roleManager.RoleExistsAsync(roleName))
        {
            var role = new ApplicationRole { Name = roleName };
            var result = await _roleManager.CreateAsync(role);
            if (result.Succeeded)
                return Ok(new { message = "Role created" });
            return BadRequest(result.Errors);
        }
        return BadRequest("Role already exists");
    }

    [HttpPost("assign")]
    public async Task<IActionResult> AssignRole(string userId, string roleName)
    {
        var user = await _userManager.FindByIdAsync(userId);
        if (user == null)
            return NotFound();

        var result = await _userManager.AddToRoleAsync(user, roleName);
        if (result.Succeeded)
            return Ok(new { message = "Role assigned" });
        return BadRequest(result.Errors);
    }
}
🔥Claims vs Roles
📊 Production Insight
Avoid hardcoding role names in code. Store them in configuration or database and use constants.
🎯 Key Takeaway
Use roles for broad access control and claims for fine-grained permissions. Combine both with policies.

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"]; }); ``

``html <a asp-action="ExternalLogin" asp-route-provider="Google">Login with Google</a> ``

``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); } ``

```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.

Program.cs (External Login)CSHARP
1
2
3
4
5
6
7
8
9
10
11
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"];
    });
⚠ Store Secrets Securely
📊 Production Insight
Always handle the case where the external login email is already associated with another account. Prompt to link or merge.
🎯 Key Takeaway
Add external authentication providers via 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>.

JwtController.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly IConfiguration _configuration;

    public AuthController(UserManager<ApplicationUser> userManager, IConfiguration configuration)
    {
        _userManager = userManager;
        _configuration = configuration;
    }

    [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;
    }
}
💡Token Expiration
📊 Production Insight
Store JWT secret key securely (e.g., Azure Key Vault). Rotate keys periodically and implement token revocation if needed.
🎯 Key Takeaway
Use JWT for stateless authentication in APIs. Generate tokens with claims and validate them with JwtBearer middleware.
● Production incidentPOST-MORTEMseverity: high

The Lockout Storm: When Identity Lockout Policy Backfired

Symptom
Users reported being unable to log in, receiving 'account locked' errors even after entering correct passwords.
Assumption
The development team assumed the lockout policy was safe because it was set to 5 failed attempts in 5 minutes.
Root cause
The lockout duration was set to 24 hours, and the failed attempt counter was not reset on successful login. During a DDoS attack, attackers triggered many failed attempts, locking out real users permanently.
Fix
Reduced lockout duration to 5 minutes, reset failed attempt count on successful login, and implemented rate limiting on login endpoint.
Key lesson
  • 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.
Production debug guideSymptom to Action4 entries
Symptom · 01
User cannot log in despite correct credentials
Fix
Check if account is locked out (UserManager.IsLockedOutAsync). Check lockout end date. Verify password hashing algorithm matches.
Symptom · 02
External login (Google/Facebook) fails
Fix
Check external authentication provider configuration (ClientId, ClientSecret). Ensure callback URLs match. Check SSL/TLS settings.
Symptom · 03
Role-based authorization not working
Fix
Verify roles are assigned to user (UserManager.IsInRoleAsync). Check role names are case-sensitive. Ensure authorization policies are correctly configured.
Symptom · 04
Email confirmation link expired
Fix
Check token lifespan settings (IdentityOptions.Tokens.EmailConfirmationTokenProvider). Ensure email sending is working. Verify token generation and validation.
★ Quick Debug Cheat Sheet for ASP.NET Core IdentityCommon issues and immediate actions
Login fails with 'Invalid login attempt'
Immediate action
Check if user exists and email is confirmed
Commands
var user = await _userManager.FindByEmailAsync(email);
var isConfirmed = await _userManager.IsEmailConfirmedAsync(user);
Fix now
If not confirmed, resend confirmation email or auto-confirm in development.
User locked out+
Immediate action
Check lockout end date and reset if needed
Commands
var lockoutEnd = await _userManager.GetLockoutEndDateAsync(user);
await _userManager.SetLockoutEndDateAsync(user, null);
Fix now
Set lockout duration to a short time in production.
External login returns error+
Immediate action
Check external provider configuration
Commands
Check appsettings.json for ClientId and ClientSecret
Verify callback URL matches the provider's registered redirect URI
Fix now
Reconfigure the external provider with correct URLs.
FeatureCookie AuthenticationJWT Authentication
StateStateful (server-side session)Stateless (client-side token)
StorageCookie in browserToken in localStorage or header
ScalabilityRequires session affinity or distributed cacheHorizontally scalable without shared state
Use CaseServer-rendered apps (MVC, Razor Pages)APIs, SPAs, mobile apps
SecurityCSRF protection built-inMust handle XSS and token storage carefully
ImplementationBuilt-in with Identity UIRequires JWT middleware and token generation
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
Program.csvar builder = WebApplication.CreateBuilder(args);Setting Up ASP.NET Core Identity
ApplicationUser.csusing Microsoft.AspNetCore.Identity;Customizing the Identity User and Role
Program.cs (Identity Options)builder.Services.AddDefaultIdentity(options =>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

1
ASP.NET Core Identity provides a complete authentication and authorization system with user management, roles, claims, and external logins.
2
Customize Identity by extending IdentityUser and IdentityRole, and configure options for passwords, lockout, and sign-in.
3
Use UserManager for user operations and SignInManager for login/logout. For APIs, use JWT tokens with Identity.
4
Always follow security best practices
require email confirmation, set short lockout durations, store secrets securely, and use HTTPS.
5
Monitor authentication events in production to detect brute-force attacks and misconfigurations.

Common mistakes to avoid

4 patterns
×

Not 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 PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how ASP.NET Core Identity handles password hashing.
Q02SENIOR
How would you implement custom validation for user registration (e.g., u...
Q03SENIOR
What are the differences between cookie authentication and JWT authentic...
Q04JUNIOR
How can you secure an API endpoint so that only users with a specific cl...
Q05SENIOR
Explain the lockout feature in Identity and how to configure it for prod...
Q01 of 05SENIOR

Explain how ASP.NET Core Identity handles password hashing.

ANSWER
Identity uses the PasswordHasher<TUser> class, which by default uses PBKDF2 with a random salt and multiple iterations. The hash includes the algorithm version, salt, and hash. When verifying, it extracts the salt and recomputes the hash. The default implementation is ASP.NET Core Identity V3, which uses HMACSHA256 with 10,000 iterations.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between AddIdentity and AddDefaultIdentity?
02
How do I reset a user's password in ASP.NET Core Identity?
03
Can I use Identity with a non-relational database?
04
How do I implement two-factor authentication (2FA) with Identity?
05
What is the purpose of UserManager and SignInManager?
N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 13, 2026
last updated
2,043
articles · all by Naren
🔥

That's ASP.NET. Mark it forged?

5 min read · try the examples if you haven't

Previous
Introduction to .NET MAUI
34 / 35 · ASP.NET
Next
Data Protection in ASP.NET Core