What is ASP.NET Identity?

ASP.NET Identity is Microsoft's membership system for .NET applications. Think of it as a complete security system for your app - handling user registration, login, password management, roles, and permissions.

Instead of building authentication from scratch (which is complex and risky), ASP.NET Identity provides a battle-tested, secure foundation that handles everything from password hashing to two-factor authentication.

Authentication vs Authorization

These terms are often confused. Think of entering a building:

AUTHENTICATION (Who are you?)
─────────────────────────────
Like showing your ID card at the entrance

Questions answered:
- Are you who you claim to be?
- Do you have valid credentials?
- Have you logged in?

Examples:
✅ Username + Password
✅ Email + Password
✅ Social login (Google, Facebook)
✅ Two-factor authentication
✅ Biometrics

---

AUTHORIZATION (What can you do?)
────────────────────────────────
Like checking which floors you can access

Questions answered:
- What are you allowed to do?
- Which resources can you access?
- What's your role/permissions?

Examples:
✅ Admin can delete users
✅ Manager can approve requests
✅ User can view own profile only
✅ Guest can only read, not write

---

ANALOGY
───────
Authentication: Proving you're John Doe (showing passport)
Authorization: Checking if John Doe is allowed in VIP lounge

Why Use ASP.NET Identity?

Building authentication yourself is like building a car from scratch when you can buy a Tesla. Here's why you should use ASP.NET Identity:

  • Security Best Practices: Built-in password hashing (PBKDF2), protection against common attacks
  • Ready-to-Use Features: User registration, login, password reset, email confirmation - all included
  • Role-Based Access: Assign roles (Admin, Manager, User) and permissions easily
  • Claims-Based Identity: Modern, flexible way to manage user information and permissions
  • Two-Factor Authentication: Built-in support for SMS, email, authenticator apps
  • Social Login: Integrate Google, Facebook, Microsoft login easily
  • Entity Framework Integration: Works seamlessly with EF Core for database storage
  • JWT Support: Perfect for APIs and mobile apps

Setting Up ASP.NET Identity

// 1. INSTALL PACKAGES
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

// 2. CREATE USER MODEL
public class ApplicationUser : IdentityUser
{
    // Add custom properties
    public string FullName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string ProfilePictureUrl { get; set; }
}

// 3. CREATE DbContext
public class AppDbContext : IdentityDbContext
{
    public AppDbContext(DbContextOptions options) : base(options) { }

    // Your other DbSets
    public DbSet Products { get; set; }
}

// 4. CONFIGURE IN Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add DbContext
builder.Services.AddDbContext(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Add Identity
builder.Services.AddIdentity(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.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
    options.Lockout.MaxFailedAccessAttempts = 5;

    // User settings
    options.User.RequireUniqueEmail = true;
    options.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores()
.AddDefaultTokenProviders();

// Add Authentication
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:SecretKey"]))
    };
});

var app = builder.Build();

// Add middleware
app.UseAuthentication();  // Who are you?
app.UseAuthorization();   // What can you do?

app.MapControllers();
app.Run();

// 5. CREATE MIGRATION
dotnet ef migrations add AddIdentity
dotnet ef database update

User Registration and Login

// REGISTER REQUEST MODEL
public class RegisterRequest
{
    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [MinLength(8)]
    public string Password { get; set; }

    [Required]
    public string FullName { get; set; }
}

// LOGIN REQUEST MODEL
public class LoginRequest
{
    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    public string Password { get; set; }
}

