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);
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
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
✅ Step 11 — Register IAuthService in DependencyInjection.cs
services.AddScoped<IAuthService, AuthService>();
✅ 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 });
}
}
✅ 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");
✅ Step 14 — Update appsettings.json
"Cors": {
"AllowedOrigins": [
"http://localhost:4200",
"https://localhost:4200"
]
}
✅ 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
✅ Step 17 — Lets Product Controller
Add Token
Next Lecture Preview
Lecture 9A : Login Frontend Integration
Creating login pages, implementing HTTP interceptors, error handling, and integrating frontend with backend authentication APIs.


















Top comments (2)
Next Lesson Please!. Congratulation. Thank You Farrukh. From Peru,Arequipa.
Hi Herik, thanks for keeping me posted. Here is the next lesson:
dev.to/farrukh_rehman/lesson-9a-lo...