logo
Project
Version

Sign In Without Specifying Tenant

Normally, ASP.NET Zero uses tenant information during the login process. This document shows you how to implement the login process without tenant information.

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 TokenAuthController

  • Then, go to TokenAuthController. (It is located in aspnet-core\src[YOURAPPNAME].Web.Core\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 Authenticate([FromBody] AuthenticateModel model) with the following content

    [HttpPost]
    public async Task<AuthenticateResultModel> Authenticate([FromBody] AuthenticateModel model)
    {
        if (UseCaptchaOnLogin())
        {
            await ValidateReCaptcha(model.CaptchaResponse);
        }
    
        var loginResult = await GetLoginResultAsync(
            model.UserNameOrEmailAddress,
            model.Password,
            await GetTenancyNameOrNull(model.UserNameOrEmailAddress)//use new GetTenancyNameOrNull method that you add previously
        );
    
        var returnUrl = model.ReturnUrl;
        if (model.SingleSignIn.HasValue && model.SingleSignIn.Value &&
            loginResult.Result == AbpLoginResultType.Success)
        {
            loginResult.User.SetSignInToken();
            returnUrl = AddSingleSignInParametersToReturnUrl(model.ReturnUrl, loginResult.User.SignInToken,
                loginResult.User.Id, loginResult.User.TenantId);
        }
    
        //Password reset
        if (loginResult.User.ShouldChangePasswordOnNextLogin)
        {
            loginResult.User.SetNewPasswordResetCode();
            return new AuthenticateResultModel
            {
                ShouldResetPassword = true,
                PasswordResetCode = loginResult.User.PasswordResetCode,
                UserId = loginResult.User.Id,
                ReturnUrl = returnUrl
            };
        }
    
        //Two factor auth
        await _userManager.InitializeOptionsAsync(loginResult.Tenant?.Id);
    
        string twoFactorRememberClientToken = null;
        if (await IsTwoFactorAuthRequiredAsync(loginResult, model))
        {
            if (model.TwoFactorVerificationCode.IsNullOrEmpty())
            {
                //Add a cache item which will be checked in SendTwoFactorAuthCode to prevent sending unwanted two factor code to users.
                _cacheManager
                    .GetTwoFactorCodeCache()
                    .Set(
                        loginResult.User.ToUserIdentifier().ToString(),
                        new TwoFactorCodeCacheItem()
                    );
                return new AuthenticateResultModel
                {
                    RequiresTwoFactorVerification = true,
                    UserId = loginResult.User.Id,
                    TwoFactorAuthProviders = await _userManager.GetValidTwoFactorProvidersAsync(loginResult.User),
                    ReturnUrl = returnUrl
                };
            }
            twoFactorRememberClientToken = await TwoFactorAuthenticateAsync(loginResult.User, model);
        }
    
        // One Concurrent Login 
        if (AllowOneConcurrentLoginPerUser())
        {
            await _userManager.UpdateSecurityStampAsync(loginResult.User);
            await _securityStampHandler.SetSecurityStampCacheItem(loginResult.User.TenantId, loginResult.User.Id,
                loginResult.User.SecurityStamp);
            loginResult.Identity.ReplaceClaim(new Claim(AppConsts.SecurityStampKey,
                loginResult.User.SecurityStamp));
        }
    
        var refreshToken = CreateRefreshToken(await CreateJwtClaims(loginResult.Identity, loginResult.User,
            tokenType: TokenType.RefreshToken));
    
        var accessToken = CreateAccessToken(await CreateJwtClaims(loginResult.Identity, loginResult.User,
            refreshTokenKey: refreshToken.key));
    
        return new AuthenticateResultModel
        {
            AccessToken = accessToken,
            ExpireInSeconds = (int) _configuration.AccessTokenExpiration.TotalSeconds,
            RefreshToken = refreshToken.token,
            RefreshTokenExpireInSeconds = (int) _configuration.RefreshTokenExpiration.TotalSeconds,
            EncryptedAccessToken = GetEncryptedAccessToken(accessToken),
            TwoFactorRememberClientToken = twoFactorRememberClientToken,
            UserId = loginResult.User.Id,
            ReturnUrl = returnUrl
        };
    }
    
  • Replace the function named ExternalAuthenticate([FromBody] ExternalAuthenticateModel model) with the following content

    [HttpPost]
    public async Task<ExternalAuthenticateResultModel> ExternalAuthenticate([FromBody] ExternalAuthenticateModel model)
    {
        var externalUser = await GetExternalUserInfo(model);
        var loginResult = await _logInManager.LoginAsync(new UserLoginInfo(model.AuthProvider, model.ProviderKey, model.AuthProvider));
        switch (loginResult.Result)
        {
            case AbpLoginResultType.Success:
            {
                var refreshToken = CreateRefreshToken(await CreateJwtClaims(loginResult.Identity, loginResult.User,
                    tokenType: TokenType.RefreshToken));
                var accessToken = CreateAccessToken(await CreateJwtClaims(loginResult.Identity, loginResult.User,
                    refreshTokenKey: refreshToken.key));
                var returnUrl = model.ReturnUrl;
    
                if (model.SingleSignIn.HasValue && model.SingleSignIn.Value &&
                    loginResult.Result == AbpLoginResultType.Success)
                {
                    loginResult.User.SetSignInToken();
                    returnUrl = AddSingleSignInParametersToReturnUrl(model.ReturnUrl, loginResult.User.SignInToken,
                        loginResult.User.Id, loginResult.User.TenantId);
                }
    
                return new ExternalAuthenticateResultModel
                {
                    AccessToken = accessToken,
                    EncryptedAccessToken = GetEncryptedAccessToken(accessToken),
                    ExpireInSeconds = (int) _configuration.AccessTokenExpiration.TotalSeconds,
                    ReturnUrl = returnUrl,
                    RefreshToken = refreshToken.token,
                    RefreshTokenExpireInSeconds = (int) _configuration.RefreshTokenExpiration.TotalSeconds
                };
            }
            case AbpLoginResultType.UnknownExternalLogin:
            {
                var newUser = await RegisterExternalUserAsync(externalUser);
                if (!newUser.IsActive)
                {
                    return new ExternalAuthenticateResultModel
                    {
                        WaitingForActivation = true
                    };
                }
                //Try to login again with newly registered user!
                loginResult = await _logInManager.LoginAsync(
                    new UserLoginInfo(model.AuthProvider, model.ProviderKey, model.AuthProvider)
                );
    
                if (loginResult.Result != AbpLoginResultType.Success)
                {
                    throw _abpLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt(
                        loginResult.Result,
                        model.ProviderKey,
                        loginResult?.Tenant?.Name
                    );
                }
    
                var refreshToken = CreateRefreshToken(await CreateJwtClaims(loginResult.Identity,
                    loginResult.User, tokenType: TokenType.RefreshToken)
                );
    
                var accessToken = CreateAccessToken(await CreateJwtClaims(loginResult.Identity,
                    loginResult.User, refreshTokenKey: refreshToken.key));
    
                return new ExternalAuthenticateResultModel
                {
                    AccessToken = accessToken,
                    EncryptedAccessToken = GetEncryptedAccessToken(accessToken),
                    ExpireInSeconds = (int) _configuration.AccessTokenExpiration.TotalSeconds,
                    RefreshToken = refreshToken.token,
                    RefreshTokenExpireInSeconds = (int) _configuration.RefreshTokenExpiration.TotalSeconds
                };
            }
            default:
            {
                throw _abpLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt(
                    loginResult.Result,
                    model.ProviderKey,
                    loginResult?.Tenant?.Name
                );
            }
        }
    }
    

Then your project will be able to use without specifying tenant.

More

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

Go to aspnet-zero-core\angular\src\account\account.component.ts and add add login to tenantChangeDisabledRoutes

 tenantChangeDisabledRoutes: string[] = [
        'select-edition',
        'buy',
        'upgrade',
        'extend',
        'register-tenant'
     	//...
        'login'//add login
    ];
In this document