Sign In Without Specifying Tenant (Mvc)

demirmusa
02 March 2022

Normally, ASP.NET Zero uses tenant information in login transactions. This document shows you how to implement tenant information independent login.

Important Note: Your user's email addresses have to be unique to implement this solution. Otherwise, this solution may not work correctly.

Updating LogInManager

  • First of all, open LogInManager. (It is located in aspnet-core\src\[YOURAPPNAME].Application\Authorization folder.)

  • Add lines shown below

  UserStore _userStore
  public LogInManager(
  	//....
      UserStore userStore
  ){
      _userStore = userStore;
  }
  
  [UnitOfWork]
  public async Task<AbpLoginResult<Tenant, User>> LoginAsync(UserLoginInfo login)
  {
      var result = await LoginAsyncInternal(login);
      await SaveLoginAttemptAsync(result, result.Tenant.Name, login.ProviderKey + "@" + login.LoginProvider);
      return result;
  }
  
  protected async Task<AbpLoginResult<Tenant, User>> LoginAsyncInternal(UserLoginInfo login)
  {
      if (login == null || login.LoginProvider.IsNullOrEmpty() || login.ProviderKey.IsNullOrEmpty())
      {
          throw new ArgumentException("login");
      }
      using (UnitOfWorkManager.Current.DisableFilter(AbpDataFilters.MayHaveTenant))
      {
          var user = await _userStore.FindAsync(login);
          if (user == null)
          {
              return new AbpLoginResult<Tenant, User>(AbpLoginResultType.UnknownExternalLogin);
          }
          //Get and check tenant
          Tenant tenant = null;
          if (!MultiTenancyConfig.IsEnabled)
          {
              tenant = await GetDefaultTenantAsync();
          }
          else if (user.TenantId.HasValue)
          {
              tenant = await TenantRepository.FirstOrDefaultAsync(t => t.Id == user.TenantId);
              if (tenant == null)
              {
                  return new AbpLoginResult<Tenant, User>(AbpLoginResultType.InvalidTenancyName);
              }
              if (!tenant.IsActive)
              {
                  return new AbpLoginResult<Tenant, User>(AbpLoginResultType.TenantIsNotActive, tenant);
              }
          }
          return await CreateLoginResultAsync(user, tenant);
      }
  }

Then, your LogInManager will be able to use given user's tenant for login process.

Updating UserManager

  • Go to UserManager. (It is located in aspnet-core\src\[YOURAPPNAME].Core\Authorization\Users folder.)

  • And add following lines;

  public async Task<int?> TryGetTenantIdOfUser(string userEmail)
  {
      using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.MayHaveTenant))
      {
          var user = await Users.SingleOrDefaultAsync(u => u.EmailAddress == userEmail.Trim());
          return user?.TenantId;
      }
  }

