IDatabaseInitializer called with HttpContext.Current == null

Oct 28, 2014 at 7:36 PM
Edited Oct 29, 2014 at 1:09 PM
I use an IDatabaseInitializer to initialize some default users and roles in my identity 2 database tables...which IDatabaseInitializer does not matter (tested with DropCreateDatabaseIfModelChanges, MigrateDatabaseToLatestVersion, and CreateDatabaseIfNotExists). When I went to deploy to production for the first time, and the relative Seed() was called, HttpContext.Current would always be null. This did not happen to me when I was developing, so I knew that it had to be some code I had modified since creating my tables in development.

I tracked it down to some code I had put in my AccountController::Login() function. I want my users to be able to login with either their email address or their username, and I have also extended the ApplicationUser to have an "IsActive" boolean column, so I want to make sure the user is active before I allow them to login.

My login function looked like this:
        // POST: /Account/Login
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            // Look to see if they entered email address, and make sure they are active
            var sUserName = model.UserName;
            var bUserActive = true;
            var user = UserManager.FindByEmail(model.UserName);
            if (user != null)
            {
                sUserName = user.UserName;
                bUserActive = user.IsActive;
            }
            else
            {
                user = UserManager.FindByName(model.UserName);
                if (user != null)
                {
                    bUserActive = user.IsActive;
                }
            }
            if (!bUserActive)
            {
                ModelState.AddModelError("", "Invalid login attempt.");
                return View(model);
            }

            // This doesn't count login failures towards account lockout
            // To enable password failures to trigger account lockout, change to shouldLockout: true
            var result = await SignInManager.PasswordSignInAsync(sUserName, model.Password, model.RememberMe, shouldLockout: false);
            switch (result)
            {
                case SignInStatus.Success:
                    return RedirectToLocal(returnUrl);
                case SignInStatus.LockedOut:
                    return View("Lockout");
                case SignInStatus.RequiresVerification:
                    return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
                case SignInStatus.Failure:
                default:
                    ModelState.AddModelError("", "Invalid login attempt.");
                    return View(model);
            }
        }
As would be expected, the call to UserManager.FindByEmail() would trigger the IDatabaseInitializer to create and seed the tables. What was not expected was that, in doing so the HttpContext.Current is null within those function calls.

So I am assuming that I must have been doing that check in the wrong place, but where is the correct place to do additional user validation?

Currently I have removed the check from my AccountController::Login(), and have instead overridden SignInManager::PasswordSignInAsync() like so:
        public override async Task<SignInStatus> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout)
        {
            var bUserActive = false;

            var eRetVal = await base.PasswordSignInAsync(userName, password, isPersistent, shouldLockout);

            // If it failed to login using the userName as the actual UserName
            if(eRetVal == SignInStatus.Failure)
            {
                // Go see if the userName is actually an email address
                var user = UserManager.FindByEmail(userName);
                if (user != null)
                {
                    // If it did find userName as an Email address, then try logging in using the real UserName
                    userName    = user.UserName;
                    bUserActive = user.IsActive;

                    eRetVal = await base.PasswordSignInAsync(userName, password, isPersistent, shouldLockout);
                }
            }
            // Else if they were able to login successfully
            else if(eRetVal == SignInStatus.Success)
            {
                // Go get the user to find out if they are Active
                var user = UserManager.FindByName(userName);
                if (user != null)
                {
                    bUserActive = user.IsActive;
                }
            }

            // If we successfully logged in, but the user is not Active
            if( (eRetVal == SignInStatus.Success) && ( ! bUserActive ) )
            {
                // Log them out and return failure
                AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie, DefaultAuthenticationTypes.TwoFactorCookie);

                eRetVal = SignInStatus.Failure;
            }

            return eRetVal;
        }
All this essentially does is do the check after the PasswordSignInAsync is called, which apparently does some magic to make sure the HttpContext is configured properly for the IDatabaseInitializer, but is it the correct way?

Thanks for any insight,
Andrew