AccountController:

using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Havit.ViewModels;
using System.Threading.Tasks;
using Havit.Services.Interfaces;

//This is not the best way to do this, but it's the only way to deploy on github pages, so it'll do for now.
public class AuthController : Controller
{
    private readonly IUserStore _userStore;
    private const string SessionKeyUser = "_User";

    public AuthController(IUserStore userStore)
    {
        _userStore = userStore;
    }

    public IActionResult Login()
    {
        return View();
    }

    public IActionResult Register()
    {
        return View();
    }

    public IActionResult AccessDenied()
    {
        return View();
    }

    [HttpPost]
    public async Task<IActionResult> Login(LoginVM model)
    {
        if (ModelState.IsValid)
        {
            if (_userStore.UserExists(model.Username))
            {
                if (_userStore.ValidateUser(model.Username, model.Password))
                {
                    var claims = new List<Claim>
                    {
                        new Claim(ClaimTypes.Name, model.Username),
                        new Claim(ClaimTypes.Role, "User")
                    };

                    var claimsIdentity = new ClaimsIdentity(
                        claims, CookieAuthenticationDefaults.AuthenticationScheme);

                    var authProperties = new AuthenticationProperties
                    {
                        IsPersistent = model.RememberMe,
                        ExpiresUtc = DateTimeOffset.UtcNow.AddHours(1)
                    };

                    await HttpContext.SignInAsync(
                        CookieAuthenticationDefaults.AuthenticationScheme,
                        new ClaimsPrincipal(claimsIdentity),
                        authProperties);

                    HttpContext.Session.SetString("Username", model.Username);

                    var principal = await HttpContext.AuthenticateAsync();
                    if (principal.Succeeded &&
                        principal.Principal.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == "User"))
                    {
                        Console.WriteLine("Access granted");
                        return RedirectToAction("Index", "HabitTracker");
                    }
                    else
                    {
                        Console.WriteLine("Access denied");
                        return RedirectToAction("AccessDenied", "Auth");
                    }
                }
            }

            ModelState.AddModelError("", "Invalid username or password");
        }

        return View(model);
    }

    [HttpPost]
    public async Task<IActionResult> Register(RegisterVM model)
    {
        if (ModelState.IsValid)
        {
            if (_userStore.UserExists(model.Username))
            {
                ModelState.AddModelError("Username", "Username already exists");
                return View(model);
            }

            if (!model.AgreedToTos)
            {
                ModelState.AddModelError("AgreedToTos", "You must agree to the Terms of Service");
                return View(model);
            }

            if (!_userStore.AddUser(model.Username, model.Password))
            {
                ModelState.AddModelError("", "Unable to register user. Please try again.");
                return View(model);
            }

            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, model.Username),
                new Claim(ClaimTypes.Role, "User")
            };

            var claimsIdentity = new ClaimsIdentity(
                claims, CookieAuthenticationDefaults.AuthenticationScheme);

            var authProperties = new AuthenticationProperties
            {
                IsPersistent = true,
                ExpiresUtc = DateTimeOffset.UtcNow.AddDays(7)
            };

            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(claimsIdentity),
                authProperties);

            HttpContext.Session.SetString(SessionKeyUser, model.Username);

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

        return View(model);
    }

    [HttpPost]
    public async Task<IActionResult> Logout()
    {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        HttpContext.Session.Clear();
        return RedirectToAction("Login");
    }
}

InMemoryStore:

using System.Collections.Concurrent;
using Havit.Services.Interfaces;
using Havit.Services.Utilities;

namespace Havit.Services.Implementations
{
    public class InMemoryUserStore : IUserStore
    {
        private readonly ConcurrentDictionary<string, (string, bool)> _users = new ConcurrentDictionary<string, (string, bool)>();
        private readonly IPasswordHasher _passwordHasher;

        public InMemoryUserStore(IPasswordHasher passwordHasher)
        {
            _passwordHasher = passwordHasher;
        }

        public bool AddUser(string username, string password)
        {
            string hashedPassword = _passwordHasher.HashPassword(password);
            return _users.TryAdd(username, (hashedPassword, false));
        }

        public bool ValidateUser(string username, string password)
        {
            if (_users.TryGetValue(username, out var userInfo))
            {
                return _passwordHasher.VerifyPassword(userInfo.Item1, password);
            }
            return false;
        }

        public bool UserExists(string username)
        {
            return _users.ContainsKey(username);
        }

        public bool IsUserAdmin(string username)
        {
            if (_users.TryGetValue(username, out var userInfo))
            {
                return userInfo.Item2;
            }
            return false;
        }
    }
}
IUserStore:

namespace Havit.Services.Interfaces
{
    public interface IUserStore
    {
        bool AddUser(string username, string password);
        bool ValidateUser(string username, string password);
        bool UserExists(string username);
        bool IsUserAdmin(string username);
    }
}
RegisterVM:

using System.ComponentModel.DataAnnotations;

namespace Havit.ViewModels
{
    public class RegisterVM
    {
        [Required(ErrorMessage = "Username is required for registration.")]
        [RegularExpression("^[a-zA-Z0-9]*$", ErrorMessage = "Username can only contain letters and numbers")]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 3)]
        public string Username { get; set; } = string.Empty;

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

        [Required(ErrorMessage = "Please confirm the password.")]
        [Compare("Password", ErrorMessage = "Passwords do not match.")]
        [Display(Name = "Confirm password")]
        [DataType(DataType.Password)]
        public string ConfirmPassword { get; set; } = string.Empty;

        [Required(ErrorMessage = "You must agree to the Terms of Service")]
        public bool AgreedToTos { get; set; }
    }
}
_LoginPartial:

@using Microsoft.AspNetCore.Mvc.TagHelpers

<ul class="navbar-nav">
    @if (User.Identity?.IsAuthenticated == true)
    {
        <li class="nav-item dropdown">
            <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                @User.Identity.Name
            </a>
            <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
                <li>
                    <form method="post" asp-controller="Auth" asp-action="Logout">
                        <button type="submit" class="dropdown-item text-white">Logout</button>
                    </form>
                </li>
            </ul>
        </li>
    }
    else
    {
        <li class="nav-item align-self-center">
            <a class="text-white btn btn-primary" id="LoginBtn" asp-area="" asp-controller="Auth" asp-action="Login">Login</a>
        </li>
    }
</ul>