ASP.NET Core MVC Full Project Part 10 | ASP.NET Core Identity Authentication & Authorization

 

In this video, we are building a complete ASP.NET Core Identity Authentication System inside our Mobile Shop Website Project. We’ll implement registration, login, logout, authentication cookies, authorization, password hashing, and full Identity flow step by step.

Step 1: Creating the Unified Account Data Blueprints

To establish a secure onboarding funnel, create a file named AccountViewModel.cs inside your ViewModels directory. We will start by implementing the RegisterViewModel to handle new user registrations for the Mobile Shop storefront.

The Code: ViewModels/AccountViewModel.cs


C# / ViewModels/RegisterViewModel.cs

public class RegisterViewModel
{
    [Required(ErrorMessage = "First name is required")]
    [Display(Name = "First Name")]
    [StringLength(50)]
    public string FirstName { get; set; } = string.Empty;

    [Required(ErrorMessage = "Last name is required")]
    [Display(Name = "Last Name")]
    [StringLength(50)]
    public string LastName { get; set; } = string.Empty;

    [Required(ErrorMessage = "Email is required")]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { get; set; } = string.Empty;

    [Required(ErrorMessage = "Phone number is required")]
    [Phone]
    [Display(Name = "Phone Number")]
    public string PhoneNumber { get; set; } = string.Empty;

    [Required(ErrorMessage = "Password is required")]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; } = string.Empty;

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; } = string.Empty;
}

Step 2: Implementing the Account Controller and Register View Gateway

Create an AccountController.cs file inside your Controllers folder. This controller will serve as the core engine handling all user authentication traffic for your Mobile Shop.

The Code: Controllers/AccountController.cs


C# / Controllers/AccountController.cs (Register Action)

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace MobileShop.Controllers
{
    public class AccountController : Controller
    {
    private readonly UserManager<ApplicationUser> _userManager;     private readonly SignInManager<ApplicationUser> _signInManager;     private readonly RoleManager<IdentityRole> _roleManager;     private readonly ApplicationDbContext _context; // ADD THIS     private readonly IShoppingCartService _cartService;     public AccountController(     UserManager<ApplicationUser> userManager,     SignInManager<ApplicationUser> signInManager,     RoleManager<IdentityRole> roleManager,     ApplicationDbContext context, // ADD THIS PARAMETER)     IShoppingCartService cartService, IFileService fileService)         {         _userManager = userManager;         _signInManager = signInManager;         _roleManager = roleManager;         _context = context; // ADD THIS         _cartService = cartService;         } [HttpGet] [AllowAnonymous] public IActionResult Register(string? returnUrl = null) { // 1. Preserving the intended navigation path ViewData["ReturnUrl"] = returnUrl; // 2. Rendering the onboarding interface return View(); } } }

Step 3: Creating the Responsive Register Razor View UI

Create a new view file named Register.cshtml inside your Views/Account/ directory.


Razor / Views/Account/Register.cshtml

@model MobileShop.ViewModels.RegisterViewModel
@{
    ViewData["Title"] = "Register";
}

<div class="container py-5">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card shadow">
                <div class="card-header bg-primary text-white text-center">
                    <h4 class="mb-0"><i class="bi bi-person-plus"></i> Create Account</h4>
                </div>
                <div class="card-body p-4">
                    <form asp-action="Register" method="post">
                        <input type="hidden" name="returnUrl" value="@ViewData["ReturnUrl"]" />

                        <div asp-validation-summary="ModelOnly" class="text-danger"></div>

                        <div class="row">
                            <div class="col-md-6 mb-3">
                                <label asp-for="FirstName" class="form-label"></label>
                                <input asp-for="FirstName" class="form-control" placeholder="First name" />
                                <span asp-validation-for="FirstName" class="text-danger"></span>
                            </div>
                            <div class="col-md-6 mb-3">
                                <label asp-for="LastName" class="form-label"></label>
                                <input asp-for="LastName" class="form-control" placeholder="Last name" />
                                <span asp-validation-for="LastName" class="text-danger"></span>
                            </div>
                        </div export>

                        <div class="mb-3">
                            <label asp-for="Email" class="form-label"></label>
                            <div class="input-group">
                                <span class="input-group-text"><i class="bi bi-envelope"></i></span>
                                <input asp-for="Email" class="form-control" placeholder="Enter your email" />
                            </div>
                            <span asp-validation-for="Email" class="text-danger"></span>
                        </div>

                        <div class="mb-3">
                            <label asp-for="PhoneNumber" class="form-label"></label>
                            <div class="input-group">
                                <span class="input-group-text"><i class="bi bi-telephone"></i></span>
                                <input asp-for="PhoneNumber" class="form-control" placeholder="Enter phone number" />
                            </div>
                            <span asp-validation-for="PhoneNumber" class="text-danger"></span>
                        </div>

                        <div class="mb-3">
                            <label asp-for="Password" class="form-label"></label>
                            <div class="input-group">
                                <span class="input-group-text"><i class="bi bi-lock"></i></span>
                                <input asp-for="Password" class="form-control" placeholder="Create password" />
                            </div>
                            <span asp-validation-for="Password" class="text-danger"></span>
                            <small class="text-muted">Password must be at least 6 characters with uppercase, lowercase, and a number.</small>
                        </div>

                        <div class="mb-3">
                            <label asp-for="ConfirmPassword" class="form-label"></label>
                            <div class="input-group">
                                <span class="input-group-text"><i class="bi bi-lock-fill"></i></span>
                                <input asp-for="ConfirmPassword" class="form-control" placeholder="Confirm password" />
                            </div>
                            <span asp-validation-for="ConfirmPassword" class="text-danger"></span>
                        </div>

                        <button type="submit" class="btn btn-primary w-100 mb-3">
                            <i class="bi bi-person-check"></i> Register
                        </button>
                    </form>

                    <hr />

                    <div class="text-center">
                        <p class="mb-0">Already have an account? <a asp-action="Login" class="text-decoration-none">Login here</a></p>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Key Technical Features Breakdown

  • Strongly Typed Model Binding (@model RegisterViewModel): Connects the form directly to the custom properties we built in Step 1, unlocking automatic compile-time safety and IntelliSense inside the HTML markup.

  • Secure Redirection (type="hidden" name="returnUrl"): Captures the navigation route from our ViewData object and keeps it hidden in the DOM, ready to submit alongside the user's form inputs to maintain a seamless shopping journey after registration.

  • ASP.NET Core Tag Helpers:

    • asp-action="Register" targets the exact destination handler on the controller.

    • asp-for automatically binds inputs, labels, and metadata properties like text types and localized fields.

    • asp-validation-for instantly hooks up validation spans to show targeted errors next to individual inputs.

  • Validation Bulletproofing (_ValidationScriptsPartial): Injecting this script bundle enables jQuery Validation. This runs our model checks immediately on the client's machine—saving server resources and providing real-time UI feedback.

Step 4: Processing Registrations with the HTTP POST Action

Add this action method inside your AccountController.cs to handle form submissions securely.


C# / Controllers/AccountController.cs (Register Post Action)

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model, string? returnUrl = null)
{
    ViewData["ReturnUrl"] = returnUrl;

    if (!ModelState.IsValid)
        return View(model);

    var user = new ApplicationUser
    {
        UserName = model.Email,
        Email = model.Email,
        FirstName = model.FirstName,
        LastName = model.LastName,
        PhoneNumber = model.PhoneNumber,
        EmailConfirmed = true
    };

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

    if (result.Succeeded)
    {
        await _userManager.AddToRoleAsync(user, "Customer");

        await _signInManager.SignInAsync(user, isPersistent: false);
        return RedirectToAction("Index", "Home");
    }

    foreach (var error in result.Errors)
    {
        ModelState.AddModelError(string.Empty, error.Description);
    }

    return View(model);
}

Core Components & Security Layers

  • Security Attributes ([ValidateAntiForgeryToken]): This defends your site against Cross-Site Request Forgery (CSRF) attacks. It verifies that the hidden cryptographic token generated by your form matches the token expected by your server, ensuring malicious third-party scripts cannot forge a registration submission.

  • Validation Gatekeeper (ModelState.IsValid): This checks if the incoming form data complies with all the Data Annotation rules we defined in Step 1 (such as matching passwords and valid email formats). If any check fails, it immediately halts execution and returns the user to the form with their inputs preserved.

  • Data Mapping to ApplicationUser: We instantiate a new Identity user profile, mapping your custom extensions (FirstName, LastName) alongside default properties. Setting UserName = model.Email sets their email as their primary login credential.

  • Asynchronous Creation (_userManager.CreateAsync): The code calls the Identity pipeline to securely check for email duplicates, hash the plaintext password using advanced industry algorithms (like PBKDF2), and save the user records safely without locking up system threads.

  • Role Assignment & Automated Session Login:

    • AddToRoleAsync automatically attaches the new user to a pre-defined security authorization group ("Customer").

    • SignInAsync creates their actual authentication cookie on the client browser. Setting isPersistent: false establishes a temporary session cookie that expires when the user closes their web browser.

  • Error Bubbling Matrix: If the Identity database engine rejects the request (e.g., the password is too weak or the email is already registered), the foreach loop extracts those specific error summaries and appends them directly back into the view's @Html.ValidationSummary area.

Step 5: Building the Login Data Model (LoginViewModel)

Create a new file named LoginViewModel.cs inside your ViewModels directory.


C# / ViewModels/LoginViewModel.cs

public class LoginViewModel
{
    [Required(ErrorMessage = "Email is required")]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { get; set; } = string.Empty;

    [Required(ErrorMessage = "Password is required")]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; } = string.Empty;

    [Display(Name = "Remember me?")]
    public bool RememberMe { get; set; }

    public string? ReturnUrl { get; set; }
}