// AUTH CONTROLLER
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly UserManager _userManager;
    private readonly SignInManager _signInManager;
    private readonly IConfiguration _configuration;

    public AuthController(
        UserManager userManager,
        SignInManager signInManager,
        IConfiguration configuration)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _configuration = configuration;
    }

    // REGISTER
    [HttpPost("register")]
    public async Task Register(RegisterRequest model)
    {
        // Create user
        var user = new ApplicationUser
        {
            UserName = model.Email,
            Email = model.Email,
            FullName = model.FullName
        };

        var result = await _userManager.CreateAsync(user, model.Password);

        if (!result.Succeeded)
        {
            return BadRequest(result.Errors);
        }

        // Assign default role
        await _userManager.AddToRoleAsync(user, "User");

        // Generate email confirmation token
        var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);

        // Send confirmation email (implement email service)
        // await _emailService.SendConfirmationEmailAsync(user.Email, token);

        return Ok(new { Message = "User registered successfully. Please confirm your email." });
    }

    // LOGIN
    [HttpPost("login")]
    public async Task Login(LoginRequest model)
    {
        var user = await _userManager.FindByEmailAsync(model.Email);

        if (user == null)
        {
            return Unauthorized(new { Message = "Invalid email or password" });
        }

        // Check if email is confirmed
        if (!user.EmailConfirmed)
        {
            return Unauthorized(new { Message = "Please confirm your email first" });
        }

        // Check password
        var result = await _signInManager.CheckPasswordSignInAsync(user, model.Password, lockoutOnFailure: true);

        if (!result.Succeeded)
        {
            if (result.IsLockedOut)
            {
                return Unauthorized(new { Message = "Account locked. Try again later." });
            }
            return Unauthorized(new { Message = "Invalid email or password" });
        }

        // Generate JWT token
        var token = GenerateJwtToken(user);

        return Ok(new
        {
            Token = token,
            Email = user.Email,
            FullName = user.FullName
        });
    }

    // GENERATE JWT TOKEN
    private async Task GenerateJwtToken(ApplicationUser user)
    {
        var claims = new List
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id),
            new Claim(ClaimTypes.Name, user.UserName),
            new Claim(ClaimTypes.Email, user.Email),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
        };

        // Add roles to claims
        var roles = await _userManager.GetRolesAsync(user);
        foreach (var role in roles)
        {
            claims.Add(new Claim(ClaimTypes.Role, role));
        }

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"]));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        var expires = DateTime.Now.AddDays(7);

        var token = new JwtSecurityToken(
            issuer: _configuration["Jwt:Issuer"],
            audience: _configuration["Jwt:Audience"],
            claims: claims,
            expires: expires,
            signingCredentials: credentials
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    // CONFIRM EMAIL
    [HttpGet("confirm-email")]
    public async Task ConfirmEmail(string userId, string token)
    {
        var user = await _userManager.FindByIdAsync(userId);
        if (user == null)
        {
            return BadRequest("Invalid user");
        }

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

        return BadRequest("Error confirming email");
    }

    // FORGOT PASSWORD
    [HttpPost("forgot-password")]
    public async Task ForgotPassword([FromBody] string email)
    {
        var user = await _userManager.FindByEmailAsync(email);
        if (user == null)
        {
            // Don't reveal that user doesn't exist
            return Ok("If email exists, reset link has been sent");
        }

        var token = await _userManager.GeneratePasswordResetTokenAsync(user);

        // Send email with reset link
        // await _emailService.SendPasswordResetEmailAsync(email, token);

        return Ok("If email exists, reset link has been sent");
    }

    // RESET PASSWORD
    [HttpPost("reset-password")]
    public async Task ResetPassword([FromBody] ResetPasswordRequest model)
    {
        var user = await _userManager.FindByEmailAsync(model.Email);
        if (user == null)
        {
            return BadRequest("Invalid request");
        }

        var result = await _userManager.ResetPasswordAsync(user, model.Token, model.NewPassword);

        if (result.Succeeded)
        {
            return Ok("Password reset successfully");
        }

        return BadRequest(result.Errors);
    }
}

Role-Based Authorization

// SEED ROLES (run once at startup)
public static class RoleSeeder
{
    public static async Task SeedRolesAsync(RoleManager roleManager)
    {
        string[] roleNames = { "Admin", "Manager", "User" };

        foreach (var roleName in roleNames)
        {
            if (!await roleManager.RoleExistsAsync(roleName))
            {
                await roleManager.CreateAsync(new IdentityRole(roleName));
            }
        }
    }
}

// Call in Program.cs
using (var scope = app.Services.CreateScope())
{
    var roleManager = scope.ServiceProvider.GetRequiredService>();
    await RoleSeeder.SeedRolesAsync(roleManager);
}

---

// ASSIGN ROLE TO USER
[HttpPost("assign-role")]
[Authorize(Roles = "Admin")]
public async Task AssignRole(string userId, string roleName)
{
    var user = await _userManager.FindByIdAsync(userId);
    if (user == null)
    {
        return NotFound("User not found");
    }

    var result = await _userManager.AddToRoleAsync(user, roleName);

    if (result.Succeeded)
    {
        return Ok($"Role {roleName} assigned to user");
    }

    return BadRequest(result.Errors);
}

