DEV Community

Cover image for 🧱 Lesson 8 - Authentication & Authorization in .NET
Farrukh Rehman
Farrukh Rehman

Posted on

🧱 Lesson 8 - Authentication & Authorization in .NET

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
Enter fullscreen mode Exit fullscreen mode

🧱 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; }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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.
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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);
Enter fullscreen mode Exit fullscreen mode

✅ 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));
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Step 6 — Update Program.cs

app.UseAuthentication();
app.UseAuthorization();
Enter fullscreen mode Exit fullscreen mode

✅ 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");
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Step 8 — Add the Above Seeting in Program.cs

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    await IdentitySeeder.SeedRolesAndAdminAsync(services);
}
Enter fullscreen mode Exit fullscreen mode

✅ 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"
Enter fullscreen mode Exit fullscreen mode

✅ 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 
Enter fullscreen mode Exit fullscreen mode

✅ 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)