Previous lesson: 🧱 Lesson 7 - Message Queues with RabbitMQ
🎯 Introduction
Add secure user authentication (JWT) and role-based authorization. We'll use patterns that fit your Clean Architecture: interfaces in Application, implementations in Infrastructure, DI wiring in DependencyInjection, and usage in the API.
🔐 What you'll learn (brief)
JWT-based auth (register / login / issue token)
Role-based auth and policy-based authorization.
Seeding admin/user roles
Protecting controllers/endpoints with [Authorize]
Refresh token strategy (overview + simple approach)
Optional: integrate Azure AD or IdentityServer (high-level steps)
📦 Packages to install
(choose versions compatible with .NET 8; run from solution root)
dotnet add ECommerce.Infrastructure package Microsoft.AspNetCore.Authentication.JwtBearer --version 8.0.21
dotnet add ECommerce.Infrastructure package Microsoft.AspNetCore.Identity.EntityFrameworkCore --version 8.0.21
dotnet add ECommerce.Domain package Microsoft.AspNetCore.Identity.EntityFrameworkCore --version 8.0.21
dotnet add ECommerce.Infrastructure package Microsoft.IdentityModel.Tokens
dotnet add ECommerce.Application package System.IdentityModel.Tokens.Jwt
🧱 High-level architecture
Identity stores (users/roles) live in your DB (via EF stores).
IAuthService in Application defines operations (Register, Login, Validate).
AuthService in Infrastructure implements token generation and uses UserManager/RoleManager.
Program.cs / DependencyInjection wires Identity + JwtBearer authentication + role seeding.
🔧 appsettings.json (JWT config example)
"JwtSettings": {
"Issuer": "ECommerceApi",
"Audience": "ECommerceClients",
"Secret": "3A6DA077-8EBC-4DA9-94DF-2C246564E749",
"ExpiresInMinutes": 60,
"RefreshTokenExpiresInDays": 30
}
![1]()
Keep Secret in secure store (Key Vault / environment variables) in production.
✅ Step 1 — Create Application User Entity
Path: ECommerce.Domain/Entities/ApplicationUser.cs
using Microsoft.AspNetCore.Identity;
namespace ECommerce.Domain.Entities;
public class ApplicationUser : IdentityUser<Guid>
{
// Add extra properties if needed (FirstName, LastName)
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
✅ Step 2 — Add Identity entities & DbContext
we already have AppDbContext derived from DbContext, extend it to use Identity:
![2]()
using ECommerce.Domain.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace ECommerce.Infrastructure.Data;
public abstract class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
// DbSets
public DbSet<Product> Products { get; set; } = null!;
public DbSet<Customer> Customers { get; set; } = null!;
public DbSet<Order> Orders { get; set; } = null!;
public DbSet<OrderItem> OrderItems { get; set; } = null!;
public DbSet<ApplicationUser> ApplicationUser { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Product
modelBuilder.Entity<Product>(b =>
{
b.ToTable("Products");
b.HasKey(p => p.Id);
b.Property(p => p.Name).IsRequired().HasMaxLength(200);
b.Property(p => p.Price).HasColumnType("decimal(18,2)");
});
// Customer
modelBuilder.Entity<Customer>(b =>
{
b.ToTable("Customers");
b.HasKey(c => c.Id);
b.Property(c => c.FirstName).IsRequired().HasMaxLength(200);
b.Property(c => c.Email).IsRequired().HasMaxLength(256);
b.HasMany(c => c.Orders)
.WithOne(o => o.Customer)
.HasForeignKey(o => o.CustomerId)
.OnDelete(DeleteBehavior.Cascade);
});
// Order
modelBuilder.Entity<Order>(b =>
{
b.ToTable("Orders");
b.HasKey(o => o.Id);
b.Property(o => o.OrderDate).IsRequired();
b.Property(o => o.TotalAmount).HasColumnType("decimal(18,2)");
b.HasMany(o => o.Items)
.WithOne(oi => oi.Order)
.HasForeignKey(oi => oi.OrderId)
.OnDelete(DeleteBehavior.Cascade);
});
// OrderItem
modelBuilder.Entity<OrderItem>(b =>
{
b.ToTable("OrderItems");
b.HasKey(oi => oi.Id);
b.Property(oi => oi.ProductName).IsRequired().HasMaxLength(200);
b.Property(oi => oi.UnitPrice).HasColumnType("decimal(18,2)");
b.Property(oi => oi.Quantity).IsRequired();
});
// If you want to seed or add indexes later, do it here.
}
}
✅ Step 3 — Define IAuthService (Application layer)
Path: ECommerce.Application/Services/Interfaces/IAuthService.cs
namespace ECommerce.Application.Services.Interfaces;
public interface IAuthService
{
Task<AuthenticationResult> RegisterAsync(RegisterRequest request);
Task<AuthenticationResult> LoginAsync(LoginRequest request);
Task<AuthenticationResult> RefreshTokenAsync(string token, string refreshToken);
}
public record RegisterRequest(string Email, string Password, string? FirstName = null, string? LastName = null);
public record LoginRequest(string Email, string Password);
public record AuthenticationResult(bool Success, string? Token, string? RefreshToken, IEnumerable<string>? Errors);
public record RefreshRequest(string Token, string RefreshToken);
✅ Step 4 — Implement AuthService (Infrastructure)
Path: ECommerce.Application/Services/Implementations/AuthService.cs
using ECommerce.Application.Services.Interfaces;
using ECommerce.Domain.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace ECommerce.Application.Services.Implementations;
public class AuthService : IAuthService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IConfiguration _config;
private readonly RoleManager<IdentityRole<Guid>> _roleManager;
public AuthService(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole<Guid>> roleManager, IConfiguration config)
{
_userManager = userManager;
_roleManager = roleManager;
_config = config;
}
public async Task<AuthenticationResult> RegisterAsync(RegisterRequest request)
{
var existing = await _userManager.FindByEmailAsync(request.Email);
if (existing != null)
return new AuthenticationResult(false, null, null, new[] { "User already exists" });
var user = new ApplicationUser { Email = request.Email, UserName = request.Email, FirstName = request.FirstName, LastName = request.LastName };
var result = await _userManager.CreateAsync(user, request.Password);
if (!result.Succeeded)
return new AuthenticationResult(false, null, null, result.Errors.Select(e => e.Description));
// Optionally add default role
await _userManager.AddToRoleAsync(user, "User");
// generate tokens
var token = await GenerateJwtToken(user);
var refreshToken = GenerateRefreshToken(); // implement secure random token and store it
// persist refresh token with user (e.g., in DB)
// ...
return new AuthenticationResult(true, token, refreshToken, null);
}
public async Task<AuthenticationResult> LoginAsync(LoginRequest request)
{
var user = await _userManager.FindByEmailAsync(request.Email);
if (user == null) return new AuthenticationResult(false, null, null, new[] { "Invalid credentials" });
if (!await _userManager.CheckPasswordAsync(user, request.Password))
return new AuthenticationResult(false, null, null, new[] { "Invalid credentials" });
var token = await GenerateJwtToken(user);
var refreshToken = GenerateRefreshToken();
// store refresh token...
return new AuthenticationResult(true, token, refreshToken, null);
}
public Task<AuthenticationResult> RefreshTokenAsync(string token, string refreshToken)
{
// validate existing refresh token stored in DB, expiration, rotation, etc.
throw new NotImplementedException();
}
private async Task<string> GenerateJwtToken(ApplicationUser user)
{
var jwt = _config.GetSection("JwtSettings");
var secret = jwt["Secret"]!;
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email!),
new Claim(ClaimTypes.Name, user.UserName!)
};
var userRoles = await _userManager.GetRolesAsync(user);
claims.AddRange(userRoles.Select(r => new Claim(ClaimTypes.Role, r)));
var token = new JwtSecurityToken(
issuer: jwt["Issuer"],
audience: jwt["Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(double.Parse(jwt["ExpiresInMinutes"] ?? "60")),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
private static string GenerateRefreshToken()
{
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
}
}
✅ Step 5 — Wire up Identity, JwtBearer and DI
![3]()
using ECommerce.API.BackgroundServices;
using ECommerce.Application.Services.Interfaces;
using ECommerce.Infrastructure.Caching;
using ECommerce.Infrastructure.Data;
using ECommerce.Infrastructure.Email;
using ECommerce.Infrastructure.Messaging;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using StackExchange.Redis;
using System.Text;
namespace ECommerce.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
var provider = configuration["DatabaseProvider"] ?? "MySQL";
if (string.Equals(provider, "SqlServer", StringComparison.OrdinalIgnoreCase))
{
var conn = configuration.GetConnectionString("SqlServer");
services.AddDbContext<AppDbContext, SqlServerDbContext>(options =>
options.UseSqlServer(conn));
}
else if (string.Equals(provider, "MySQL", StringComparison.OrdinalIgnoreCase))
{
var conn = configuration.GetConnectionString("MySQL");
services.AddDbContext<AppDbContext, MySqlDbContext>(options =>
options.UseMySql(conn, ServerVersion.AutoDetect(conn)));
}
else if (string.Equals(provider, "PostgreSQL", StringComparison.OrdinalIgnoreCase))
{
var conn = configuration.GetConnectionString("PostgreSQL");
services.AddDbContext<AppDbContext, PostgresDbContext>(options =>
options.UseNpgsql(conn));
}
else
{
throw new InvalidOperationException($"Unsupported provider: {provider}");
}
// ✅ Add Identity
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
// ✅ JWT Authentication setup
var jwtSettings = configuration.GetSection("JwtSettings");
var key = Encoding.UTF8.GetBytes(jwtSettings["Secret"]);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings["Issuer"],
ValidAudience = jwtSettings["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(key)
};
});
// ✅ Authorization
services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
options.AddPolicy("CustomerOnly", policy => policy.RequireRole("Customer"));
});
// ✅ Redis cache setup
var redisConnection = configuration.GetConnectionString("Redis");
if (!string.IsNullOrEmpty(redisConnection))
{
services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(redisConnection));
services.AddSingleton<ICacheService, RedisCacheService>();
}
// RabbitMQ setup
services.AddSingleton<IMessageQueueService, RabbitMQService>();
services.AddScoped<IEmailSenderService, EmailSenderService>();
return services;
}
}
✅ Step 6 — Update Program.cs
app.UseAuthentication();
app.UseAuthorization();
![4]()
✅ Step 7 — Seed roles and an admin user
Path: ECommerce.Infrastructure/Identity/IdentitySeeder.cs
using ECommerce.Domain.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;
namespace ECommerce.Infrastructure.Identity
{
public static class IdentitySeeder
{
public static async Task SeedRolesAndAdminAsync(IServiceProvider serviceProvider)
{
// Use the correct types matching your Identity setup
var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole<Guid>>>();
var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
string[] roles = { "Admin", "Customer" };
// Ensure roles exist
foreach (var role in roles)
{
if (!await roleManager.RoleExistsAsync(role))
{
await roleManager.CreateAsync(new IdentityRole<Guid>(role));
}
}
// Create default admin user if missing
var adminEmail = "[email protected]";
var adminUser = await userManager.FindByEmailAsync(adminEmail);
if (adminUser == null)
{
adminUser = new ApplicationUser
{
UserName = adminEmail,
Email = adminEmail,
EmailConfirmed = true
};
var result = await userManager.CreateAsync(adminUser, "Admin@123");
if (result.Succeeded)
{
await userManager.AddToRoleAsync(adminUser, "Admin");
}
}
}
}
}
✅ Step 8 — Add the Above Seeting in Program.cs
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
await IdentitySeeder.SeedRolesAndAdminAsync(services);
}
![5]()
✅ Step 9 — Lets Create Migrations for All DBs
dotnet ef migrations add IdentityMySQL -p ECommerce.Infrastructure -s ECommerce.API --context MySqlDbContext --output-dir "Migrations/MySQL"
dotnet ef migrations add IdentitySqlServer -p ECommerce.Infrastructure -s ECommerce.API --context SqlServerDbContext --output-dir "Migrations/SqlServer"
dotnet ef migrations add IdentityPostgreSQL -p ECommerce.Infrastructure -s ECommerce.API --context PostgresDbContext --output-dir "Migrations/PostgreSQL"
✅ Step 10 — Update DB
dotnet ef database update -p ECommerce.Infrastructure -s ECommerce.API --context MySqlDbContext
dotnet ef database update -p ECommerce.Infrastructure -s ECommerce.API --context SqlServerDbContext
dotnet ef database update -p ECommerce.Infrastructure -s ECommerce.API --context PostgresDbContext
✅ Step 10 — Let Run and Seed Data
![6]()
Verify Data in DB
![7]()
✅ Step 11 — Register IAuthService in DependencyInjection.cs
services.AddScoped<IAuthService, AuthService>();
![8]()
✅ Step 12 — Create Auth Controller
using ECommerce.Application.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ECommerce.API.Controllers;
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly IAuthService _auth;
public AuthController(IAuthService auth) => _auth = auth;
[HttpPost("register")]
public async Task<IActionResult> Register(Application.Services.Interfaces.RegisterRequest req)
{
var res = await _auth.RegisterAsync(req);
if (!res.Success) return BadRequest(res.Errors);
return Ok(new { token = res.Token, refreshToken = res.RefreshToken });
}
[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> Login(Application.Services.Interfaces.LoginRequest req)
{
var res = await _auth.LoginAsync(req);
if (!res.Success) return Unauthorized(res.Errors);
return Ok(new { token = res.Token, refreshToken = res.RefreshToken });
}
[HttpPost("refresh")]
public async Task<IActionResult> Refresh(Application.Services.Interfaces.RefreshRequest req)
{
var res = await _auth.RefreshTokenAsync(req.Token, req.RefreshToken);
if (!res.Success) return Unauthorized(res.Errors);
return Ok(new { token = res.Token, refreshToken = res.RefreshToken });
}
}
![9]()
✅ Step 13 — Lets Work on CORS for Angular
Update Program file, Add Core Configurations after swagger Step 1
// ------------------------------------------------------
// CORS
// ------------------------------------------------------
var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins(allowedOrigins!)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
Step 2: Use Cors after Redirection
app.UseCors("AllowFrontend");
![10]()
✅ Step 14 — Update appsettings.json
"Cors": {
"AllowedOrigins": [
"http://localhost:4200",
"https://localhost:4200"
]
}
![11]()
✅ Step 15 — Configure Auth Settings in Swagger
Update in Programfile, Replace builder.Services.AddSwaggerGen() with below code
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = "ECommerce API", Version = "v1" });
// Add JWT Authentication to Swagger
c.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Name = "Authorization",
Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
In = Microsoft.OpenApi.Models.ParameterLocation.Header,
Description = "Enter JWT token: Bearer {your token}"
});
c.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement
{
{
new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Reference = new Microsoft.OpenApi.Models.OpenApiReference
{
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
});
});
✅ Step 16 — Lets Auth Test Application
![12]()
![13]()
![14]()
✅ Step 17 — Lets Product Controller
![15]()
Add Token
![16]()
![17]()
![18]()
Next Lecture Preview
Lecture 9A : Login Frontend Integration
Creating login pages, implementing HTTP interceptors, error handling, and integrating frontend with backend authentication APIs.