Key Component Breakdown

  • Credential Validation ([Required], [EmailAddress]): These attributes act as client and server-side gatekeepers. They guarantee that a user cannot submit empty text boxes and that the input strictly matches standard email formatting patterns before your code ever contacts the database.

  • Security Masking ([DataType(DataType.Password)]): This is a critical UI instruction. It tells the Razor engine to render an HTML element, automatically hiding the characters behind dots or asterisks as the user types to prevent shoulder-surfing.

  • Persistent Sessions (RememberMe): This boolean binds directly to a frontend checkbox. If checked, it dictates whether ASP.NET Core Identity generates a long-lived persistent cookie (staying logged in even after closing the browser) or a temporary session-based cookie.

  • Smart Redirection (ReturnUrl): Just like in our registration workflow, this optional property holds the path of the protected resource (like the checkout page) the user was trying to reach before being asked to log in.

Step 6: Crafting the HTTP GET Login Entry Action

Add this action method inside your AccountController.cs to handle incoming requests for your login screen interface.


C# / Controllers/AccountController.cs (Login Get Action)

[HttpGet]
[AllowAnonymous]
public IActionResult Login(string? returnUrl = null)
{
    ViewData["ReturnUrl"] = returnUrl;
    return View();
}

Core Functionality & UX Architecture

  • Explicit HTTP Routing ([HttpGet]): Restricts this endpoint to listen exclusively to standard web navigation page requests. It prevents conflicts with the form submission processor (HTTP POST) that shares the exact same route name.

  • Bypassing Security Filters ([AllowAnonymous]): Ensures that guest shoppers, unauthenticated visitors, and returning clients can instantly view the login page without getting blocked by any global authorization lockouts.

  • Capturing and Passing Intent (ViewData["ReturnUrl"] = returnUrl;): Captures the exact path of the page the user was trying to access before being prompted to log in (for example, your newly created dynamic shopping cart page from Part 8). By storing this string payload inside the ViewData collection, you pass it safely to the frontend view so your login form remembers exactly where to redirect the user upon a successful sign-in.

Step 7: Creating the Responsive Login Razor View UI

Create a new view file named Login.cshtml inside your Views/Account/ directory.


Razor / Views/Account/Login.cshtml

@model MobileShop.ViewModels.LoginViewModel
@{
    ViewData["Title"] = "Login";
}

<div class="container py-5">
    <div class="row justify-content-center">
        <div class="col-md-5">
            <div class="card shadow">
                <div class="card-header bg-primary text-white text-center">
                    <h4 class="mb-0"><i class="bi bi-person-circle"></i> Login</h4>
                </div>
                <div class="card-body p-4">
                    <form asp-action="Login" method="post">
                        <input type="hidden" name="returnUrl" value="@ViewData["ReturnUrl"]" />

                        <div asp-validation-summary="ModelOnly" class="text-danger"></div>

                        <div class="mb-3">
                            <label asp-for="Email" class="form-label"></label>
                            <div class="input-group">
                                <span class="input-group-text"><i class="bi bi-envelope"></i></span>
                                <input asp-for="Email" class="form-control" placeholder="Enter your email" />
                            </div>
                            <span asp-validation-for="Email" class="text-danger"></span>
                        </div>

                        <div class="mb-3">
                            <label asp-for="Password" class="form-label"></label>
                            <div class="input-group">
                                <span class="input-group-text"><i class="bi bi-lock"></i></span>
                                <input asp-for="Password" class="form-control" placeholder="Enter your password" />
                            </div>
                            <span asp-validation-for="Password" class="text-danger"></span>
                        </div>

                        <div class="mb-3 form-check">
                            <input asp-for="RememberMe" class="form-check-input" />
                            <label asp-for="RememberMe" class="form-check-label"></label>
                        </div>

                        <button type="submit" class="btn btn-primary w-100 mb-3">
                            <i class="bi bi-box-arrow-in-right"></i> Login
                        </button>

                        <div class="text-center">
                            <a asp-action="ForgotPassword" class="text-decoration-none">Forgot password?</a>
                        </div>
                    </form>

                    <hr />

                    <div class="text-center mb-3">
                        <p class="text-muted">Or login with</p>
                        <div class="d-flex justify-content-center gap-2">
                            <input type="hidden" name="provider" value="Google" />
                            <button type="submit" class="btn btn-outline-danger">
                                <i class="bi bi-google"></i> Google
                            </button>
                            <input type="hidden" name="provider" value="Facebook" />
                            <button type="submit" class="btn btn-outline-primary">
                                <i class="bi bi-facebook"></i> Facebook
                            </button>
                        </div>
                    </div>

                    <hr />

                    <div class="text-center">
                        <p class="mb-0">Don't have an account? <a asp-action="Register" class="text-decoration-none">Register here</a></p>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Step 8: Handling Login Submissions & Shopping Cart Migration (HTTP POST)

Add this code to your AccountController.cs to process user credentials securely and manage their session transitions.


C# / Controllers/AccountController.cs (Login Post Action)

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string? returnUrl = null)
{
    ViewData["ReturnUrl"] = returnUrl;

    if (!ModelState.IsValid)
        return View(model);

    var result = await _signInManager.PasswordSignInAsync(
        model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);

    if (result.Succeeded)
    {

        // Migrate cart
        var user = await _userManager.FindByEmailAsync(model.Email);
        if (user != null)
        {
            await _cartService.MigrateCartAsync(user.Id);
        }

        if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
            return Redirect(returnUrl);

        return RedirectToAction("Index", "Home");
    }

    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
    return View(model);
}

Core Components & Business Logic

  • Secure Authentication Engine (_signInManager.PasswordSignInAsync): This is the core method provided by ASP.NET Core Identity. It securely lookups the user by their email, pulls their hashed password from the database, runs the input password through the same hashing algorithm, and compares them.

  • The Session Lifespan Parameter (model.RememberMe): By passing this boolean into the sign-in engine, Identity determines the cookie's lifespan. If checked, it issues a persistent cookie that survives browser restarts; if unchecked, it issues a temporary session cookie.

  • Smart Shopping Cart Migration (_cartService.MigrateCartAsync):

    E-commerce Pro Tip: When a guest user adds a smartphone to their cart, that data is tracked anonymously (usually tied to a temporary Session ID or a browser cookie). Once they successfully log in, this block fetches their unique user.Id and passes it to your custom cart service, instantly merging or reassigning those anonymous items directly to their database account.

  • Open Redirection Protection (Url.IsLocalUrl): Before executing a redirection to a non-empty returnUrl, this safety check runs. It guarantees that the target destination is local to your domain, preventing Open Redirect Vulnerabilities where a malicious actor alters the query parameter to hijack your user and send them to a phishing site.

