Open Closed

Problem implementing Custom Login Model #2572


User avatar
0
jlavallet created
Please scroll to the bottom for updates!
  • ABP Framework version: v5.1.3

  • UI type: MVC

  • DB provider: EF Core

  • Tiered (MVC) or Identity Server Separated (Angular): yes

  • Exception message and stack trace: Line 29 (see attached image): 'LoginModel' does not contain a constructor that takes 3 arguments. Line 35, 47, 48, 60, and 61: Cannot resolve the symbol 'LoginInput'.

  • Others appear to be the result of subclassing the wrong base class.

  • Steps to reproduce the issue: I have been trying to override the login page to implement the ideas expressed in these posts:

    https://community.abp.io/posts/hide-the-tenant-switch-of-the-login-page-4foaup7p https://community.abp.io/articles/how-to-customize-the-login-page-for-mvc-razor-page-applications-9a40f3cd

I have been especially trying to follow the second article since it is coming from ABP. Because I am using the commercial product, it looks like I don't have access Volo.Abp. Account.Web or it is not available because that namespace has been replaced. I feel like I am probably missing a reference. Please advise.

UPDATE 2022/02/14

I was able to get my code to compile my borrowing the CustomLoginModel class from the abp-samples/SignInWithoutSpecifyingTenant project. This project is based on version 4.3.2, however, and as I noticed later, it does not have a separate IdentityServer project for authentication.

When I noticed the Identity Server difference, it dawned on me that I was probably overriding the wrong Login page!

Because we are using a multitier separated tenant model with a separate Identity Server site for authentication, overriding the Login.cshtml file in the BlueSpot.Web project has no effect. Now I’m trying to figure out now how to override the Login page of the BlueSpot.IdentityServer project instead.

Please advise!

Here is my latest code:

From the BlueSpot.Web.BlueSpotWebModule:

public override void ConfigureServices(ServiceConfigurationContext context)
{
  var hostingEnvironment = context.Services.GetHostingEnvironment();
  var configuration = context.Services.GetConfiguration();
 
  ConfigureBundles();
  ConfigurePages(configuration);
  ConfigureCache(configuration);
  ConfigureDataProtection(context, configuration, hostingEnvironment);
  ConfigureUrls(configuration);
  ConfigureAuthentication(context, configuration);
  ConfigureImpersonation(context, configuration);
  ConfigureAutoMapper();
  ConfigureVirtualFileSystem(hostingEnvironment);
  ConfigureNavigationServices(configuration);
  ConfigureSwaggerServices(context.Services);
  ConfigureMultiTenancy();
  ConfigureBackgroundJobs();
  ConfigureTenantResolver(context);
}
 
private static void ConfigureTenantResolver(ServiceConfigurationContext context)
{
  context.Services.Configure<AbpTenantResolveOptions>(options =>
  {
    options.TenantResolvers.Clear();
    options.TenantResolvers.Add(new CurrentUserTenantResolveContributor());
  });
}

And from my BlueSpot.Web.Pages.Account.CustomLoginModel.cs file:

using System.Threading.Tasks;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Owl.reCAPTCHA;
using Volo.Abp.Account.ExternalProviders;
using Volo.Abp.Account.Public.Web;
using Volo.Abp.Account.Security.Recaptcha;
using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Security.Claims;
using Volo.Saas.Tenants;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
 
namespace BlueSpot.Web.Pages.Account;
 
[ExposeServices(typeof(IdentityServerSupportedLoginModel))]
public class CustomLoginModel : IdentityServerSupportedLoginModel
{
  private readonly ITenantRepository _tenantRepository;
 
  public CustomLoginModel(
    IAuthenticationSchemeProvider schemeProvider,
    IOptions<AbpAccountOptions> accountOptions,
    IAccountExternalProviderAppService accountExternalProvider,
    IIdentityServerInteractionService interaction,
    IClientStore clientStore,
    IEventService identityServerEvents,
    ICurrentPrincipalAccessor currentPrincipalAccessor,
    IAbpRecaptchaValidatorFactory recaptchaValidatorFactory,
    IOptions<IdentityOptions> identityOptions,
    IOptionsSnapshot<reCAPTCHAOptions> reCaptchaOptions,
    ITenantRepository tenantRepository)
    : base(schemeProvider, accountOptions, accountExternalProvider, interaction, clientStore, identityServerEvents,
      currentPrincipalAccessor, recaptchaValidatorFactory, identityOptions, reCaptchaOptions)
  {
    _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);
    }
  }
 
  public override async Task<IActionResult> OnGetExternalLoginCallbackAsync(string returnUrl = "",
    string returnUrlHash = "", string remoteError = null)
  {
    var user = await FindUserAsync(LoginInput.UserNameOrEmailAddress);
    using (CurrentTenant.Change(user?.TenantId))
    {
      return await base.OnGetExternalLoginCallbackAsync(returnUrl, returnUrlHash, remoteError);
    }
  }
 
  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;
  }
}

Note that I had to modify the constructor to support the IdentityServerSupportedLoginModel base class constructor.

And here is my Login.cshtml file:

@page
@using Microsoft.AspNetCore.Mvc.Localization
@using Volo.Abp.Account.Localization
@using Volo.Abp.Account.Settings
@using Volo.Abp.Settings
@model BlueSpot.Web.Pages.Account.CustomLoginModel
@inject IHtmlLocalizer<AccountResource> L
@inject Volo.Abp.Settings.ISettingProvider SettingProvider
<div class="card mt-3 shadow-sm rounded">
    <div class="card-body p-5">
        <h4>@L["Login"]</h4>
        @if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled))
        {
            <strong>
                @L["AreYouANewUser"]
                <a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Register"]</a>
            </strong>
        }
        @if (Model.EnableLocalLogin)
        {
            <form method="post" class="mt-4">
                <div class="mb-3">
                    <label asp-for="LoginInput.UserNameOrEmailAddress" class="form-label"></label>
                    <input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control"/>
                    <span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span>
                </div>
                <div class="mb-3">
                    <label asp-for="LoginInput.Password" class="form-label"></label>
                    <input asp-for="LoginInput.Password" class="form-control"/>
                    <span asp-validation-for="LoginInput.Password" class="text-danger"></span>
                </div>
                <abp-row>
                    <abp-column>
                        <abp-input asp-for="LoginInput.RememberMe" class="mb-4"/>
                    </abp-column>
                    <abp-column class="text-end">
                        <a href="@Url.Page("./ForgotPassword", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})">@L["ForgotPassword"]</a>
                    </abp-column>
                </abp-row>
                <div class="d-grid gap-2">
                    <abp-button type="submit" button-type="Primary" name="Action" value="Login" class="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-lg mt-3">@L["Cancel"]</abp-button>
                    }
                </div>
            </form>
        }
 
        @if (Model.VisibleExternalProviders.Any())
        {
            <div class="mt-2">
                <h5>@L["OrLoginWith"]</h5>
                <form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post">
                    @foreach (var provider in Model.VisibleExternalProviders)
                    {
                        <button type="submit" class="btn btn-primary m-1" name="provider" value="@provider.AuthenticationScheme" title="@L["LogInUsingYourProviderAccount", provider.DisplayName]">@provider.DisplayName</button>
                    }
                </form>
            </div>
        }
 
        @if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any())
        {
            <div class="alert alert-warning">
                <strong>@L["InvalidLoginRequest"]</strong>
                @L["ThereAreNoLoginSchemesConfiguredForThisClient"]
            </div>
        }
 
    </div>
</div>
UPDATE 2022-02-15

This morning I checked to see if there was any response to this post. I am now going on four days without a response. If I am a paying customer, then why am I not getting a response?! I am on almost exactly the opposite side of the globe so each day I don't get a response means that I lose a full day waiting. I made the recommendation to go with ABP.IO Commercial and now I'm getting heat from my boss because the product is not being supported. Please advise!

Since last night, I was able to find the following post on your website: https://gist.github.com/ebicoglu/ce0f0425bab806d0ee1a87d0073af96b. Using that and the documentation here: https://docs.abp.io/en/abp/latest/UI/AspNetCore/Customization-User-Interface, I was able to make changes to my implementation, moving the Account folder to the Identity Server project and updating the code so that I am now able to replace the Login functionality. The Login page works as it should now!

However, as soon I try to log out, I now get the following error:

An unhandled exception occurred while processing the request.
ComponentNotRegisteredException: The requested service 'Volo.Abp.Account.Public.Web.Pages.Account.LogoutModel' has not been registered. To avoid this exception, either register a component to provide the service, check for service registration using IsRegistered(), or use the ResolveOptional() method to resolve an optional dependency.
Autofac.ResolutionExtensions.ResolveService(IComponentContext context, Service service, IEnumerable<Parameter> parameters)

I believe this problem probably has something to do with clearing the TenantResolvers in the BlueSpotIdentityServerModule. I am doing so because I want to follow the original post and use the email address of the user to resolve the tenant instead of requiring the user to select the tenant or use a subdomain/A record to do so.

According to my theory, I set a breakpoint so I could look at the default set of resolvers:

I was hoping to see some kind of Logout resolver that I could add back in. Instead I see all the multitenant resolvers and I have no idea whether to add one of these back into the mix.

Another approach I tried was to create a CustomLogoutModel and Logout.cshtml file:

using Volo.Abp.Account.Public.Web.Pages.Account;

namespace BlueSpot.Pages.Account;

public class CustomLogoutModel : LogoutModel { }

@page "/Account/Logout"
@model BlueSpot.Pages.Account.CustomLogoutModel

But that did not seem to help. Again, please advise!


5 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi Sorry for the delay. Can I check it remotely? liming.ma@volosoft.com

  • User Avatar
    0
    jlavallet created

    Li, I sent you an email regarding a time to do a remote session I am in the Central Standard Time zone (GMT -6). I can make myself available with a little bit of notice.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    You can also send me your project and I'll download and check for issues. liming.ma@volosoft.com

  • User Avatar
    0
    jlavallet created

    Let me get it back into a compiled state, and I will send it to you.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Thanks

Made with ❤️ on ABP v9.1.0-preview. Updated on November 11, 2024, 11:11