---

// PROTECT ENDPOINTS WITH ROLES
[HttpGet("admin-only")]
[Authorize(Roles = "Admin")]
public IActionResult AdminOnly()
{
    return Ok("You are an admin!");
}

[HttpGet("admin-or-manager")]
[Authorize(Roles = "Admin,Manager")]
public IActionResult AdminOrManager()
{
    return Ok("You are admin or manager");
}

[HttpGet("authenticated")]
[Authorize]  // Any logged-in user
public IActionResult Authenticated()
{
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
    var email = User.FindFirstValue(ClaimTypes.Email);
    return Ok($"Hello {email}");
}

---

// CHECK ROLES IN CODE
public async Task SomeAction()
{
    var user = await _userManager.GetUserAsync(User);

    if (await _userManager.IsInRoleAsync(user, "Admin"))
    {
        // Admin-specific logic
    }

    var roles = await _userManager.GetRolesAsync(user);
    // List of roles user belongs to
}

Claims-Based Authorization

Claims are more flexible than roles. Think of claims as attributes about a user (like "CanApproveExpenses" or "Department:Sales"):

// ADD CLAIMS TO USER
[HttpPost("add-claim")]
public async Task AddClaim(string userId, string claimType, string claimValue)
{
    var user = await _userManager.FindByIdAsync(userId);

    var claim = new Claim(claimType, claimValue);
    await _userManager.AddClaimAsync(user, claim);

    return Ok("Claim added");
}

// Examples:
// ("Department", "Sales")
// ("CanApproveExpenses", "true")
// ("MaxApprovalAmount", "10000")

---

// POLICY-BASED AUTHORIZATION
// Program.cs
builder.Services.AddAuthorization(options =>
{
    // Require specific claim
    options.AddPolicy("CanApproveExpenses", policy =>
        policy.RequireClaim("CanApproveExpenses", "true"));

    // Require role AND claim
    options.AddPolicy("SeniorManager", policy =>
        policy.RequireRole("Manager")
              .RequireClaim("Seniority", "Senior"));

    // Custom requirement
    options.AddPolicy("Over18", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(18)));
});

---

// USE IN CONTROLLER
[HttpPost("approve-expense")]
[Authorize(Policy = "CanApproveExpenses")]
public IActionResult ApproveExpense(int expenseId)
{
    // Only users with "CanApproveExpenses" claim can access
    return Ok("Expense approved");
}

---

// CUSTOM AUTHORIZATION HANDLER
public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }
    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

public class MinimumAgeHandler : AuthorizationHandler
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst(c => c.Type == "DateOfBirth");

        if (dateOfBirthClaim != null)
        {
            var dateOfBirth = DateTime.Parse(dateOfBirthClaim.Value);
            var age = DateTime.Today.Year - dateOfBirth.Year;

            if (age >= requirement.MinimumAge)
            {
                context.Succeed(requirement);
            }
        }

        return Task.CompletedTask;
    }
}

// Register handler
builder.Services.AddSingleton();

JWT Authentication for APIs

JWT (JSON Web Token) is perfect for APIs and mobile apps. It's like a digitally signed passport that proves who you are:

// appsettings.json
{
  "Jwt": {
    "SecretKey": "YourVeryLongSecretKeyHere_AtLeast32Characters!",
    "Issuer": "YourAppName",
    "Audience": "YourAppUsers",
    "ExpiryMinutes": 60
  }
}

// HOW JWT WORKS
1. User logs in with username/password
2. Server validates credentials
3. Server generates JWT token with user info
4. Server sends token to client
5. Client stores token (localStorage/secure storage)
6. Client sends token with every API request
7. Server validates token and processes request

---

// JWT TOKEN STRUCTURE
Header.Payload.Signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.      ← Header
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikp     ← Payload (user info)
vaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c   ← Signature

Payload contains:
- User ID
- Email
- Roles
- Claims
- Expiration time

---

// CLIENT USAGE (JavaScript)
// After login, save token
localStorage.setItem('token', response.token);

