Hide Tenant Switch from an ABP Framework Login page
Introduction
In this step-by-step guide I will explain how you can hide the tenant switch on the login page of an ABP Framework application. After implementing all the steps below a user should be able to login with email address and password without losing multitenancy.
Source Code
The sample application has been developed with Blazor as UI framework and SQL Server as database provider.
The source code of the completed application is available on GitHub.
Requirements
The following tools are needed to run the solution.
- .NET 8.0 SDK
- VsCode, Visual Studio 2022 or another compatible IDE
- ABP CLI 8.0.0 or higher
Development
Create a new ABP Framework Application
- Install or update the ABP CLI:
dotnet tool install -g Volo.Abp.Cli || dotnet tool update -g Volo.Abp.Cli
- Use the following ABP CLI command to create a new Blazor ABP application:
abp new AbpHideTenantSwitch -u blazor -o AbpHideTenantSwitch
Open & Run the Application
- Open the solution in Visual Studio (or your favorite IDE).
- Run the
AbpHideTenantSwitch.DbMigrator
application to apply the migrations and seed the initial data. - Run the
AbpHideTenantSwitch.HttpApi.Host
application to start the server side. - Run the
AbpHideTenantSwitch.Blazor
application to start the Blazor UI project.
Open HttpApi.Host.csproj and comment out the line below
<!-- <PackageReference Include="Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite" Version="3.0.*-*" /> -->
Add Volo.BasicTheme module to the project
- Open a command prompt in the root of the project and run the command abp add-module
abp add-module Volo.BasicTheme --with-source-code --add-to-solution-file
Build Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic project
Open a command prompt in the Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic project and run command below:
dotnet build
Add reference to Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic
Open a command prompt in the HttpApi.Host project and add a reference to Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic in the HttpApi.Host.csproj file by running command below
dotnet add reference ../../modules/Volo.BasicTheme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.csproj
Replace AbpAspNetCoreMvcUiLeptonXLiteThemeModule
Replace typeof(AbpAspNetCoreMvcUiLeptonXLiteThemeModule), with typeof(AbpAspNetCoreMvcUiBasicThemeModule) in the DependsOn section of the HttpApiHostModule.cs file in the HttpApi.Host project
Hide Tenant Switch in Account.cshtml file
Goto the Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic\Themes\Basic\Themes\Basic\Layouts\Account.cshtml file in the BasicTheme module
Comment out if statement below to hide Tenant Switch.
@* @if (MultiTenancyOptions.Value.IsEnabled &&
(TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(CookieTenantResolveContributor.ContributorName) == true ||
TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(QueryStringTenantResolveContributor.ContributorName) == true))
{
<div class="card shadow-sm rounded mb-3">
<div class="card-body px-5">
<div class="row">
<div class="col">
<span style="font-size: .8em;" class="text-uppercase text-muted">@MultiTenancyStringLocalizer["Tenant"]</span><br />
<h6 class="m-0 d-inline-block">
@if (CurrentTenant.Id == null)
{
<span>
@MultiTenancyStringLocalizer["NotSelected"]
</span>
}
else
{
<strong>@(CurrentTenant.Name ?? CurrentTenant.Id.Value.ToString())</strong>
}
</h6>
</div>
<div class="col-auto">
<a id="AbpTenantSwitchLink" href="javascript:;" class="btn btn-sm mt-3 btn-outline-primary">@MultiTenancyStringLocalizer["Switch"]</a>
</div>
</div>
</div>
</div>
} *@
Add ConfigureTenantResolver() method in HttpApiHostModule of HttpApi.Host project
Add method ConfigureTenantResolver right under the ConfigureServices method in the HttpApiHostModule class of the HttpApi.Host project
// import using statements
// using Volo.Abp.MultiTenancy;
private void ConfigureTenantResolver(ServiceConfigurationContext context, IConfiguration configuration)
{
Configure<AbpTenantResolveOptions>(options =>
{
options.TenantResolvers.Clear();
options.TenantResolvers.Add(new CurrentUserTenantResolveContributor());
});
}
Call ConfigureTenantResolver() method from ConfigureServices() method
public override void ConfigureServices(ServiceConfigurationContext context)
{
// other code here ...
ConfigureTenantResolver(context, configuration);
}
Add a Pages/Account folder to HttpApi.Host project
Add a CustomLoginModel.cs class to the Account folder
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Volo.Abp.Account.Web;
using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp.Identity;
using Volo.Abp.TenantManagement;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
namespace AbpHideTenantSwitch.HttpApi.Host.Pages.Account
{
public class CustomLoginModel : LoginModel
{
private readonly ITenantRepository _tenantRepository;
public CustomLoginModel(IAuthenticationSchemeProvider schemeProvider, IOptions<AbpAccountOptions> accountOptions, IOptions<IdentityOptions> identityOptions, ITenantRepository tenantRepository, IdentityDynamicClaimsPrincipalContributorCache contributorCache)
: base(schemeProvider, accountOptions, identityOptions, contributorCache)
{
_tenantRepository = tenantRepository;
}
public override async Task<IActionResult> OnPostAsync(string action)
{
var user = await FindUserAsync(LoginInput.UserNameOrEmailAddress);
using (CurrentTenant.Change(user?.TenantId))
{
return await base.OnPostAsync(action);
}
}
protected virtual async Task<IdentityUser> FindUserAsync(string uniqueUserNameOrEmailAddress)
{
IdentityUser user = null;
using (CurrentTenant.Change(null))
{
user = await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ??
await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress);
if (user != null)
{
return user;
}
}
foreach (var tenant in await _tenantRepository.GetListAsync())
{
using (CurrentTenant.Change(tenant.Id))
{
user = await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ??
await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress);
if (user != null)
{
return user;
}
}
}
return null;
}
}
}
Add a Login.cshtml file to the Account folder
@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling
@using Microsoft.AspNetCore.Mvc.Localization
@using Volo.Abp.Account.Localization
@using Volo.Abp.Account.Settings
@using Volo.Abp.Settings
@model AbpHideTenantSwitch.HttpApi.Host.Pages.Account.CustomLoginModel
@inject IHtmlLocalizer<AccountResource> L
@inject Volo.Abp.Settings.ISettingProvider SettingProvider
<div class="card text-center mt-3 shadow-sm rounded">
<div class="card-body abp-background p-5">
<div class="form-group">
<img
src="https://raw.githubusercontent.com/bartvanhoey/AbpHideTenantSwitch/main/src/AbpHideTenantSwitch.HttpApi.Host/wwwroot/images/thumbs-up.png?raw=true"
alt="ThumbsUp"
width="100%"
/>
</div>
@if (Model.EnableLocalLogin) {
<form method="post" class="mt-4 text-left">
<input asp-for="ReturnUrl" />
<input asp-for="ReturnUrlHash" />
<div class="form-group">
<label>Email address</label>
<input
asp-for="LoginInput.UserNameOrEmailAddress"
class="form-control"
/>
<span
asp-validation-for="LoginInput.UserNameOrEmailAddress"
class="text-danger"
></span>
</div>
<div class="form-group">
<label asp-for="LoginInput.Password"></label>
<input asp-for="LoginInput.Password" class="form-control" />
<span
asp-validation-for="LoginInput.Password"
class="text-danger"
></span>
</div>
<abp-button
type="submit"
button-type="Danger"
name="Action"
value="Login"
class="btn-block btn-lg mt-3"
>@L["Login"]</abp-button
>
@if (Model.ShowCancelButton) {
<abp-button
type="submit"
button-type="Secondary"
formnovalidate="formnovalidate"
name="Action"
value="Cancel"
class="btn-block btn-lg mt-3"
>@L["Cancel"]</abp-button
>
}
</form>
}
</div>
</div>
>
Custom styles Login page
Add a custom-login-styles.css file to the wwwroot folder of the HttpApi.Host project
.abp-background { background-color: yellow !important; }
Add custom styles to Bundle
Open file AbpHideTenantSwitchHttpApiHostModule.cs of the HttpApi.Host project and add custom-login-styles.css to bundle.
private void ConfigureBundles()
{
Configure<AbpBundlingOptions>(options =>
{
options.StyleBundles.Configure(
BasicThemeBundles.Styles.Global,
bundle =>
{
bundle.AddFiles("/global-styles.css");
bundle.AddFiles("/custom-login-styles.css");
}
);
});
}
Add an Image
Add an assets/images folder to the wwwroot folder of the HttpApi.Host project and copy/paste an image into images folder.
Start both the Blazor and the HttpApi.Host project to run the application
Et voilà! The Login page without a Tenant switch!
A user can now login with email address and username without specifying the tenant name.
Get the source code on GitHub.
Enjoy and have fun!
Comments
Halil İbrahim Kalkan 162 weeks ago
Thanks for the article submission.
CustomLoginModel.cs loops though the tenants. Could we disable the multi-tenancy filter, then query once. I know that doesn't work for "database per tenant" approach, but can be a good option if we use a single/shared database for all tenants.
Halil İbrahim Kalkan 162 weeks ago
You can use
abp add-module Volo.BasicTheme --with-source-code --add-to-solution-file
command instead of manually downloading and replacing the Basic Theme. However, it can be possible to override all these without requiring to have the source code. See https://community.abp.io/articles/how-to-customize-the-login-page-for-mvc-razor-page-applications-9a40f3cdDon Vliet 162 weeks ago
What would happen if the same user exists in more than one tenant using the same username and password for all tenants?
Also, if we have the following: Tenant A - Username: Darren & Password: ABC --- Tenant B - Username: Darren & Password: DEF --- Tenant C - Username: Darren & Password: GHI ---
If the user enters Username/Password as Darren / GHI - would the code find the first instance of Darren in Tenant A, and then try and login as Darren / GHI in Tenant A ?
bushbert 160 weeks ago
Blazor needs support for the Domainresolver really badly.
Nicolas Nicolas 161 weeks ago
There is my feedback on using this solution with our mvc project.
When all the tenant resolver are cleared and only CurrentUserTenantResolveContributor is left the tenant switch is automatically hided. So no need to override the login page.
The change of current tenant ID set not the current tenant name. This resulting to have securitylogs without tenantname.
Beside this two things this solution works like a charm. Thanks for the sharing.
Adam Gilmore 130 weeks ago
This method works for Blazor 5.2.0 Commercial version.
First remove the "select tenant" option from the login form. In the <appname>HttpAPIHostModule class in the HttpApi.Host project add the following to the ConfigureServices method.
Create a Pages then Account folder in the HttpApi.Host project. Add a class called CustomLoginModel.cs
Use the following code:
using IdentityServer4.Services; using IdentityServer4.Stores; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Volo.Abp.Account.Web.Pages.Account; using Volo.Abp.DependencyInjection; using IdentityUser = Volo.Abp.Identity.IdentityUser; using Volo.Abp.Account.Public.Web.Pages.Account; using Volo.Abp.Account.Public.Web; using Volo.Abp.Account.ExternalProviders; using Volo.Abp.Account.Security.Recaptcha; using Volo.Abp.Security.Claims; using Owl.reCAPTCHA; using Volo.Abp.Data; using Volo.Abp.MultiTenancy;
namespace *.App.Pages.Account { [ExposeServices(typeof(LoginModel))] public class CustomLoginModel : IdentityServerSupportedLoginModel { public IDataFilter DataFilter { get; set; }
}
I only allow login by email. DataFilter.Disable<IMultiTenant> disables MultiTenancy. The FindByEmailASync method queries the whole User table, not just the records for the current tenant. The DataFilter is injected using Property (rather than constructor) injection.
Grant Bremner 128 weeks ago
Adam - this is great - no need to rewrite all the UI! Works with 5.3 Commercial EF Blazor WASM
NickB 127 weeks ago
I'm new to ABP, I love the ideas and concepts. However just a philosophical question. Rather than injecting the ITenantRepository, should you be injecting the ITenantAppService. By using the repository directly are you breaking some dependency rule (I dont know just trying to understand best practice) Thanks Nick
directdeals2021@gmail.com 123 weeks ago
Thanks for spreading a fruitful awareness about the Microsoft product in such a good way.
David Brenchley 106 weeks ago
How do we do this on Blazer WASM Framework?
David Brenchley 106 weeks ago
How do we do this on Blazer WASM Framework?
David Brenchley 97 weeks ago
How can we do this for the LeptonX-Lite theme? They aren't releasing any of the source code for that.