Step 9: Deep-Dive Into the Cart Migration Engine

Add this logic to your cart service layer. It acts as the bridge that transfers a shopper's guest items over to their permanent account the millisecond they log in.


C# / Services/CartService.cs (MigrateCartAsync Method)

public async Task MigrateCartAsync(string userId)
{
    var cartId = GetCartId();
    var cartItems = await _context.ShoppingCartItems
        .Where(c => c.CartId == cartId)
        .ToListAsync();

    foreach (var item in cartItems)
    {
        item.CartId = userId;
    }

    await _context.SaveChangesAsync();
    _httpContextAccessor.HttpContext?.Session.Remove("CartId");
}

Line-by-Line Technical Breakdown

  • var cartId = GetCartId(); This calls your internal utility helper to fetch the temporary tracking string (the guest ID) currently assigned to the user's browser session.

  • _context.ShoppingCartItems.Where(c => c.CartId == cartId).ToListAsync(); The app hits your SQL database to pull every single product row that matches that specific guest session ID. Using ToListAsync() executes this search asynchronously, keeping your application highly responsive under heavy traffic.

  • The In-Place Ownership Loop:

    foreach (var item in cartItems)
    {
        item.CartId = userId;
    }

Step 10: Implementing the Secure Logout Action

Add this code inside your AccountController.cs to handle user sign-outs.


C# / Controllers/AccountController.cs (Logout Action)

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
    await _signInManager.SignOutAsync();
    return RedirectToAction("Index", "Home");
}

Core Components & Security Breakdown

  • The HTTP POST Security Rule ([HttpPost]):

    Critical Security Concept: Never use a standard hyperlink ( tag) or an HTTP GET request for logouts. If logout is accessible via GET, a malicious site could hide an image tag like on their page. The moment your logged-in user visits that bad site, their browser would automatically fire a request to that URL and log them out without their consent. Forcing an HTTP POST completely blocks this vector.

  • Anti-Forgery Protection ([ValidateAntiForgeryToken]): Because the logout is processed as a form post, this attribute requires the incoming request to carry a cryptographically secure token generated by your server. This completely protects your users from Cross-Site Request Forgery (CSRF) attacks.

  • Wiping the Auth Cookie (_signInManager.SignOutAsync()): This core ASP.NET Core Identity method clears the user's authentication claims and commands the client browser to immediately destroy the encrypted security cookie.

  • Safe Re-routing (RedirectToAction): Once the user's session is completely wiped, the system instantly redirects them back to the public homepage as an anonymous visitor.

Step 11: Implementing the Profile Management Data Model

Append the ProfileViewModel class into your existing ViewModels/AccountViewModel.cs file.


C# / ViewModels/ProfileViewModel.cs

public class ProfileViewModel
{
    [Required]
    [Display(Name = "First Name")]
    [StringLength(50)]
    public string FirstName { get; set; } = string.Empty;

    [Required]
    [Display(Name = "Last Name")]
    [StringLength(50)]
    public string LastName { get; set; } = string.Empty;

    [Required]
    [EmailAddress]
    public string Email { get; set; } = string.Empty;

    [Phone]
    [Display(Name = "Phone Number")]
    public string? PhoneNumber { get; set; }

    [Display(Name = "Date of Birth")]
    [DataType(DataType.Date)]
    public DateTime? DateOfBirth { get; set; }

    [StringLength(200)]
    public string? Address { get; set; }

    [StringLength(100)]
    public string? City { get; set; }

    [StringLength(20)]
    [Display(Name = "Postal Code")]
    public string? PostalCode { get; set; }

    [StringLength(50)]
    public string? Country { get; set; }

    [Display(Name = "Profile Image")]
    public IFormFile? ProfileImage { get; set; }

    public string? ProfileImageUrl { get; set; }
}

Step 12: Creating the User Profile Dashboard Retrieval Engine (HTTP GET)

Add the following action method inside your AccountController.cs to query, assemble, and render the authenticated user's personal details.


C# / Controllers/AccountController.cs (Profile Get Action)

[HttpGet]
[Authorize]
public async Task<IActionResult> Profile()
{
    var user = await _userManager.GetUserAsync(User);
    if (user == null)
        return NotFound();

    var model = new ProfileViewModel
    {
        FirstName = user.FirstName,
        LastName = user.LastName,
        Email = user.Email!,
        PhoneNumber = user.PhoneNumber,
        DateOfBirth = user.DateOfBirth,
        Address = user.Address,
        City = user.City,
        PostalCode = user.PostalCode,
        Country = user.Country,
        ProfileImageUrl = user.ProfileImageUrl
    };

    ViewBag.CartItemCount = await _cartService.GetCartItemCountAsync();
    return View(model);
}

Key Technical Features & Architecture Breakdown

  • Enforcing Strict Account Locks ([Authorize]): Unlike your login or registration endpoints, the profile workspace is shielded by the [Authorize] filter. This commands the security system to instantly block guest visitors. If an unauthenticated shopper attempts to view this URL, the framework intercepts the request and automatically routes them straight to the Login screen.

  • Contextual Identity Extraction (_userManager.GetUserAsync(User)): Your code passes the global security principal User (the encrypted identity cookie payload present in the HTTP request) into the manager. The system decrypts it automatically on the fly to find the user's entry inside the database without requiring you to manually pass unsecure query strings or IDs in the URL path.

  • Defensive Guard Boundary (NotFound()): If for any odd reason a user has a cookie session active but their backend record has been removed or modified from the database, the system catches the discrepancy instantly via the null check and returns a standard HTTP 404 response to protect application resources.

Step 13: Implementing the Reusable File Management Service

Create an interface and implementation file named IFileService.cs inside your Services folder. This service uses built-in .NET infrastructure to handle single uploads, batch files, and secure physical deletions.


C# / Services/FileService.cs (IFileService & FileService)

namespace MobileShop.Services
{
    public interface IFileService
    {
        Task<string> SaveFileAsync(IFormFile file, string folder);
        Task<List<string>> SaveFilesAsync(List<IFormFile> files, string folder);
        void DeleteFile(string filePath);
    }

    public class FileService : IFileService
    {
        private readonly IWebHostEnvironment _environment;
        private readonly ILogger<FileService> _logger;

        public FileService(IWebHostEnvironment environment, ILogger<FileService> logger)
        {
            _environment = environment;
            _logger = logger;
        }

        public async Task<string> SaveFileAsync(IFormFile file, string folder)
        {
            if (file == null || file.Length == 0)
                return string.Empty;

            var uploadsFolder = Path.Combine(_environment.WebRootPath, folder);
            if (!Directory.Exists(uploadsFolder))
                Directory.CreateDirectory(uploadsFolder);

            var fileName = $"{Guid.NewGuid()}_{Path.GetFileName(file.FileName)}";
            var filePath = Path.Combine(uploadsFolder, fileName);

            using (var stream = new FileStream(filePath, FileMode.Create))
            {
                await file.CopyToAsync(stream);
            }

            var relativePath = $"/{folder}/{fileName}";
            _logger.LogInformation($"File saved: {relativePath}");
            return relativePath;
        }

        public async Task<List<string>> SaveFilesAsync(List<IFormFile> files, string folder)
        {
            var paths = new List<string>();
            foreach (var file in files)
            {
                var path = await SaveFileAsync(file, folder);
                if (!string.IsNullOrEmpty(path))
                    paths.Add(path);
            }
            return paths;
        }

        public void DeleteFile(string filePath)
        {
            if (string.IsNullOrEmpty(filePath)) return;

            var fullPath = Path.Combine(_environment.WebRootPath, filePath.TrimStart('/'));
            if (File.Exists(fullPath))
            {
                File.Delete(fullPath);
                _logger.LogInformation($"File deleted: {fullPath}");
            }
        }
    }
}