// Send with every request
fetch('https://api.example.com/products', {
    headers: {
        'Authorization': `Bearer ${localStorage.getItem('token')}`
    }
});

---

// CLIENT USAGE (C# Blazor)
// Save token
await localStorage.SetItemAsync("token", loginResponse.Token);

// HTTP Client with token
public class AuthenticatedHttpClient
{
    private readonly HttpClient _http;
    private readonly ILocalStorageService _localStorage;

    public async Task GetAsync(string url)
    {
        var token = await _localStorage.GetItemAsync("token");
        _http.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);

        return await _http.GetAsync(url);
    }
}

Two-Factor Authentication (2FA)

// ENABLE 2FA FOR USER
[HttpPost("enable-2fa")]
[Authorize]
public async Task EnableTwoFactor()
{
    var user = await _userManager.GetUserAsync(User);

    await _userManager.SetTwoFactorEnabledAsync(user, true);

    // Generate authenticator key
    var key = await _userManager.GetAuthenticatorKeyAsync(user);
    if (string.IsNullOrEmpty(key))
    {
        await _userManager.ResetAuthenticatorKeyAsync(user);
        key = await _userManager.GetAuthenticatorKeyAsync(user);
    }

    // Return QR code data for authenticator app
    var authenticatorUri = $"otpauth://totp/YourApp:{user.Email}?secret={key}&issuer=YourApp";

    return Ok(new { Key = key, QrCodeUri = authenticatorUri });
}

// VERIFY 2FA CODE
[HttpPost("verify-2fa")]
public async Task VerifyTwoFactor(string email, string password, string code)
{
    var user = await _userManager.FindByEmailAsync(email);

    // Verify password first
    var passwordValid = await _userManager.CheckPasswordAsync(user, password);
    if (!passwordValid)
    {
        return Unauthorized("Invalid credentials");
    }

    // Verify 2FA code
    var isValid = await _userManager.VerifyTwoFactorTokenAsync(
        user,
        _userManager.Options.Tokens.AuthenticatorTokenProvider,
        code);

    if (!isValid)
    {
        return Unauthorized("Invalid 2FA code");
    }

    // Generate token
    var token = GenerateJwtToken(user);
    return Ok(new { Token = token });
}

Security Best Practices

  • Never store passwords in plain text: ASP.NET Identity handles hashing automatically
  • Use HTTPS everywhere: Tokens and passwords should never travel over HTTP
  • Set strong password requirements: Minimum 8 characters, mixed case, numbers
  • Implement account lockout: Prevent brute-force attacks
  • Use secure JWT secret keys: At least 32 characters, randomly generated
  • Set appropriate token expiration: Shorter for sensitive apps (1 hour), longer for convenience (7 days)
  • Implement refresh tokens: For long-lived sessions without compromising security
  • Validate email addresses: Confirm ownership before activation
  • Log authentication events: Track failed login attempts, password changes
  • Use claims for fine-grained access: More flexible than roles alone
  • Implement rate limiting: Prevent abuse of login endpoints
  • Consider 2FA for sensitive operations: Not just login, but also critical actions

Common Authentication Patterns

PATTERN 1: Cookie-Based (Traditional Web Apps)
───────────────────────────────────────────────
- User logs in
- Server creates session cookie
- Cookie sent with every request
- Good for: MVC apps, Blazor Server

---

PATTERN 2: JWT Token (APIs, SPAs, Mobile)
──────────────────────────────────────────
- User logs in
- Server returns JWT token
- Client stores token
- Token sent in Authorization header
- Good for: React/Angular SPAs, Mobile apps, Microservices

---

PATTERN 3: OAuth2 / OpenID Connect (Enterprise)
────────────────────────────────────────────────
- Login via external provider (Google, Microsoft, Okta)
- Receive identity token
- Use token to access resources
- Good for: Enterprise apps, SSO scenarios

---

PATTERN 4: API Keys (Simple APIs)
──────────────────────────────────
- Generate API key for user
- Include in request header
- Server validates key
- Good for: Simple APIs, IoT, webhooks

Master Security with Expert Mentorship

Our Full Stack .NET program covers authentication and authorization in depth. Build secure applications with industry best practices and personalized guidance.

Explore Full Stack .NET Program

Related Articles