Search code examples
asp.net-coreauthenticationvalidationcookiesasp.net-core-mvc

Failing to edit existing user profile ASP.NET Core MVC


I am building this project as practice. I have done the basic features needed for user login, signup and logout. I wanted to add edit user profile feature to the project, however it is not working, there are no exceptions provided.

Moreover the ModelState is invalid for the Guid Id at the Edit action method as it is also passing {00000000-0000-0000-0000-000000000000} as the Guid and not the Guid of the logged-in user, how can I Validate the user Id properly so that everything works correctly?

I have done the cookies authentication as my own custom authentication without using the scaffolded one (Identity).

You can refer to my previous questions for the code

  1. (Editing existing user profiles ASP.NET Core MVC).
  2. (Authorization is not working properly in ASP.NET Core MVC)

Edit 1:

After debugging and trying new things out, I figured out that I should pass the user id to the view and controller after the user is valid and can login, I made some changes here and there, I even created a new method for editing user profile.

However, I have a new issue, at first any user logged in or not can change the URL to their liking and access the update profile page of other users for example the user can modify the last route in the URL to any other Guid and have access to that page localhost:44334/Users/UpdateProfile/19F17789-A52D-4752-5372-08DBA10B2W69

I tried adding [Authorize] to one of the action methods of UpdateProfile but still it didn't work, but then I placed it on both methods for UpdateProfile then it worked in which logged in users can access it only, but I still have the main issue of users accessing each others' edit profile page.

Here is the new code.

Controller that has login and logout:

    public class LogUsersController : Controller
{
    private readonly ILogger<LogUsersController> _logger;

    private readonly TheAppContext _context;

    public static int CanUserLogout; //0/CANT, 1/CAN

    public static string DisplayedUsername;

    //public Guid DisplayedUserId { get; protected set; }

    public static Guid DisplayedUserId; // Add this field


    public LogUsersController(TheAppContext context, ILogger<LogUsersController> logger)
    {
        _context = context;
        _logger = logger;
    }
    public ActionResult Expired()
    {
        CanUserLogout = 0;
        TempData["Message"] = "★ Please sign-in to gain access...";
        
        return RedirectToAction("Login", "Logusers");
    }
    public IActionResult Index()
    {
        return View();
    }
    public IActionResult Login()
    {
        ViewBag.msg = TempData["Message"];
        return View();
    }
    [AllowAnonymous]
    [HttpPost]
    public async Task<IActionResult> Login(Users login) //login users
    {

        if(IsValidUser(login.Username, login.Password))
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, login.Username),
                new Claim(ClaimTypes.Role, "User"),
            };
            var claimsIdentity = new ClaimsIdentity(
                claims, CookieAuthenticationDefaults.AuthenticationScheme);
            var authProperties = new AuthenticationProperties
            {
                //The time at which the authentication ticket expires.
                //ExpiresUtc = DateTime.Now.AddMinutes(60),

                // let the cookie being deleted when session ends (browser closed).
                IsPersistent = false,
            };
            
            await HttpContext.SignInAsync(
            CookieAuthenticationDefaults.AuthenticationScheme,
            new ClaimsPrincipal(claimsIdentity),
            authProperties);

            // Log the value of login.Id
            _logger.LogInformation($"login.Id: {login.Id}");

            DisplayedUsername = "@" + login.Username;
            //DisplayedUserId = login.Id; // Store the user's ID here

            CanUserLogout = 1;

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

        }
        else
        {
            
            ViewBag.message = "Failed to login with @" + login.Username + ", please try again later..";
            return View();
        }

    }
    
    private bool IsValidUser(string username, string password)
    {
        var user = _context.Users.FirstOrDefault(u => u.Username == username && u.Password == password);

        if (user != null)
        {
            // Log user.Id and other relevant information
            _logger.LogInformation("User found: Username = {Username}, Id = {UserId}", user.Username, user.Id);
            DisplayedUserId = user.Id; // Store the user's ID here
            return true;
        }
        // Log failed login attempts
        _logger.LogInformation("Login failed for username: {Username}", username);
        return false;
    }
    

    [Authorize]
    public async Task<ActionResult> Logout()
    {
        await HttpContext.SignOutAsync(
            CookieAuthenticationDefaults.AuthenticationScheme);
        CanUserLogout = 0;
        return RedirectToAction("Login", "Logusers");
    }

}

Relevant part of _Layout.cshtml which contains routing for the user to edit profile.

                <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
                <ul class="navbar-nav flex-grow-1">
                    <li class="nav-item">
                        <a class="nav-link text-white" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link text-white" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                    </li>

                    @if (TheApp.Controllers.LogUsersController.CanUserLogout == 1){

                <li class="nav-item">
                    <a class="nav-link text-white" asp-area="" asp-controller="Logusers" asp-action="Logout">Logout</a>
                </li>
                <li class="nav-item">
                    <a class="navbar-text text-white font-italic">Hello there,  @TheApp.Controllers.LogUsersController.DisplayedUsername!</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link text-white" asp-controller="Users" asp-action="UpdateProfile" asp-route-id="@TheApp.Controllers.LogUsersController.DisplayedUserId">Profile</a>
                </li>
            }
            else
            {
                <li class="nav-item">
                    <a class="nav-link text-white" asp-area="" asp-controller="Logusers" asp-action="Login">Login</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link text-white" asp-area="" asp-controller="Users" asp-action="Create">Register</a>
                </li>
            }