Detailed Component & Architecture Breakdown

  • Interface Separation (IFileService): By defining an interface contract, you are teaching your viewers the Dependency Inversion Principle (the 'D' in SOLID design). This makes your application highly testable and loosely coupled. If you later decide to switch from saving images locally to hosting them in the cloud (like Azure Blob Storage or AWS S3), you can write a new class without rewriting a single line of code in your controllers!

  • Locating the Web Root (IWebHostEnvironment): This built-in service provides access to the web root path property, which dynamically resolves the absolute path of your app's wwwroot folder on the hosting server. It ensures your pathing logic works perfectly on your local machine, a Windows Server, or a Linux Docker container.

  • Collision Prevention with Cryptographic Keys (Guid.NewGuid()): If two separate users upload an avatar named profile.jpg, the second upload would instantly overwrite the first one. By prefixing the file name with a unique Globally Unique Identifier (GUID), your code ensures that every file written to disk has a globally unique string signature, safely eliminating data loss or asset overlapping bugs.

  • Memory-Safe Async Streaming (using and CopyToAsync): The using statement ensures that the operating system's file stream handler is instantly released and disposed of as soon as the file finishes writing, preventing file lockouts. Running the copy operation asynchronously avoids blocking web request worker threads under heavy download pressure.

  • Returning the Virtual Relative Path: The method transforms the ugly internal operating system file string into a standardized web URL structure (such as /uploads/filename.jpg). This is exactly what you want to save to your SQL database table so your Razor views can directly map it into normal HTML image source attributes.

  • Server Hygiene Automation (DeleteFile): When a customer updates their avatar or changes a mobile device thumbnail, leaving the old image behind turns your server into a graveyard of dead assets. Your deletion logic parses the stored relative web path, cleans any leading slashes, locates the exact server file location, and permanently purges it from disk to save storage capacity.

Step 14: Processing User Profile Updates & Avatar Management (HTTP POST)

Add this action method inside your AccountController.cs to handle incoming profile form submissions securely.


C# / Controllers/AccountController.cs (Profile Post Action)

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Profile(ProfileViewModel model)
{
    if (!ModelState.IsValid)
    {
        ViewBag.CartItemCount = await _cartService.GetCartItemCountAsync();
        return View(model);
    }

    var user = await _userManager.GetUserAsync(User);
    if (user == null)
        return NotFound();

    user.FirstName = model.FirstName;
    user.LastName = model.LastName;
    user.PhoneNumber = model.PhoneNumber;
    user.DateOfBirth = model.DateOfBirth;
    user.Address = model.Address;
    user.City = model.City;
    user.PostalCode = model.PostalCode;
    user.Country = model.Country;

    if (model.ProfileImage != null)
    {
        var imagePath = await _fileService.SaveFileAsync(model.ProfileImage, "images/profiles");
        if (!string.IsNullOrEmpty(imagePath))
        {
            if (!string.IsNullOrEmpty(user.ProfileImageUrl))
                _fileService.DeleteFile(user.ProfileImageUrl);
            user.ProfileImageUrl = imagePath;
        }
    }

    var result = await _userManager.UpdateAsync(user);
    if (result.Succeeded)
    {
        TempData["Success"] = "Profile updated successfully.";
        return RedirectToAction(nameof(Profile));
    }

    foreach (var error in result.Errors)
    {
        ModelState.AddModelError(string.Empty, error.Description);
    }

    ViewBag.CartItemCount = await _cartService.GetCartItemCountAsync();
    return View(model);
}

Core Components & Business Logic Breakdown

  • Security Stack Guarding ([Authorize] & [ValidateAntiForgeryToken]): The Authorize filter guarantees that only a verified logged-in user can submit updates to this method, completely locking out unauthorized requests. The token validation attribute acts as a firewall against Cross-Site Request Forgery (CSRF) hijacks, validating that the post data originated genuinely from your own form.

  • Form Validation Resilience (ModelState.IsValid): If a user types an invalid phone format or exceeds a string length, the check evaluates to false. Before sending the user back to the view with their errors, the code calls the cart service item count manager asynchronously. This is a vital architectural step because it prevents your layout shell's shopping cart bubble from breaking or disappearing on a validation post-back.

  • Data Synchronization Flow: Once the core user record is retrieved from database storage via the user manager, the fields from your submitted profile view model are cleanly mapped over to the actual tracking entity (names, shipping data, and date of birth), modifying the tracked database entity values in local memory.

  • Smart Media Lifecycle Automation: This block is the highlight of the tutorial! If the user uploaded a new image, your custom file service saves it physically inside your web root profile folder and returns its web-ready relative path string. Before updating the database string field with this new path, it evaluates whether an old profile image URL exists. If an old avatar path exists, it triggers the file deletion logic to instantly wipe the old, dead image file from your hard drive, keeping your web server beautifully clean.

  • Asynchronous Persistence (_userManager.UpdateAsync(user)): The modified entity object is sent down the identity pipeline to commit the text values and new file path string directly to the SQL database safely and asynchronously.

  • PRG Pattern Compliance: Upon success, the code uses the Post-Redirect-Get (PRG) software design pattern. Storing a message in TempData ensures it survives exactly one cross-request trip. By redirecting the browser back to the GET profile method instead of simply returning the View, you prevent the annoying "Confirm Form Resubmission" alert from triggering if the user refreshes their browser page!

Step 15: Initializing the Forgot Password Pathway (HTTP GET)

Add this lightweight gateway action inside your AccountController.cs file.


C# / Controllers/AccountController.cs (ForgotPassword Get Action)

[HttpGet]
[AllowAnonymous]
public IActionResult ForgotPassword()
{
    return View();
}

Core Functionality & Routing Logic

  • Handling Initial Navigation Requests ([HttpGet]): This attribute restricts the method to look out exclusively for standard web layout calls (like a customer clicking the "Forgot password?" hyperlink we created in our step 7 login card view). It provides a dedicated setup route separate from the upcoming form validation engine.

  • Universal Public Accessibility ([AllowAnonymous]): Because this feature is explicitly designed for visitors who cannot log into their profiles, you must flag it to bypass your controller's security layer. This guarantees that locked-out guests or returning customers can load the interface safely without being blocked by global app authentication locks.

  • Instantiating a Clean Interface Container (return View();): Since this initial page load only requires a blank input text box for the user to type their lost email account address, the action doesn't need to load or pass a loaded database entity into the Razor view context. It simply commands the framework rendering engine to output the empty entry form shell to the user's browser.

Step 16: Creating the Responsive Forgot Password UI View

Create a new view file named ForgotPassword.cshtml inside your Views/Account/ directory.


Razor / Views/Account/ForgotPassword.cshtml

@model MobileShop.ViewModels.ForgotPasswordViewModel
@{
    ViewData["Title"] = "Forgot Password";
}