Updating AccountController

  • Then, go to AccountController. (It is located in aspnet-core\src\[YOURAPPNAME].Mvc\Controllers folder.)

  • Replace the function named GetTenancyNameOrNull with the following content

  private async Task<string> GetTenancyNameOrNull(string email)
  {
      var tenantId = await _userManager.TryGetTenantIdOfUser(email);
      if (!tenantId.HasValue)
      {
          return null;
      }
      return _tenantCache.GetOrNull(tenantId.Value)?.TenancyName;
  }
  • Replace the function named Login(LoginViewModel loginModel, string returnUrl = "", string returnUrlHash = "", string ss = "") with the following content
  [HttpPost]
  [UnitOfWork]
  public virtual async Task<JsonResult> Login(LoginViewModel loginModel, string returnUrl = "", string returnUrlHash = "", string ss = "")
  {
      returnUrl = NormalizeReturnUrl(returnUrl);
      if (!string.IsNullOrWhiteSpace(returnUrlHash))
      {
          returnUrl = returnUrl + returnUrlHash;
      }
      
      if (UseCaptchaOnLogin())
      {
          await _recaptchaValidator.ValidateAsync(HttpContext.Request.Form[RecaptchaValidator.RecaptchaResponseKey]);
      }
      
      var loginResult = await GetLoginResultAsync(loginModel.UsernameOrEmailAddress, loginModel.Password, await GetTenancyNameOrNull(loginModel.UsernameOrEmailAddress));//use new GetTenancyNameOrNull method that you add previously
      if (loginResult?.Tenant?.Id != AbpSession.TenantId)
      {
          SetTenantIdCookie(loginResult?.Tenant?.Id);
          CurrentUnitOfWork.SetTenantId(loginResult?.Tenant?.Id);
      }
      
      if (!string.IsNullOrEmpty(ss) && ss.Equals("true", StringComparison.OrdinalIgnoreCase) && loginResult.Result == AbpLoginResultType.Success)
      {
          loginResult.User.SetSignInToken();
          returnUrl = AddSingleSignInParametersToReturnUrl(returnUrl, loginResult.User.SignInToken, loginResult.User.Id, loginResult.User.TenantId);
      }
      
      if (_settingManager.GetSettingValue<bool>(AppSettings.UserManagement.AllowOneConcurrentLoginPerUser))
      {
          await _userManager.UpdateSecurityStampAsync(loginResult.User);
      }
      
      if (loginResult.User.ShouldChangePasswordOnNextLogin)
      {
          loginResult.User.SetNewPasswordResetCode();
          return Json(new AjaxResponse
          {
              TargetUrl = Url.Action(
                  "ResetPassword",
                  new ResetPasswordViewModel
                  {
                      TenantId = AbpSession.TenantId,
                      UserId = loginResult.User.Id,
                      ResetCode = loginResult.User.PasswordResetCode,
                      ReturnUrl = returnUrl,
                      SingleSignIn = ss
                  })
          });
      }
      
      var signInResult = await _signInManager.SignInOrTwoFactorAsync(loginResult, loginModel.RememberMe);
      if (signInResult.RequiresTwoFactor)
      {
          return Json(new AjaxResponse
          {
              TargetUrl = Url.Action(
                  "SendSecurityCode",
                  new
                  {
                      returnUrl = returnUrl,
                      rememberMe = loginModel.RememberMe
                  })
          });
      }
      
      Debug.Assert(signInResult.Succeeded);
      await UnitOfWorkManager.Current.SaveChangesAsync();
      
      return Json(new AjaxResponse { TargetUrl = returnUrl });
  }
  • Replace the function named ExternalLoginCallback(string returnUrl, string remoteError = null, string ss = "") with the following content
  [UnitOfWork]
  public virtual async Task<ActionResult> ExternalLoginCallback(string returnUrl, string remoteError = null, string ss = "")
  {
      returnUrl = NormalizeReturnUrl(returnUrl);
      if (remoteError != null)
      {
          Logger.Error("Remote Error in ExternalLoginCallback: " + remoteError);
          throw new UserFriendlyException(L("CouldNotCompleteLoginOperation"));
      }
      var externalLoginInfo = await _signInManager.GetExternalLoginInfoAsync();
      if (externalLoginInfo == null)
      {
          Logger.Warn("Could not get information from external login.");
          return RedirectToAction(nameof(Login));
      }
      var loginResult = await _logInManager.LoginAsync(externalLoginInfo);//use new login method that you add previously
      switch (loginResult.Result)
      {
          case AbpLoginResultType.Success:
          {
              await _signInManager.SignInAsync(loginResult.Identity, false);
              if (!string.IsNullOrEmpty(ss) && ss.Equals("true", StringComparison.OrdinalIgnoreCase) && loginResult.Result == AbpLoginResultType.Success)
              {
                  loginResult.User.SetSignInToken();
                  returnUrl = AddSingleSignInParametersToReturnUrl(returnUrl, loginResult.User.SignInToken, loginResult.User.Id, loginResult.User.TenantId);
              }
              return Redirect(returnUrl);
          }
          case AbpLoginResultType.UnknownExternalLogin:
              return await RegisterForExternalLogin(externalLoginInfo);
          default:
              throw _abpLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt(
                  loginResult.Result,
                  externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? externalLoginInfo.ProviderKey,
                  loginResult.Tenant?.Name
              );
      }
  }

Then your users will be able to login without specifying a tenant.

More

For a more stable UI, you can remove the tenant selection model used for login operations.

Go to aspnet-core\src}[YOURAPPNAME].Web.Mvc\Views\Account\Login.cshtml and add following code part

@{
    ViewBag.DisableTenantChange = true;
}

image.png

image.png