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.
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.
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;
}
}
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;
}
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 });
}
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.
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;
}