<div class="container py-5">
    <div class="row justify-content-center">
        <div class="col-md-5">
            <div class="card shadow">
                <div class="card-header bg-primary text-white text-center">
                    <h4 class="mb-0"><i class="bi bi-envelope"></i> Forgot Password</h4>
                </div>
                <div class="card-body p-4">
                    <p class="text-muted text-center">Enter your email address and we'll send you a password reset link.</p>

                    <form asp-action="ForgotPassword" method="post">
                        <div asp-validation-summary="ModelOnly" class="text-danger"></div>

                        <div class="mb-3">
                            <label asp-for="Email" class="form-label"></label>
                            <div class="input-group">
                                <span class="input-group-text"><i class="bi bi-envelope"></i></span>
                                <input asp-for="Email" class="form-control" placeholder="Enter your email" />
                            </div>
                            <span asp-validation-for="Email" class="text-danger"></span>
                        </div>

                        <button type="submit" class="btn btn-primary w-100 mb-3">
                            <i class="bi bi-send"></i> Send Reset Link
                        </button>
                    </form>

                    <hr />

                    <div class="text-center">
                        <p class="mb-0">Remember your password? <a asp-action="Login" class="text-decoration-none">Login here</a></p>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Key Technical and Design Highlights Breakdown

  • Strict View-Model Binding: The view declares a contract with ForgotPasswordViewModel. This single-purpose model ensures that the view only knows about what is strictly required for this page: the user's email address.

  • Visual Interface Consistency (card shadow): By matching the layout width constraints (col-md-5), padding structures, and shadow depths used in your login view, the transitions between your authentication pages feel completely seamless to your Mobile Shop users.

  • Contextual Visual Cues (input-group & bi-send): The code embeds Bootstrap Icons directly into the layout. Wrapping the input box inside a Bootstrap input-group prefixes the field with a clean envelope icon (bi-envelope). The submission button displays a flying paper-plane icon (bi-send), explicitly illustrating the action of sending data out over the web.

  • Targeted Validation Elements (asp-validation-for): The inline data spans track error flags precisely below the target field. If a user types text that violates standard email naming constraints or submits an empty input, the system intercepts the action locally and marks the field with a clear red layout notice (text-danger).

  • Zero-Latency Client Validation Injection: The script block at the very bottom imports your partial scripts loader. This pulls the jQuery Validation core libraries onto the page, allowing the browser to parse your model annotations and display syntax errors instantly without performing an expensive, full-page server post-back.

Step 17: Appending the Forgot Password View Model

Add this class definition to your existing ViewModels/AccountViewModel.cs file.


C# / ViewModels/ForgotPasswordViewModel.cs

public class ForgotPasswordViewModel
{
    [Required]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { get; set; } = string.Empty;
}

Technical Data Validation Breakdown

  • Enforcing Presence Security ([Required]): This data annotation commands the framework engine to block empty form submissions. If a user clicks the submit button without entering anything, the validation tracker sets an error flag instantly. This completely protects your controller from receiving null strings, eliminating potential application crash bugs.

  • Pattern Matching Verification ([EmailAddress]): Instead of writing complex regular expressions (regex patterns) by hand to make sure an input looks like a real email address, this native metadata tag automatically validates that the input string conforms to standard formatting criteria (like containing an @ symbol and a valid domain suffix). It acts as a double-layered defense on both the user's browser screen and your application's backend server.

  • Explicit Label Overrides ([Display(Name = "Email")]): This attribute tells your frontend Razor view exactly how to generate the corresponding tag text. While the property name matches perfectly here, using this attribute gives you the precise control to update the display language or text format in the future across all your views from one centralized model location.

  • Defensive String Initialization (= string.Empty;): By setting the property to an empty string instead of leaving it uninitialized, you satisfy the compiler's null-safety requirements. This clean coding standard guarantees that your application won't trigger unexpected null reference exceptions while handling your user's form submission.

Step 18: Processing Password Recovery Requests Securely (HTTP POST)

Add this action method to your AccountController.cs file. It evaluates validation rules, performs user lookups, and safely isolates data leaks using secure messaging design patterns.


C# / Controllers/AccountController.cs (ForgotPassword Post Action)

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
    if (!ModelState.IsValid)
        return View(model);

    var user = await _userManager.FindByEmailAsync(model.Email);
    if (user == null)
    {
        TempData["Success"] = "If your email is registered, you will receive password reset instructions.";
        return RedirectToAction(nameof(Login));
    }

    var code = await _userManager.GeneratePasswordResetTokenAsync(user);
    var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code }, protocol: Request.Scheme);

    // TODO: Send email with reset link

    TempData["Success"] = "If your email is registered, you will receive password reset instructions.";
    return RedirectToAction(nameof(Login));
}

Core Components & Security Architecture Breakdown

  • Mitigating Account Enumeration (The Duplicate Messaging Strategy): This is the absolute highlight of your method. Notice that you return the exact same message whether the email exists in your database or not. If your code explicitly stated "Email not found" for a non-registered account, a malicious actor could build an automated script to systematically guess email addresses on your site to discover exactly who has an account on your Mobile Shop. Spitting out a generic, uniform success message completely stops this data disclosure vector.

  • Cryptographic Reset Key Generation (GeneratePasswordResetTokenAsync): When a valid user is found, the user manager generates a complex, secure, short-lived cryptographic hash string linked explicitly to that specific user record. This token acts as a high-security temporary master key that automatically expires after use or once its validity window (configured inside your Identity options) closes.

  • Absolute URI Construction via Url.Action: Unlike internal app page links that use relative path structures (such as /Account/ResetPassword), email hyperlinks must be absolute. By passing the request scheme variable as a parameter, .NET reads the running web server state to explicitly append the absolute domain scheme prefix (such as https://enterwebsitenamehere.com) so the link functions perfectly from inside a customer's email inbox app.

  • State Redirection and Messaging Resilience: Once the link tracking is built, the action records the confirmation status message into TempData and routes the visitor straight to the login interface. This keeps the user experience clean and lets the user know what step to take next without stalling their browsing flow.

Step 19: Implementing the Change Password Data View Model

Append this class architecture definition directly into your ViewModels/AccountViewModel.cs file

C# / ViewModels/ChangePasswordViewModel.cs
    public class ChangePasswordViewModel
    {
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Current password")]
        public string OldPassword { get; set; } = string.Empty;

        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "New password")]
        public string NewPassword { get; set; } = string.Empty;

        [DataType(DataType.Password)]
        [Display(Name = "Confirm new password")]
        [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; } = string.Empty;
    }

Technical Attribute & Security Validation Breakdown

  • Dynamic Rule Injections via Property Tokens: The StringLength attribute handles validation alerts dynamically. The framework injects your metadata attributes straight into the text placeholders at runtime. The {0} token reads the value from your Display tag ("Password"), and the {2} token grabs your designated minimum threshold parameter (6 characters). This saves you from typo mistakes and makes localization translation a breeze.

  • The Cross-Field Verification Lock ([Compare]): This attribute acts as a front-facing sentinel that matches the values typed inside the input tags. If the string values don't line up perfectly character-for-character, the validation engine flags a compilation match failure before any database transactions occur, preventing typos from locking a user out.

  • Hiding Input Plaintext ([DataType(DataType.Password)]): This annotation instructs the Razor engine to explicitly inject the HTML attribute type="password" when building the form elements. This keeps user keystrokes protected by displaying black circles or stars instead of raw plaintext text on the screen.

  • The Hidden Payload Carrier (Code Property): The Code property doesn't require visual annotations because it is never typed by the user. It is built to silently hold the raw cryptographic reset token string passed from the email link. By capturing it inside the model state contract, you can route it invisibly through the form post lifecycle to authorize the password modification.

Step 20: Initializing the Profile Password Management Gateway (HTTP GET)

Add this action method inside your AccountController.cs file to allow logged-in users to navigate to their security management panel.


C# / Controllers/AccountController.cs (ChangePassword Get Action)

[HttpGet]
[Authorize]
public IActionResult ChangePassword()
{
    return View();
}

Core Functionality & Security Boundary Breakdown

  • Enforcing Dashboard Session Locks ([Authorize]): Unlike anonymous recovery workflows, this method is locked down tightly with the Authorize filter attribute. This blocks public guest traffic entirely. If a random visitor attempts to force their way into this URL path, the core authentication handler intercepts them and redirects them straight to your standard login screen.

  • Segregating Initial Page Queries ([HttpGet]): This attribute confines the method to pick up initial browser hits—like a customer clicking a "Change Password" settings button inside their profile workspace. It keeps your layout retrieval logic cleanly isolated from data persistence mutations.

  • Delivering a Fresh State Blueprint (return View();): Since this page only needs to present an empty input box form for the user to type their existing credentials and establish their new choice, you don't need to load or bind any active records out of your database tables yet. The action instantly hands off a blank layout matrix directly to the client browser.

Step 21: Creating the Responsive Change Password View

Create a new file named ChangePassword.cshtml inside your Views/Account/ directory.


Razor / Views/Account/ChangePassword.cshtml

