Series: From Code to Cloud: Building a Production-Ready .NET Application
By: Farrukh Rehman - Senior .NET Full Stack Developer / Team Lead
LinkedIn: https://linkedin.com/in/farrukh-rehman
GitHub: https://github.com/farrukh1212cs
Source Code Backend : https://github.com/farrukh1212cs/ECommerce-Backend.git
Source Code Frontend : https://github.com/farrukh1212cs/ECommerce-Frontend.git
🎯 Introduction
add secure user authentication (JWT), 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
}
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:
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);
✅ 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
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();
✅ 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 = "admin@ecommerce.com";
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);
}
✅ 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
Seedind Done
Verify Data in DB
Next Lecture Preview
Lecture 9 : Building the Backoffice Web App (Angular/React)
Creating a modern admin UI for managing products, orders, and users, integrated with the backend API.







Top comments (0)