Search code examples
c#asp.net-core.net-coreasp.net-core-mvcasp.net-identity

Can I automatically log back into my SignInManager and UserManager (Identity) via using cookies -> Asp.Net-Core Identity (Deployed live)


The primary question is: Can I automatically log back into my SignInManager and UserManager (Identity) via using cookies; and in the event this isn't possible, what would you recommend as an alternative?

So, the primary issue here boils down to the fact my dedicated IIS pool terminates the workers after 5 minutes; now I can configure this to session-based but since it's a shared server, this brings a whole new area of issues.
When the workers get terminated, the session expires and all logged in users will logout. However, we still have cookies.
It is worth noting that my target framework is "netcoreapp3.1" and that the deployment of this project is to a live server.

Let's delve into the main things you'd need to know:
The Login Function (Path: /Account/Login):

public async Task<IActionResult> Login(LoginViewModel model, string returnUrl)
    {       
        if (ModelState.IsValid)
        {
            var result = await signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberMe, false);
            if (result.Succeeded)
            {
                if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
                {
                    return Redirect(returnUrl);
                }
                else
                {
                    return RedirectToAction("Index", "Home");
                }
            }

            ModelState.AddModelError(String.Empty, "Invalid Login Attempt");
        }
        return View(model);
    }

Next, the relevant startup, in ConfigureServices:

services.AddIdentity<ApplicationUser, IdentityRole>(options =>
         {
             options.Password.RequiredLength = 3;
             options.Password.RequiredUniqueChars = 0;
             options.Password.RequireNonAlphanumeric = false;
             options.Password.RequireLowercase = false;
             options.Password.RequireUppercase = false;
             options.Password.RequireDigit = false;

         }).AddEntityFrameworkStores<AppDbContext>().AddDefaultTokenProviders();

services.ConfigureApplicationCookie(e =>
        {   
            e.LoginPath = new PathString("/Account/Login");
            e.LogoutPath = new PathString("/Account/Logout");
            e.AccessDeniedPath = new PathString("/Account/AccessDenied");
            e.Cookie.MaxAge = TimeSpan.FromDays(3);
            e.ExpireTimeSpan = TimeSpan.FromDays(3);
            e.Cookie.HttpOnly = true;
            e.SlidingExpiration = true;
            e.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
        });

And in Configure:

app.UseAuthentication();

AddIdentity default calls services.AddAuthentication();
This creates your standard boilerplate Identity.Application Cookie, whilst the session doesn't terminate or crash, it'll keep you logged in for the 3 days; naturally since our session does expire, this terminates in 5 minutes. So, to restate the question at the start, is it possible to keep the user logged in (or relog them in) using the cookie we have so that the user won't be adversely affected by the session expiring?

So, am I fundamentally flawed in what I functionality I want to deploy, or is this something that's possible with a bit of working around?
Quite a few of the "solutions" I've come across don't have a way to log back into the SignInManager/UserManager or have since become deprecated.

Any advice here would be greatly apprecaited! :)


Solution

  • Now I managed to create a solution to this, and I'll start by saying although it works, it has its own set of problems (primarily with security).

    In this it is worth stating that I already had an ApplicationUser setup, if you do not then you would need to create it extending IdentityUser to add the cookieId field into your identity database.

    The first step was to create a way for the user to login in to signInManager via cookies, we achieved this by extending the UserManager class as so:

     public static class UserManagerExtensions
    {
        public static async Task<ApplicationUser> FindByCookieAsync(this UserManager<ApplicationUser> um, string cookieId)
        {
            return await um?.Users?.SingleOrDefaultAsync(x => x.CookieId.Equals(cookieId));
        }
    }
    

    This allows us to retrieve the ApplicationUser which we will use to login with.

    The next step is setting this cookie, we do this by applying the following code to both our login and register (if your register automatically logs the user in):

    var cookieVal = generateRandomCookieValue();
                    HttpContext.Response.Cookies.Append("CookieName", cookieVal, new Microsoft.AspNetCore.Http.CookieOptions
                    {
                        Expires = DateTimeOffset.UtcNow.AddDays(7),
                        HttpOnly = true,
                        Secure = true,
                        SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax
                    }) ;
    
                    if (cookieVal != null)
                    {
    
                        ApplicationUser userModel = await userManager.FindByNameAsync(model.Username);
                        userModel.CookieId = cookieVal;
                        await userManager.UpdateAsync(userModel);
                    }
    

    When the user is logged out, we simply delete this cookie from the header and clear the stored cookie from the database. This section is called in our Logout method:

     HttpContext.Response.Cookies.Delete("CookieName");
            ApplicationUser userModel = await userManager.FindByNameAsync(User.Identity.Name);
            userModel.CookieId = null;
            await userManager.UpdateAsync(userModel);
    

    Personally, I use this block of code after the user has successfully signed in. Since we're not storing any data in our cookie and instead just using it as a key, we don't need a complex cookie, however I preferred to mimic the length of Identity.Application's cookie.

    private string generateRandomCookieValue()
        {
            StringBuilder returnString = new StringBuilder();
            Random rand = new Random();
            for (int i = 0; i < 646; i++)
            {
                var x = rand.Next(48, 124);
                if((x<58 || x>64) && (x<91 || x > 96))
                {
                    if (x == 123)
                    {
                        returnString.Append('-');
                    }
                    else if (x == 124)
                    {
                        returnString.Append('_');
                    }
                    else
                    {
                        returnString.Append((char)x);
                    }
                }
                else
                {
                    if (i != 0)
                    {
                        i--;
                    }
                }
            }
            return returnString.ToString();
        }
    

    Now we have our cookie being created for the duration of 1 week after the user logs in, we need a way to check the status of this every request to potential log the user back in. In our startup configure method we add the following block:

    app.Use(async (context, next) =>
            {
                var cookie = context.Request.Cookies["CookieName"];
                var userAuth = context.User.Identity.IsAuthenticated;
                if (cookie != null && !userAuth)
                {
                    context.Request.Method = "POST";
                    context.Request.Path = "/Account/AutoLogin";
                }
                await next();
            });
    
            app.UseRouting();
    

    We need to add the "UseRouting()" method for this to work. We're using this method to direct to an AutoLogin method which is the last step.

    [HttpPost]
        public async Task<IActionResult> AutoLogin()
        {
            var cookie = HttpContext.Request.Cookies["CookieName"];
            var user = await userManager.FindByCookieAsync(cookie);
            if(user != null)
            {
                await signInManager.SignInAsync(user, true);
    
                //If you want to configuring sliding expiration (which essentially issues a new cookie), you will need to manually do it here
                //Using the same setup we used in login, and then propagate that update to the database.
                var y = HttpContext;
                return RedirectToAction("Index", "Home");
            }
            else
            {
                HttpContext.Response.Cookies.Delete("CookieName");
                return RedirectToAction("Index", "Home");
            }            
        }
    

    In our autoLogin method we could manually create a sliding expiration by creating a new cookie, and updating the user within the database, this process is similar to the login.

    Similar to a session, there is the security problem of if you have a users' cookie, you can log in as them, I could not think of a way around this with this solution, adding the custom sliding expiration to issue a new cookie minorly increases it. Secure connections will very much be recommend.

    I hope it helps to guide anyone along to a potentially better solution for those facing the same issue.