@model MobileShop.ViewModels.ChangePasswordViewModel
@{
    ViewData["Title"] = "Change Password";
}

<div class="container py-5">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card shadow">
                <div class="card-header bg-primary text-white text-center">
                    <h4 class="mb-0"><i class="bi bi-key"></i> Change Password</h4>
                </div>
                <div class="card-body p-4">
                    <form asp-action="ChangePassword" method="post">
                        <div asp-validation-summary="ModelOnly" class="text-danger"></div>

                        <div class="mb-3">
                            <label asp-for="OldPassword" class="form-label"></label>
                            <div class="input-group">
                                <span class="input-group-text"><i class="bi bi-lock"></i></span>
                                <input asp-for="OldPassword" class="form-control" placeholder="Enter current password" />
                            </div>
                            <span asp-validation-for="OldPassword" class="text-danger"></span>
                        </div>

                        <div class="mb-3">
                            <label asp-for="NewPassword" class="form-label"></label>
                            <div class="input-group">
                                <span class="input-group-text"><i class="bi bi-lock-fill"></i></span>
                                <input asp-for="NewPassword" class="form-control" placeholder="Enter new password" />
                            </div>
                            <span asp-validation-for="NewPassword" class="text-danger"></span>
                            <small class="text-muted">Password must be at least 6 characters with uppercase, lowercase, and a number.</small>
                        </div>

                        <div class="mb-3">
                            <label asp-for="ConfirmPassword" class="form-label"></label>
                            <div class="input-group">
                                <span class="input-group-text"><i class="bi bi-lock-fill"></i></span>
                                <input asp-for="ConfirmPassword" class="form-control" placeholder="Confirm new password" />
                            </div>
                            <span asp-validation-for="ConfirmPassword" class="text-danger"></span>
                        </div>

                        <button type="submit" class="btn btn-primary w-100">
                            <i class="bi bi-check-circle"></i> Change Password
                        </button>
                    </form>

                    <hr />

                    <div class="text-center">
                        <a asp-action="Profile" class="text-decoration-none"><i class="bi bi-arrow-left"></i> Back to Profile</a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Detailed Component & Interface Breakdown

  • Dedicated Data Contract Binding (ChangePasswordViewModel): The view defines a clear contract with your view model layer. This keeps the layout independent from your core database architecture, ensuring that only the specific fields required to transition old credentials into new entries exist in the browser context.

  • Clever Visual Distinctions via Bootstrap Icons: The form uses deliberate design indicators to guide the user's eye. The current credentials input group utilizes an open padlock icon (bi-lock). The new password fields switch to a filled padlock variant (bi-lock-fill), subtly emphasizing that a new layer of protection is being established.

  • Preventive Requirement Micro-copy: The small, muted helper text below the input acts as a critical UI guide. By spelling out your password policy requirements clearly directly below the input field (at least 6 characters, uppercase, lowercase, and a number), you drastically lower form validation failures, helping your shoppers avoid frustrating trial-and-error mistakes.

  • The Escape Route Backlink Pattern: Just like our previous account pages, you are avoiding an isolated workflow trap. Adding a distinct navigational link back to the profile with a clean back-arrow icon ensures the customer can effortlessly exit the password panel and return to their main profile settings dashboard whenever they choose.

  • Zero-Latency Interface Interception (_ValidationScriptsPartial): By packing the client-side script partial at the base of the file, jQuery hooks directly into your model definitions. If a user tries to change their password but leaves the validation rules broken, the page intercepts the click instantly, displaying standard red helper text without forcing an expensive full-page refresh.

Step 22: Processing Account Password Updates (HTTP POST)

Add this action method inside your AccountController.cs file to securely validate, transition, and re-authenticate password changes.

C# / Controllers/AccountController.cs (ChangePassword Post Action)

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ChangePassword(ChangePasswordViewModel model)
{
    if (!ModelState.IsValid)
        return View(model);

    var user = await _userManager.GetUserAsync(User);
    if (user == null)
        return NotFound();

    var result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword);
    if (result.Succeeded)
    {
        await _signInManager.RefreshSignInAsync(user);
        TempData["Success"] = "Password changed successfully.";
        return RedirectToAction(nameof(Profile));
    }

    foreach (var error in result.Errors)
    {
        ModelState.AddModelError(string.Empty, error.Description);
    }

    return View(model);
}

Core Components & Security Architecture Breakdown

  • Multi-Layered Security Guarding: The combination of the Authorize filter and the anti-forgery token verification attribute acts as a double-fortified firewall. It ensures that the submission is coming from an authenticated user session and prevents unauthorized cross-site data injection exploits.

  • Encapsulated Cryptographic Verification (ChangePasswordAsync): Instead of manually retrieving hash algorithms, salting strings, or running comparison loops, your code leverages ASP.NET Core Identity. The user manager handles the heavy lifting by comparing the incoming old password string against the existing cryptographic database hash, checking validation policies, and generating a brand new secure salt-and-hash pattern if valid.

  • Maintaining Session Continuity (RefreshSignInAsync): This line is the most important technical detail in the entire method. When an identity password changes, the security stamp in the database updates automatically, which instantly invalidates all existing login cookies across all browsers. By calling _signInManager.RefreshSignInAsync(user), the application immediately replaces the old browser cookie with a brand new one on the fly, keeping your customer securely logged into their dashboard session without interruption.

  • Post-Redirect-Get (PRG) Workflow Strategy: Once success is achieved, the message is stored inside TempData, and the engine triggers a hard redirect back to the main Profile landing view. This architectural loop prevents database duplication risks and stops annoying form re-submission browser dialog warnings if the customer hits refresh.

  • Identity Error Harvesting Loops: If the update fails (for instance, if the current credentials are wrong or the new input fails your application's special character requirements), the foreach loop catches the complete array of pipeline descriptions and binds them straight into the shared validation summary block at the top of your Razor view template.

Step 23: Establishing the Reset Password View Model

Append this class definition into your existing ViewModels/AccountViewModel.cs folder structure to govern the password reset data contract.

C# / ViewModels/ResetPasswordViewModel.cs

public class ResetPasswordViewModel
{
    [Required]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { get; set; } = string.Empty;

    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; } = string.Empty;

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; } = string.Empty;

    public string Code { get; set; } = string.Empty;
}

Technical Attribute & Validation Mechanics Breakdown

  • String Length Optimization and Dynamic Placeholders: The StringLength metadata tag provides a fantastic way to handle input bounds. Instead of hardcoding text strings, the framework dynamically injects properties at runtime: the {0} token extracts your Display attribute text ("Password"), while the {2} token automatically injects your minimum threshold requirement (6 characters). This keeps your code error-free and clean.

  • The Cross-Field Verification Lock ([Compare]): This attribute acts as an automated validation sentinel. It compares the string data typed inside the confirmation field against the primary password field character-for-character. If there is a single typo mismatch, it stops the form from posting to your database, preventing users from accidentally locking themselves out with a mistyped password.

  • Enforcing Plaintext Masking ([DataType(DataType.Password)]): This annotation instructs your frontend Razor rendering engine to explicitly replace normal input fields with secure password fields. This forces the browser to display dots or asterisks instead of raw characters, protecting the customer from over-the-shoulder privacy leaks.

  • The Invisible Security Payload (Code Property): Notice that the Code property doesn't have any visual display labels or required tags. That is intentional! The customer will never type this manually. When they click the link in their email inbox, your system extracts the long cryptographic token from the URL path parameter and places it into a hidden form input field inside this property. This enables your backend controller to verify that the password reset attempt is completely genuine.

Step 24: Creating the Reset Password Landing Gateway (HTTP GET)

Add this action method inside your AccountController.cs file to intercept incoming email recovery links safely.

C# / Controllers/AccountController.cs (ResetPassword Get Action)

[HttpGet]
[AllowAnonymous]
public IActionResult ResetPassword(string? code = null)
{
    if (code == null)
        return BadRequest("A code must be supplied for password reset.");

    return View();
}

