Search code examples
c#cookiesasp.net-core-mvcauthorization.net-8.0

User not Signed in after SignInManager.PasswordSignInAsync method Call


I'm having trouble understanding why my user is not signed in. After signing in, it should show the navbar, but I get a redirect to the login page. I have no idea why this behavior exists if I am signed in and the result has succeeded. What I found from my research is that it appears that the sign-in is successful but it should not redirect to the login page. I have the Authorize attribute in the Admin Controller to authorize the user to sign in. I am using .NET8 MVC and the code is as follows.

Program.cs


builder.Services.AddDefaultIdentity<User>()
    .AddRoles<IdentityRole<int>>()
    .AddEntityFrameworkStores<MyDBContext>();

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(opts => {
        opts.LoginPath = "/Home/Login";
        opts.LogoutPath = "/Home/Login";
        opts.ExpireTimeSpan = TimeSpan.FromMinutes(30);
        opts.AccessDeniedPath = "/Home/Unauthorized";
        opts.SlidingExpiration = true;
    });

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Login}/{id?}");

app.Run();

HomeController.cs


public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly SignInManager<User> _signInManager;

    public HomeController(ILogger<HomeController> logger, SignInManager<User> signInManager)
    {
        _logger = logger;
        _signInManager = signInManager;
    }

    [HttpPost]
    public async Task<IActionResult> Login(LoginViewModel loginCreds)
    {
        if(!ModelState.IsValid)
            return View(loginCreds);

        _logger.LogInformation("Trying to sign in user");
        var result = await _signInManager.PasswordSignInAsync(loginCreds.Email, loginCreds.Password, false, false);
        if(!result.Succeeded)
        {
            _logger.LogInformation("Failed attempt");

            ModelState.AddModelError(string.Empty, "Hmm something went wrong!");
            return View(loginCreds);
        }

        _logger.LogInformation("Succeeded");
        return RedirectToAction("NewClient", "Admin");
    }

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

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

_Layout.cshtml


@using Microsoft.AspNetCore.Identity
@inject SignInManager<User> SignInManager

    <body>
        // Why does this come back false when user is signed in
        @if(SignInManager.IsSignedIn(User))
        {
        <header>
            <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
                <div class="container-fluid">
                    <a class="navbar-brand" asp-area="" asp-controller="Admin" asp-action="Dashboard">SuperAnchor</a>
                    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                            aria-expanded="false" aria-label="Toggle navigation">
                        <span class="navbar-toggler-icon"></span>
                    </button>
                    <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                        <ul class="navbar-nav flex-grow-1">
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Admin" asp-action="Dashboard">Home</a>
                            </li>
                            @* <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Admin" asp-action="TestPage">Test Page</a>
                                @Html.ActionLink("Test Link Page", "TestPage", "Admin", null, new { @class="nav-link text-dark" })
                            </li> *@
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                            </li>
                            @if(User.IsInRole("Admin"))
                            {
                                <li class="nav-item">
                                    <a class="nav-link text-dark" asp-area="" asp-controller="Admin" asp-action="NewClient">Add new Client</a>
                                </li>
                                <li class="nav-item">
                                    <a class="nav-link text-dark" asp-area="" asp-controller="Admin" asp-action="AddProduct">Add new Product</a>
                                </li>
                            }
                        </ul>
                    </div>
                    @await Html.PartialAsync("_LoginPartial")
                </div>
            </nav>
        </header>
        }

        <div class="container">
            <main role="main" class="pb-3">
                @* @await Html. *@
                @RenderBody()
            </main>
        </div>
    </body>
</html>

AdminController.cs

    
    [Authorize]
    public class AdminController : Controller
    {
        private readonly ILogger<AdminController> _logger;
        private readonly UserManager<User> _userManager;
        private readonly RoleManager<IdentityRole<int>> _roleManager;
    
        public AdminController(ILogger<AdminController> logger, UserManager<User> userManager, RoleManager<IdentityRole<int>> roleManager)
        {
            _logger = logger;
            _userManager = userManager;
            _roleManager = roleManager;
        }
    
        public IActionResult NewClient()
        {
            return View();
        }
    }