Controller that has Create user and Update User profile methods.

 public class UsersController : Controller
{

    private readonly TheAppContext _context;

    public UsersController(TheAppContext context)
    {
        _context = context;
    }
    public IActionResult Index()
    {
        return View();
    }
    public IActionResult Create()
    {
        return View();
    }
    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Create(Users uc) //register users
    {
        _context.Add(uc);
        _context.SaveChanges();
        return View(uc); //uc?
    }
    [Authorize]
    public IActionResult UpdateProfile(Guid id)
    {
        Users user = _context.Users.SingleOrDefault(u => u.Id == id);

        if (user != null)
        {
            return View(user);
        }
        else
        {
            return NotFound();
        }
    }
    [Authorize]
    [HttpPost]
    public IActionResult UpdateProfile(Users updatedUser)
    {
        if (ModelState.IsValid)
        {
            Users existingUser = _context.Users.FirstOrDefault(u => u.Id == updatedUser.Id);

            if (existingUser != null)
            {
                existingUser.Name = updatedUser.Name;
                existingUser.Email = updatedUser.Email;
                existingUser.Username = updatedUser.Username;
                existingUser.Password = updatedUser.Password;
                existingUser.ConfirmPass = updatedUser.ConfirmPass;

                _context.SaveChanges();

                return RedirectToAction("Index", "Home");
            }
            else
            {
                return NotFound();
            }
        }

        return View(updatedUser);
    }
}

Solution

  • I solved the final issue by adding a new claim to the list which stores the user ID then in the UpdateProfile method I check if the id in the parameter matches the id stored in the claim. Here is the updated relevant code.

    LogusersController.cs

        [AllowAnonymous]
        [HttpPost]
        public async Task<IActionResult> Login(Users login) //login users
        {
    
            if(IsValidUser(login.Username, login.Password))
            {
                var claims = new List<Claim>
                {
    
                    new Claim(ClaimTypes.Name, login.Username),
                    new Claim(ClaimTypes.Role, "User"),
                    new Claim(ClaimTypes.NameIdentifier, DisplayedUserId.ToString()), // Add the user's ID as the NameIdentifier claim
                };
                var claimsIdentity = new ClaimsIdentity(
                    claims, CookieAuthenticationDefaults.AuthenticationScheme);
                var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
    
                var authProperties = new AuthenticationProperties
                {
                    //The time at which the authentication ticket expires.
                    //ExpiresUtc = DateTime.Now.AddMinutes(60),
    
                    // let the cookie being deleted when session ends (browser closed).
                    IsPersistent = false,
                };
                
                await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(claimsIdentity),
                authProperties);
    
                _logger.LogInformation($"Claim Types.NameIdentifier set: Value = {DisplayedUserId}");
    
    
                // Log the value of login.Id
                _logger.LogInformation($"login.Id: {DisplayedUserId}");
    
                DisplayedUsername = "@" + login.Username;
                //DisplayedUserId = login.Id; // Store the user's ID here
    
                CanUserLogout = 1;
    
                return RedirectToAction("Index", "Home");
    

    UsersController

    [Authorize]
        public IActionResult UpdateProfile(Guid id)
        {
            // Get the currently authenticated user's ID
            var authenticatedUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
    
            if (id != authenticatedUserId)
            {
                // User is trying to access another user's profile, handle accordingly (e.g., redirect to unauthorized page)
                return Forbid();
            }
    
            Users user = _context.Users.SingleOrDefault(u => u.Id == id);
    
            if (user != null)
            {
                return View(user);
            }
            else
            {
                return NotFound();
            }
        }
        [Authorize]
        [HttpPost]
        public IActionResult UpdateProfile(Users updatedUser)
        {
            if (ModelState.IsValid)
            {
                Users existingUser = _context.Users.FirstOrDefault(u => u.Id == updatedUser.Id);
    
                // Get the currently authenticated user's ID
                var authenticatedUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
    
                if (existingUser == null || existingUser.Id != authenticatedUserId)
                {
                    // User is trying to update another user's profile or the profile doesn't exist, handle accordingly
                    return NotFound();
                }
    
                existingUser.Name = updatedUser.Name;
                existingUser.Email = updatedUser.Email;
                existingUser.Username = updatedUser.Username;
                existingUser.Password = updatedUser.Password;
                existingUser.ConfirmPass = updatedUser.ConfirmPass;
    
                _context.SaveChanges();
            }
    
            return View(updatedUser);
        }