Core Components & Defensive Routing Breakdown

  • Public Access Configuration ([AllowAnonymous]): Because a user resetting their password is completely locked out of their account, this endpoint must bypass your controller's global authentication constraints. Tagging it with this attribute ensures that public guest traffic coming straight from an external mail client can load the initialization screen safely.

  • The Optional String Parameter Guard (string? code = null): By using a nullable string with a default value of null, your method handles broken or manual URL entry gracefully. If a user simply types the reset link path into their browser address bar without the necessary query strings, the application intercepts the request immediately instead of throwing an unhandled exception or rendering a broken form.

  • Defensive Token Validation (BadRequest): The conditional statement checking if the code is null acts as a programmatic firewall. If the required cryptographic key token is missing from the request URL, the application stops execution immediately and surfaces an HTTP 400 Bad Request error status page. This prevents malicious actors or curious users from loading your password editing interface without authorization.

  • Transitioning to the Layout State Blueprint (return View();): Once the URL argument successfully satisfies your presence validation checks, the controller routes the browser directly to your corresponding Razor template layout. This sets up the interface to bind the incoming token silently when the user types their new security credentials.

Step 25: Creating the Reset Password View Interface

Create a new file named ResetPassword.cshtml inside your Views/Account/ directory.

Razor / Views/Account/ResetPassword.cshtml

@model MobileShop.ViewModels.ResetPasswordViewModel
@{
    ViewData["Title"] = "Reset Password";
}

<div class="container py-5">
    <div class="row justify-content-center">
        <div class="col-md-5">
            <div class="card shadow">
                <div class="card-header bg-primary text-white text-center">
                    <h4 class="mb-0"><i class="bi bi-key"></i> Reset Password</h4>
                </div>
                <div class="card-body p-4">
                    <form asp-action="ResetPassword" method="post">
                        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
                        <input asp-for="Code" type="hidden" />

                        <div class="mb-3">
                            <label asp-for="Email" class="form-label"></label>
                            <div class="input-group">
                                <span class="input-group-text"><i class="bi bi-envelope"></i></span>
                                <input asp-for="Email" class="form-control" placeholder="Enter your email" />
                            </div>
                            <span asp-validation-for="Email" class="text-danger"></span>
                        </div>

                        <div class="mb-3">
                            <label asp-for="Password" class="form-label"></label>
                            <div class="input-group">
                                <span class="input-group-text"><i class="bi bi-lock"></i></span>
                                <input asp-for="Password" class="form-control" placeholder="Enter new password" />
                            </div>
                            <span asp-validation-for="Password" class="text-danger"></span>
                        </div>

                        <div class="mb-3">
                            <label asp-for="ConfirmPassword" class="form-label"></label>
                            <div class="input-group">
                                <span class="input-group-text"><i class="bi bi-lock-fill"></i></span>
                                <input asp-for="ConfirmPassword" class="form-control" placeholder="Confirm new password" />
                            </div>
                            <span asp-validation-for="ConfirmPassword" class="text-danger"></span>
                        </div>

                        <button type="submit" class="btn btn-primary w-100">
                            <i class="bi bi-check-circle"></i> Reset Password
                        </button>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Detailed Component & Interface Breakdown

  • Silent Security Payload Binding: The inclusion of the hidden input element for the Code property is the most vital architectural line in this layout. This tag ensures that the long cryptographic verification token parsed by your HTTP GET method stays anchored to the page lifecycle. When the user clicks the final submit button, the code is posted back alongside the new inputs completely invisibly to prevent tampering.

  • A Familiar Design Aesthetic (col-md-5 and card shadow): By matching the strict physical size and container shadow depth of the other authentication panels, you give your Mobile Shop a polished corporate appearance. The transitions between forgot password request, login, and reset windows feel like parts of a single application framework rather than separate components.

  • Dynamic Target Controls using ASP Tag Helpers: The specialized asp-for and asp-validation-for helper pairs automate your layout mechanics. Instead of manually writing error parsing rules or string tags, .NET injects matching validation metadata from your backend view model rules directly into the HTML structure at runtime.

  • Intuitive Visual Icon Styling: The layout uses Bootstrap Icons to make user entry easy. The email field features a simple envelope icon, the primary password uses an open padlock icon, and the confirmation field utilizes a locked padlock icon. This visually underlines the transition from an open, unverified state to a newly secured account lock.

  • Client-Side Validation Shielding: The partial script tag at the bottom loads the framework's default jQuery tracking logic. If a shopper types mismatching strings into the password blocks, the tracking system blocks the form submission instantly right on the user's screen, preserving server resources and giving your tutorial viewers a smooth user experience.

Step 26: Processing and Executing Account Password Resets (HTTP POST)

Add this action method inside your AccountController.cs file to execute the final cryptographic identity updates.

C# / Controllers/AccountController.cs (ResetPassword Post Action)

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
{
    if (!ModelState.IsValid)
        return View(model);

    var user = await _userManager.FindByEmailAsync(model.Email);
    if (user == null)
    {
        return RedirectToAction(nameof(ResetPasswordConfirmation));
    }

    var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
    if (result.Succeeded)
    {
        return RedirectToAction(nameof(ResetPasswordConfirmation));
    }

    foreach (var error in result.Errors)
    {
        ModelState.AddModelError(string.Empty, error.Description);
    }

    return View(model);
}