EDIT

After some debugging, I found out on the _Layout.cshtml the user is not authenticated (@if(SignInManager.IsSignedIn(User))). That to me sounds like it never signed in properly. How is that possible if the signInManager.PasswordSignInAsync method in HomeControler succeeded?

After further investigation I added these lines in HomeController after the if check in Login post method to validate the user is signed in.


    var user = await _userManager.FindByEmailAsync(loginCreds.Email);
        var userPrincipals = await _signInManager.CreateUserPrincipalAsync(user);
        // Boolean comes out true
        bool signdIn = _signInManager.IsSignedIn(userPrincipals);
        _logger.LogInformation("Succeeded " + signdIn);


Solution

  • I noticed that you have builder.Services.AddDefaultIdentity and builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme at the same time, so that I deduce you are trying to use the asp.net core default identity, but you might want to implement your custom sign-in logic.

    If you don't want to have custom business logic in your app, I suggest you using the built-in method so that you don't need to do so much work on the sign in module. In Visual Studio, we can create a new mvc project with Identity, it requires us to choose the authentication type as Individual User Accounts. We will have a project which having an Areas folder but we won't see so many view files. If we want to manage the views, we can use scaffold tool to get the view files and modify them. Just need to right click on the Area folder -> add -> new scaffolded items... -> choose identity -> pick up the required views.

    If you insist on implementing your own login business and just want to relay on the methods provided by default identity, like SignInManager you used, you can follow this section to use builder.Services.ConfigureApplicationCookie instead of builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie.

    If you don't want to use the default identity(using asp.net core default identity requires a database which having some default tables, and when you create a new app following the tutorial I shared above, you will get a local database too), you just want to implement cookie based authentication for test purpose and the user accounts and passwords might be hard code in your app, you can also take a look at this blog, or you can follow this official document, it shows how to use cookie authentication without ASP.NET Core Identity. It uses codes below to sign in user. By the way I have builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(x => x.LoginPath = "/account/login"); in my Program.cs

    [HttpPost]
    public async Task<IActionResult> Login(LoginModel objLoginModel)
    {
        if (ModelState.IsValid)
        {
            var user = users.Where(x => x.Username == objLoginModel.UserName && x.Password == objLoginModel.Password).FirstOrDefault();
            if (user == null)
            {
                //Add logic here to display some message to user    
                ViewBag.Message = "Invalid Credential";
                return View(objLoginModel);
            }
            else
            {
                //A claim is a statement about a subject by an issuer and    
                //represent attributes of the subject that are useful in the context of authentication and authorization operations.    
                var claims = new List<Claim>() {
                new Claim(ClaimTypes.NameIdentifier, Convert.ToString(user.UserId)),
                    new Claim(ClaimTypes.Name, user.Username),
                    new Claim(ClaimTypes.Role, user.Role),
                    new Claim("FavoriteDrink", "Tea")
            };
                //Initialize a new instance of the ClaimsIdentity with the claims and authentication scheme    
                var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
                //Initialize a new instance of the ClaimsPrincipal with ClaimsIdentity    
                var principal = new ClaimsPrincipal(identity);
                //SignInAsync is a Extension method for Sign in a principal for the specified scheme.    
                await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, new AuthenticationProperties()
                {
                    IsPersistent = objLoginModel.RememberLogin
                });
    
                return LocalRedirect(objLoginModel.ReturnUrl);
            }
        }
        return View(objLoginModel);
    }
    

    And use codes below to check the user sign-in status.

    <ul class="navbar-nav">
        @if (User.Identity.IsAuthenticated)
        {
            <li class="nav-item">
                <span class="navbar-text text-dark">Hello @User.Identity.Name!</span>
            </li>
            <li class="nav-item">
                <a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignOut">Sign out</a>
            </li>
        }
        else
        {
            <li>user is : @User.Identity.Name  + @User.Identity.IsAuthenticated</li>
            <li class="nav-item">
                <a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a>
            </li>
        }
    </ul>