Core Components & Security Architecture Breakdown

  • Public Processing Rules ([AllowAnonymous]): Just like the landing gateway page, this post action must remain open to public traffic. Because the client cannot log in until their credentials are fully repaired, this attribute lets them communicate directly with your validation framework from an unauthenticated browser state.

  • The Anti-Forgery Verification Lock ([ValidateAntiForgeryToken]): This security attribute intercepts incoming forms and verifies that the hidden cryptographic validation token generated by your Razor view matches your server's records exactly. This protects your application from Cross-Site Request Forgery (CSRF) attacks, preventing malicious external scripts from hijacking an open window to modify user credentials.

  • Defending Against Account Probing Redirection: If the email address submitted through the form doesn't match an active record in your database tables, look closely at how your method reacts: it immediately returns a redirect to the ResetPasswordConfirmation page anyway. This is an exceptional defensive programming technique. By treating missing emails exactly like successful requests, you block malicious actors from using this form to scan and discover valid registration emails on your Mobile Shop platform.

  • Executing Safe Cryptographic Overwrites via ResetPasswordAsync: This line handles the most critical security operational logic in the method. It hands off three variables to the core Identity engine: the matching user record, the hidden cryptographic token code from the email link, and the brand new password string. The framework reads the token, confirms it hasn't expired or been altered, purges the old password salt pattern, and securely writes the new encrypted hash straight into your database storage layers.

  • Graceful Identity Error Harvesting Loops: If the update cycle fails (for instance, if the email token has expired or the new entry fails your system's complexity rules), the foreach block iterates through the full array of errors and appends them straight to the empty model state contract. This instantly surfaces clean validation alerts right inside your view layout summary box.

Step 27: Creating the Reset Password Confirmation Landing Gateway (HTTP GET)

Add this action method inside your AccountController.cs file to serve the final confirmation view page after a password reset attempt.

C# / Controllers/AccountController.cs (ResetPasswordConfirmation Get Action)

[HttpGet]
[AllowAnonymous]
public IActionResult ResetPasswordConfirmation()
{
    return View();
}

Core Functionality & User Flow Breakdown

  • Public Access Configuration ([AllowAnonymous]): Because users landing on this page have just reset their password and are still unauthenticated, this endpoint must bypass global authorization constraints. Tagging it with this attribute ensures that anyone arriving from the password reset redirect can load this static page safely without being blocked by the application's login walls.

  • Isolating Page Requests ([HttpGet]): This attribute restricts the action method exclusively to handling standard browser page requests. It serves as a dedicated read-only target layout gateway, completely isolated from any back-end form handling or data modification operations.

  • Terminating the Reset State Machine (return View();): This method serves a crucial UX role: it breaks the form-submission loop. By routing users away from the active form post action and onto this clean, independent landing action, you prevent accidental form re-submission browser warnings. It loads a static state layout that safely tells the user their request has been processed, providing a clean link to route them back to the standard login page.

Step 28: Creating the Reset Password Confirmation Interface

Create a new file named ResetPasswordConfirmation.cshtml inside your Views/Account/ directory.

Razor / Views/Account/ResetPasswordConfirmation.cshtml
@{
    ViewData["Title"] = "Password Reset Confirmation";
}

<div class="container py-5">
    <div class="row justify-content-center">
        <div class="col-md-6 text-center">
            <div class="card shadow">
                <div class="card-body p-5">
                    <i class="bi bi-check-circle-fill text-success display-1"></i>
                    <h2 class="mt-4">Password Reset Successful</h2>
                    <p class="text-muted lead">Your password has been reset successfully.</p>
                    <p>You can now log in with your new password.</p>
                    <a asp-action="Login" class="btn btn-primary btn-lg">
                        <i class="bi bi-box-arrow-in-right"></i> Go to Login
                    </a>
                </div>
            </div>
        </div>
    </div>
</div>

Detailed Component & Interface Breakdown

  • Positive Visual Reinforcement: The use of the bi-check-circle-fill icon combined with the text-success color class provides immediate psychological feedback to the user. It instantly signals that the complex background operations—token verification, database hash updates, and cache clearing—have completed without error.

  • Minimalist Design for Maximum Clarity: By removing all form fields, labels, and validation summaries, this view eliminates any chance of user confusion. You are guiding the visitor toward the only logical next step: returning to the login page.

  • Call-to-Action (CTA) Optimization: The login button uses btn-primary and btn-lg classes to create a high-conversion UI element. By scaling the button and placing it centrally, you ensure that the path back to the application's authenticated core is unmistakable and easy to interact with on any device, especially for your mobile shoppers.

  • The "Generic Success" Security Strategy: As we discussed in the previous step, this page is where you land users regardless of whether their email was found or the reset was fully successful. It keeps the UI clean and, more importantly, ensures you don't leak information about which email addresses are registered in your system.

Step 29: Creating the Access Denied Landing Gateway (HTTP GET)

Add this action method inside your AccountController.cs file. This acts as the destination for the authorization policy engine whenever a user fails a role-based or permission-based check.

C# / Controllers/AccountController.cs (AccessDenied Get Action)

[HttpGet]
[AllowAnonymous]
public IActionResult AccessDenied()
{
    return View();
}

Core Functionality & Routing Strategy

  • Public Access Configuration ([AllowAnonymous]): This method must remain accessible to anyone—even those who aren't logged in—because it serves as the final landing zone for failed authorization attempts. If you were to lock this behind an [Authorize] filter, a guest user would trigger an infinite redirection loop, crashing their browser experience.

  • Isolating the Error State: By separating this into its own AccessDenied action, you decouple your security logic from your standard application flow. It keeps your controller cleaner and allows you to build a custom, visually appealing view that explains to the user why they were redirected, rather than just forcing them back to the login screen.

  • Standardizing the Security Response: When you define a global authorization policy in your Program.cs or Startup.cs configuration, you can explicitly point the application to this method. This creates a uniform experience across your entire Mobile Shop; no matter where the user is, if they click a link they aren't supposed to, they are guided gracefully to this safe harbor.

Step 30: Designing the Access Denied Interface

Create a new file named AccessDenied.cshtml inside your Views/Account/ directory.

Razor / Views/Account/AccessDenied.cshtml

@{
    ViewData["Title"] = "Access Denied";
}

<div class="container py-5">
    <div class="row justify-content-center">
        <div class="col-md-6 text-center">
            <div class="card shadow">
                <div class="card-body p-5">
                    <i class="bi bi-shield-exclamation text-danger display-1"></i>
                    <h2 class="mt-4">Access Denied</h2>
                    <p class="text-muted lead">You do not have permission to access this page.</p>
                    <p>If you believe this is an error, please contact the administrator.</p>
                    <div class="d-flex justify-content-center gap-3">
                        <a asp-controller="Home" asp-action="Index" class="btn btn-primary">
                            <i class="bi bi-house"></i> Go Home
                        </a>
                        <a asp-action="Login" class="btn btn-outline-primary">
                            <i class="bi bi-box-arrow-in-right"></i> Login
                        </a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

Detailed Component & Interface Breakdown

  • Immediate Visual Warning: The combination of the bi-shield-exclamation icon and the text-danger class is a universal design pattern for security alerts. It immediately notifies the user that they have encountered a permission boundary without being overly aggressive or alarming.

  • Contextual Navigation Options: Unlike the success pages, this view offers two distinct paths:

    • Go Home: Redirects the user to the store index, acting as a "reset" button for their browsing journey.

    • Login: Gives users who might have accidentally been logged out (or are using the wrong account) a fast path to re-authenticate. The use of btn-primary and btn-outline-primary creates a clear visual hierarchy, prioritizing the primary action while keeping the alternative clearly visible.

  • Maintaining Platform Consistency: By utilizing the same container, card, and shadow structures used in your login, registration, and reset pages, the experience feels like a cohesive part of the Mobile Shop application rather than a broken or generic server error page.

  • Professional Tone: The messaging—"You do not have permission to access this page"—is direct and polite. It shifts the focus from a "system error" to a "permission restriction," which helps minimize frustration for your students and customers.

Step 31: Implementing Dynamic Identity-Aware Navigation

Insert this code block into the navigation section of your Views/Shared/_Layout.cshtml file (typically inside the navbar-nav list).

Razor / Views/Shared/_LoginPartial.cshtml

@if (User.Identity?.IsAuthenticated == true)
{
    <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">
            <i class="bi bi-person-circle"></i> @User.Identity.Name
        </a>
        <ul class="dropdown-menu dropdown-menu-end">
            <li><a class="dropdown-item" asp-controller="Account" asp-action="Profile"><i class="bi bi-person"></i> My Profile</a></li>
            <li><a class="dropdown-item"><i class="bi bi-bag"></i> My Orders</a></li>
            <li><a class="dropdown-item"><i class="bi bi-heart"></i> Wishlist</a></li>
            <li><hr class="dropdown-divider"></li>
            @if (User.IsInRole("Admin"))
            {
                <li><a class="dropdown-item"><i class="bi bi-speedometer2"></i> Admin Dashboard</a></li>
                <li><hr class="dropdown-divider"></li>
            }
            <li>
                <form asp-controller="Account" asp-action="Logout" method="post" class="d-inline">
                    <button type="submit" class="dropdown-item"><i class="bi bi-box-arrow-right"></i> Logout</button>
                </form>
            </li>
        </ul>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link" asp-controller="Account" asp-action="Login">Login</a>
    </li>
    <li class="nav-item">
        <a class="nav-link" asp-controller="Account" asp-action="Register">Register</a>
    </li>
}

Core Functionality & Logic Breakdown

  • Conditional Session Rendering (User.Identity?.IsAuthenticated): This is the core security check. It asks the framework: "Is there a valid, active user session associated with this browser?"

    • If True: It renders the personalized dropdown menu, showing the username and specific account options.

    • If False: It hides the sensitive account links and replaces them with simple "Login" and "Register" buttons. This keeps your interface clean and ensures guests aren't presented with links they cannot access.

  • Contextual Personalization: Using @User.Identity.Name is a powerful UX move. It provides instant visual confirmation to the user that they are indeed signed into the correct account, which is a hallmark of professional e-commerce design.

  • Role-Based UI Filtering (User.IsInRole("Admin")): This is a perfect example of secure UI design. By wrapping the "Admin Dashboard" link in this check, you ensure that the sensitive link only appears for users who have the administrative claim. Even if a regular user managed to guess the URL, the server-side authorization we've built in previous steps would block them—but this UI check ensures they don't even see the link in the first place, reducing clutter and confusion.

  • Secure Logout via POST (form asp-action="Logout"): Notice that the Logout button is wrapped in a rather than a standard link. This is a critical security best practice. Logouts should never be performed via a simple GET request, as they can be easily triggered by link-prefetching bots or crawlers. Using a POST form ensures that the user actively initiates the session termination.


Comments