Activities of "Hon-Tre_IFS"

I have fallowed as per above now it got updated

And Now I am not able to log in the same tenant user in different browser is the expected behavior right? and the login screen falls back

Hi Maliming

  1. Abp Version 10.0.2

  2. I have Logout all the past session using this UI

  3. Still it is same not past session count are getting deleted. rows are not getting updated in abp session table

  4. Does it work again if you temporarily remove CustomLoginModel? Did you change anything else in the Account module? Ans I did not change any functional apart from validation and UI

The IsDynamicClaimsEnabled configuration and the middleware order (UseAuthentication, UseDynamicClaims, UseAuthorization) in your AuthServer and HttpApi.Host modules. ANS: all this configuration are placed

We have overridden the Accounts module after that** abp concurrent session Management is not working **

Adding login code for your reference

using Hon.IFS.SiteHost.Pages.Account; using Hon.IFS.SiteManagement.Sites; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using Owl.reCAPTCHA; using Microsoft.Extensions.Logging; using System; using System.Linq; using System.Threading.Tasks; using Volo.Abp.Account.ExternalProviders; using Volo.Abp.Account; using Volo.Abp.Account.Public.Web; using Volo.Abp.Account.Public.Web.Pages.Account; using Volo.Abp.Account.Security.Recaptcha; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; using Volo.Abp.Identity.Settings; using Volo.Abp.Security.Claims; using Volo.Abp.Settings; using Volo.Saas.Tenants;

namespace Hon.IFS.SiteHost.Pages.Account { [ExposeServices(typeof(CustomLoginModel), typeof(LoginModel))] public class CustomLoginModel : LoginModel { private readonly ITenantRepository _tenantRepository; private readonly SiteHostBrandingProvider _brandingProvider; private readonly ISiteRepository _siteRepository; private readonly IConfiguration _configuration; private readonly ISettingProvider _settingProvider; private readonly IAccountAppService _accountAppService;

    public string? TenantHostName { get; set; }

    [BindProperty]
    public new LoginInput LoginInput { get; set; } = new();

    public CustomLoginModel(
        IAuthenticationSchemeProvider schemeProvider,
        IOptions<AbpAccountOptions> accountOptions,
        IAccountExternalProviderAppService accountExternalProviderAppService,
        ICurrentPrincipalAccessor currentPrincipalAccessor,
        IAbpRecaptchaValidatorFactory recaptchaValidatorFactory,
        IOptions<IdentityOptions> identityOptions,
        IOptionsSnapshot<reCAPTCHAOptions> reCaptchaOptions,
        ITenantRepository tenantRepository,
        SiteHostBrandingProvider brandingProvider,
        ISiteRepository siteRepository,
        IConfiguration configuration,
        ISettingProvider settingProvider,
        IAccountAppService accountAppService)
        : base(schemeProvider, accountOptions, recaptchaValidatorFactory,
               accountExternalProviderAppService, currentPrincipalAccessor,
               identityOptions, reCaptchaOptions)
    {
        _tenantRepository = tenantRepository;
        _brandingProvider = brandingProvider;
        _siteRepository = siteRepository;
        _configuration = configuration;
        _settingProvider = settingProvider;
        _accountAppService = accountAppService;
    }

    public override async Task<IActionResult> OnGetAsync()
    {
        ParseTenantHostName();

        // If tenantName is not in the query string but _tenantHost exists in the ReturnUrl
        // (passed through the OIDC authorize URL), redirect with tenantName so the
        // branding provider and tenant resolution work correctly.
        if (string.IsNullOrWhiteSpace(TenantHostName) && !string.IsNullOrWhiteSpace(ReturnUrl))
        {
            var tenantFromReturnUrl = ExtractTenantFromReturnUrl(ReturnUrl);
            if (!string.IsNullOrWhiteSpace(tenantFromReturnUrl))
            {
                // Strip _tenantHost from ReturnUrl — it was only needed to resolve the tenant.
                // Keeping it would leak it into the final URL the user sees after login.
                var cleanReturnUrl = StripTenantHostFromUrl(ReturnUrl);
                var loginPath = (Request.PathBase + Request.Path).ToUriComponent();
                var redirectUrl = QueryHelpers.AddQueryString(loginPath, "tenantName", tenantFromReturnUrl);
                redirectUrl = QueryHelpers.AddQueryString(redirectUrl, "ReturnUrl", cleanReturnUrl);
                if (!string.IsNullOrWhiteSpace(ReturnUrlHash))
                {
                    redirectUrl = QueryHelpers.AddQueryString(redirectUrl, "ReturnUrlHash", ReturnUrlHash);
                }
                return Redirect(redirectUrl);
            }
        }

        return await base.OnGetAsync();
    }

    public override async Task<IActionResult> OnPostAsync(string action)
    {
        ParseTenantHostName();

        try
        {
            var tenant = await ResolveTenantAsync();
            if (tenant == null && !string.IsNullOrEmpty(_brandingProvider.TenantAppName))
            {
                return AddErrorAndReturnPage("LoginInput.UserNameOrEmailAddress", "Invalid tenant configuration.");
            }

            if (!ValidateCredentials(out var username, out var password))
            {
                return Page();
            }

            return await ProcessLoginAsync(tenant, username, password, action);
        }
        catch (Exception ex)
        {
            Logger.LogWarning(ex, "An error occurred during login.");
            Alerts.Danger("An error occurred during login. Please try again.");
            return Page();
        }
    }

    private void ParseTenantHostName()
    {
        var tenantName = Request.Query["tenantName"].ToString();
        TenantHostName = string.IsNullOrWhiteSpace(tenantName) ? null : Uri.UnescapeDataString(tenantName);
    }

    /// <summary>
    /// Extracts the _tenantHost parameter from the OIDC authorize URL embedded in ReturnUrl.
    /// This is used when the Angular client passes _tenantHost through the OIDC flow.
    /// </summary>
    private string? ExtractTenantFromReturnUrl(string returnUrl)
    {
        try
        {
            var queryIndex = returnUrl.IndexOf('?');
            if (queryIndex < 0) return null;

            var queryString = returnUrl[(queryIndex + 1)..];
            var queryParams = QueryHelpers.ParseQuery(queryString);

            if (queryParams.TryGetValue("_tenantHost", out var tenantValues) && tenantValues.Count > 0)
            {
                var tenant = tenantValues.First();
                return string.IsNullOrWhiteSpace(tenant) ? null : tenant;
            }
        }
        catch (Exception ex)
        {
            Logger.LogWarning(ex, "Failed to extract tenant from ReturnUrl: {ReturnUrl}", returnUrl);
        }

        return null;
    }

    /// <summary>
    /// Removes the _tenantHost query parameter from a URL after it has been used
    /// for tenant resolution, so it doesn't leak into the final redirect URL.
    /// </summary>
    private string StripTenantHostFromUrl(string url)
    {
        if (string.IsNullOrWhiteSpace(url))
        {
            return url;
        }

        try
        {
            var queryIndex = url.IndexOf('?');
            if (queryIndex < 0)
            {
                return url;
            }

            var path = url[..queryIndex];
            var queryParams = QueryHelpers.ParseQuery(url[(queryIndex + 1)..]);

            if (!queryParams.Remove("_tenantHost"))
            {
                return url;
            }

            if (queryParams.Count == 0)
            {
                return path;
            }

            var result = path;
            foreach (var param in queryParams)
            {
                foreach (var value in param.Value)
                {
                    result = QueryHelpers.AddQueryString(result, param.Key, value ?? string.Empty);
                }
            }

            return result;
        }
        catch (Exception ex)
        {
            Logger.LogWarning(ex, "Failed to strip _tenantHost from URL: {Url}", url);
            return url;
        }
    }

    private async Task<Tenant?> ResolveTenantAsync()
    {
        var tenantKey = _brandingProvider.TenantAppName;
        return string.IsNullOrEmpty(tenantKey) ? null : await _tenantRepository.FindByNameAsync(tenantKey);
    }

    private bool ValidateCredentials(out string username, out string password)
    {
        username = LoginInput?.UserNameOrEmailAddress?.Trim() ?? string.Empty;
        password = LoginInput?.Password?.Trim() ?? string.Empty;

        var isValid = true;

        if (string.IsNullOrEmpty(username))
        {
            ModelState.AddModelError("LoginInput.UserNameOrEmailAddress", "Username is required.");
            isValid = false;
        }

        if (string.IsNullOrEmpty(password))
        {
            ModelState.AddModelError("LoginInput.Password", "Password is required.");
            isValid = false;
        }

        return isValid;
    }

    private async Task<IActionResult> ProcessLoginAsync(Tenant? tenant, string username, string password, string action)
    {
        using (CurrentTenant.Change(tenant?.Id, tenant?.Name))
        {
            var user = await FindUserAsync(username);
            if (user == null)
            {
                return AddErrorAndReturnPage("LoginInput.UserNameOrEmailAddress", "Username is incorrect.");
            }

            await IdentityOptions.SetAsync();

            if (await UserManager.IsLockedOutAsync(user))
            {
                return AddErrorAndReturnPage("LoginInput.Password", L["UserLockedOutMessage"]);
            }

            if (!await UserManager.CheckPasswordAsync(user, password))
            {
                if (await UserManager.GetLockoutEnabledAsync(user))
                {
                    await UserManager.AccessFailedAsync(user);

                    if (await UserManager.IsLockedOutAsync(user))
                    {
                        return AddErrorAndReturnPage("LoginInput.Password", L["UserLockedOutMessage"]);
                    }
                }

                return AddErrorAndReturnPage("LoginInput.Password", "Invalid password.");
            }

            if (await _settingProvider.GetAsync<bool>(IdentitySettingNames.SignIn.RequireConfirmedEmail) && !user.EmailConfirmed)
            {
                try
                {
                    await _accountAppService.SendEmailConfirmationTokenAsync(new SendEmailConfirmationTokenDto
                    {
                        AppName = "MVC",
                        UserId = user.Id
                    });
                }
                catch (Exception ex)
                {
                    Logger.LogError(ex, "Failed to send email confirmation for user {UserId}.", user.Id);
                    return AddErrorAndReturnPage("LoginInput.UserNameOrEmailAddress", "We couldn't send the verification email. Please contact your administrator.");
                }

                return AddErrorAndReturnPage("LoginInput.UserNameOrEmailAddress", "Your email is not verified. A verification email has been sent.");
            }

            SyncBaseLoginInput();

            // Strip _tenantHost from ReturnUrl before the OIDC flow — it was only
            // needed for tenant resolution and shouldn't appear in the final URL.
            if (!string.IsNullOrWhiteSpace(ReturnUrl))
            {
                ReturnUrl = StripTenantHostFromUrl(ReturnUrl);
            }

            var result = await base.OnPostAsync(action);
            if (IsLoginFailed(result))
            {
                return AddErrorAndReturnPage("LoginInput.UserNameOrEmailAddress", "The username or password is incorrect.");
            }

            user = await UserManager.FindByIdAsync(user.Id.ToString());
            if (user == null)
            {
                Alerts.Danger("User not found after login. Please contact support.");
                return Page();
            }

            if (user.ShouldChangePasswordOnNextLogin)
            {
                return await RedirectToChangePasswordAsync(user);
            }

            if (!string.IsNullOrWhiteSpace(ReturnUrl))
            {
                return result;
            }

            return await RedirectAfterLoginAsync(tenant);
        }
    }

    private void SyncBaseLoginInput()
    {
        var baseProperty = typeof(LoginModel).GetProperty("LoginInput");
        var baseLoginInput = baseProperty?.GetValue(this);

        if (baseLoginInput == null)
        {
            var loginInputType = baseProperty?.PropertyType;
            if (loginInputType != null)
            {
                baseLoginInput = Activator.CreateInstance(loginInputType);
                baseProperty?.SetValue(this, baseLoginInput);
            }
        }

        if (baseLoginInput != null)
        {
            var baseType = baseLoginInput.GetType();
            baseType.GetProperty("UserNameOrEmailAddress")?.SetValue(baseLoginInput, LoginInput.UserNameOrEmailAddress);
            baseType.GetProperty("Password")?.SetValue(baseLoginInput, LoginInput.Password);
            baseType.GetProperty("RememberMe")?.SetValue(baseLoginInput, LoginInput.RememberMe);
        }
    }

    private static bool IsLoginFailed(IActionResult? result)
        => result == null || (result is RedirectToPageResult redirect && redirect.PageName == "./Login");

    private IActionResult AddErrorAndReturnPage(string key, string message)
    {
        ModelState.AddModelError(key, message);
        return Page();
    }

    private async Task<IActionResult> RedirectToChangePasswordAsync(Volo.Abp.Identity.IdentityUser user)
    {
        var resetToken = await UserManager.GeneratePasswordResetTokenAsync(user);
        return RedirectToPage("./ChangePassword", new
        {
            userId = user.Id,
            resetToken,
            tenantId = CurrentTenant.Id
        });
    }

    private async Task<IActionResult> RedirectAfterLoginAsync(Tenant? tenant)
    {
        var angularUrl = _configuration["App:AngularUrl"]
            ?? throw new InvalidOperationException("App:AngularUrl is not configured.");

        var baseUrl = angularUrl.TrimEnd('/');

        if (tenant?.Id == null)
        {
            return Redirect($"{baseUrl}/dashboard");
        }

        var sites = await _siteRepository.GetListAsync();
        return Redirect(sites.Any() ? $"{baseUrl}/dashboard" : $"{baseUrl}/onboarding");
    }

    protected virtual async Task<Volo.Abp.Identity.IdentityUser?> FindUserAsync(string usernameOrEmail)
    {
        return await UserManager.FindByNameAsync(usernameOrEmail)
               ?? await UserManager.FindByEmailAsync(usernameOrEmail);
    }
}

}

Are you talking about the auto login once the session is expired

During automatic logout, the web app is redirecting to default department host login screen instead of tenant login screen.

This is not allowing user to navigate to the tenant portal:

** For Reference Loging Paged Overridden code **

using Hon.IFS.DepartmentHost.Pages.Account; using Hon.IFS.DeptManagement.FireDepartments; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using Owl.reCAPTCHA; using Microsoft.Extensions.Logging; using System; using System.Linq; using System.Threading.Tasks; using Volo.Abp.Account.ExternalProviders; using Volo.Abp.Account.Public.Web; using Volo.Abp.Account.Public.Web.Pages.Account; using Volo.Abp.Account.Security.Recaptcha; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; using Volo.Abp.Security.Claims; using Volo.Saas.Tenants;

namespace Hon.IFS.DepartmentHost.Pages.Account { [ExposeServices(typeof(CustomLoginModel), typeof(LoginModel))] public class CustomLoginModel : LoginModel { private readonly ITenantRepository _tenantRepository; private readonly DepartmentHostBrandingProvider _brandingProvider; private readonly IFireDepartmentRepository _fireDepartmentRepository; private readonly IConfiguration _configuration;

    public string? TenantHostName { get; set; }

    [BindProperty]
    public new LoginInput LoginInput { get; set; } = new();

    public CustomLoginModel(
        IAuthenticationSchemeProvider schemeProvider,
        IOptions<AbpAccountOptions> accountOptions,
        IAccountExternalProviderAppService accountExternalProviderAppService,
        ICurrentPrincipalAccessor currentPrincipalAccessor,
        IAbpRecaptchaValidatorFactory recaptchaValidatorFactory,
        IOptions<IdentityOptions> identityOptions,
        IOptionsSnapshot<reCAPTCHAOptions> reCaptchaOptions,
        ITenantRepository tenantRepository,
        DepartmentHostBrandingProvider brandingProvider,
        IFireDepartmentRepository fireDepartmentRepository,
        IConfiguration configuration)
        : base(schemeProvider, accountOptions, recaptchaValidatorFactory,
               accountExternalProviderAppService, currentPrincipalAccessor,
               identityOptions, reCaptchaOptions)
    {
        _tenantRepository = tenantRepository;
        _brandingProvider = brandingProvider;
        _fireDepartmentRepository = fireDepartmentRepository;
        _configuration = configuration;
    }

    public override async Task<IActionResult> OnGetAsync()
    {
        ParseTenantHostName();

        // If tenantName is not in the query string but __tenant exists in the ReturnUrl
        // (passed through the OIDC authorize URL), redirect with tenantName so the
        // branding provider and tenant resolution work correctly.
        if (string.IsNullOrWhiteSpace(TenantHostName) && !string.IsNullOrWhiteSpace(ReturnUrl))
        {
            var tenantFromReturnUrl = ExtractTenantFromReturnUrl(ReturnUrl);
            if (!string.IsNullOrWhiteSpace(tenantFromReturnUrl))
            {
                // Strip _tenantHost from ReturnUrl — it was only needed to resolve the tenant.
                // Keeping it would leak it into the final URL the user sees after login.
                var cleanReturnUrl = StripTenantHostFromUrl(ReturnUrl);
                var redirectUrl = $"{Request.Path}?tenantName={Uri.EscapeDataString(tenantFromReturnUrl)}&ReturnUrl={Uri.EscapeDataString(cleanReturnUrl)}";
                if (!string.IsNullOrWhiteSpace(ReturnUrlHash))
                {
                    redirectUrl += $"&ReturnUrlHash={Uri.EscapeDataString(ReturnUrlHash)}";
                }
                return Redirect(redirectUrl);
            }
        }

        return await base.OnGetAsync();
    }

    public override async Task<IActionResult> OnPostAsync(string action)
    {
        ParseTenantHostName();

        try
        {
            var tenant = await ResolveTenantAsync();
            if (tenant == null && !string.IsNullOrEmpty(_brandingProvider.TenantAppName))
            {
                return AddErrorAndReturnPage("LoginInput.UserNameOrEmailAddress", "Invalid tenant configuration.");
            }

            if (!ValidateCredentials(out var username, out var password))
            {
                return Page();
            }

            return await ProcessLoginAsync(tenant, username, password, action);
        }
        catch (Exception ex)
        {
            Logger.LogWarning(ex, "An error occurred during login.");
            Alerts.Danger("An error occurred during login. Please try again.");
            return Page();
        }
    }

    private void ParseTenantHostName()
    {
        var tenantName = Request.Query["tenantName"].ToString();
        TenantHostName = string.IsNullOrWhiteSpace(tenantName) ? null : Uri.UnescapeDataString(tenantName);
    }

    /// <summary>
    /// Extracts the _tenantHost parameter from the OIDC authorize URL embedded in ReturnUrl.
    /// This is used when the notification deep link passes _tenantHost through the OIDC flow.
    /// </summary>
    private string? ExtractTenantFromReturnUrl(string returnUrl)
    {
        try
        {
            var queryIndex = returnUrl.IndexOf('?');
            if (queryIndex < 0) return null;

            var queryString = returnUrl[(queryIndex + 1)..];
            var queryParams = QueryHelpers.ParseQuery(queryString);

            if (queryParams.TryGetValue("_tenantHost", out var tenantValues) && tenantValues.Count > 0)
            {
                var tenant = tenantValues.First();
                return string.IsNullOrWhiteSpace(tenant) ? null : tenant;
            }
        }
        catch (Exception ex)
        {
            Logger.LogWarning(ex, "Failed to extract tenant from ReturnUrl: {ReturnUrl}", returnUrl);
        }

        return null;
    }

    /// <summary>
    /// Removes the _tenantHost query parameter from a URL after it has been used
    /// for tenant resolution, so it doesn't leak into the final redirect URL.
    /// </summary>
    private string StripTenantHostFromUrl(string url)
    {
        if (string.IsNullOrWhiteSpace(url))
        {
            return url;
        }

        try
        {
            var queryIndex = url.IndexOf('?');
            if (queryIndex < 0)
            {
                return url;
            }

            var path = url[..queryIndex];
            var queryParams = QueryHelpers.ParseQuery(url[(queryIndex + 1)..]);

            if (!queryParams.Remove("_tenantHost"))
            {
                return url;
            }

            if (queryParams.Count == 0)
            {
                return path;
            }

            var result = path;
            foreach (var param in queryParams)
            {
                foreach (var value in param.Value)
                {
                    result = QueryHelpers.AddQueryString(result, param.Key, value ?? string.Empty);
                }
            }

            return result;
        }
        catch (Exception ex)
        {
            Logger.LogWarning(ex, "Failed to strip _tenantHost from URL: {Url}", url);
            return url;
        }
    }

    private async Task<Tenant?> ResolveTenantAsync()
    {
        var tenantKey = _brandingProvider.TenantAppName;
        return string.IsNullOrEmpty(tenantKey) ? null : await _tenantRepository.FindByNameAsync(tenantKey);
    }

    private bool ValidateCredentials(out string username, out string password)
    {
        username = LoginInput?.UserNameOrEmailAddress?.Trim() ?? string.Empty;
        password = LoginInput?.Password?.Trim() ?? string.Empty;

        var isValid = true;

        if (string.IsNullOrEmpty(username))
        {
            ModelState.AddModelError("LoginInput.UserNameOrEmailAddress", "Username is required.");
            isValid = false;
        }

        if (string.IsNullOrEmpty(password))
        {
            ModelState.AddModelError("LoginInput.Password", "Password is required.");
            isValid = false;
        }

        return isValid;
    }

    private async Task<IActionResult> ProcessLoginAsync(Tenant? tenant, string username, string password, string action)
    {
        using (CurrentTenant.Change(tenant?.Id, tenant?.Name))
        {
            var user = await FindUserAsync(username);
            if (user == null)
            {
                return AddErrorAndReturnPage("LoginInput.UserNameOrEmailAddress", "Username is incorrect.");
            }

            if (!await UserManager.CheckPasswordAsync(user, password))
            {
                return AddErrorAndReturnPage("LoginInput.Password", "Invalid password.");
            }

            SyncBaseLoginInput();

            // Strip _tenantHost from ReturnUrl before the OIDC flow — it was only
            // needed for tenant resolution and shouldn't appear in the final URL.
            if (!string.IsNullOrWhiteSpace(ReturnUrl))
            {
                ReturnUrl = StripTenantHostFromUrl(ReturnUrl);
            }

            var result = await base.OnPostAsync(action);
            if (IsLoginFailed(result))
            {
                return AddErrorAndReturnPage("LoginInput.UserNameOrEmailAddress", "The username or password is incorrect.");
            }

            user = await UserManager.FindByIdAsync(user.Id.ToString());
            if (user == null)
            {
                Alerts.Danger("User not found after login. Please contact support.");
                return Page();
            }

            if (user.ShouldChangePasswordOnNextLogin)
            {
                return await RedirectToChangePasswordAsync(user);
            }

            // If ReturnUrl is set (e.g., from OIDC/OAuth flow), respect it
            // so the user is redirected back to the originally requested page
            if (!string.IsNullOrWhiteSpace(ReturnUrl) && result != null)
            {
                return result;
            }

            return await RedirectAfterLoginAsync(tenant);
        }
    }

    private void SyncBaseLoginInput()
    {
        var baseProperty = typeof(LoginModel).GetProperty("LoginInput");
        var baseLoginInput = baseProperty?.GetValue(this);

        if (baseLoginInput == null)
        {
            var loginInputType = baseProperty?.PropertyType;
            if (loginInputType != null)
            {
                baseLoginInput = Activator.CreateInstance(loginInputType);
                baseProperty?.SetValue(this, baseLoginInput);
            }
        }

        if (baseLoginInput != null)
        {
            var baseType = baseLoginInput.GetType();
            baseType.GetProperty("UserNameOrEmailAddress")?.SetValue(baseLoginInput, LoginInput.UserNameOrEmailAddress);
            baseType.GetProperty("Password")?.SetValue(baseLoginInput, LoginInput.Password);
            baseType.GetProperty("RememberMe")?.SetValue(baseLoginInput, LoginInput.RememberMe);
        }
    }

    private static bool IsLoginFailed(IActionResult? result)
        => result == null || (result is RedirectToPageResult redirect && redirect.PageName == "./Login");

    private IActionResult AddErrorAndReturnPage(string key, string message)
    {
        ModelState.AddModelError(key, message);
        return Page();
    }

    private async Task<IActionResult> RedirectToChangePasswordAsync(Volo.Abp.Identity.IdentityUser user)
    {
        var resetToken = await UserManager.GeneratePasswordResetTokenAsync(user);
        return RedirectToPage("./ChangePassword", new
        {
            userId = user.Id,
            resetToken,
            tenantId = CurrentTenant.Id
        });
    }

    private async Task<IActionResult> RedirectAfterLoginAsync(Tenant? tenant)
    {
        var angularUrl = _configuration["App:AngularUrl"]
            ?? throw new InvalidOperationException("App:AngularUrl is not configured.");

        var baseUrl = angularUrl.TrimEnd('/');

        if (tenant?.Id == null)
        {
            return Redirect($"{baseUrl}/dashboard");
        }

        var departments = await _fireDepartmentRepository.GetCountAsync();
        return Redirect(departments > 0 ? $"{baseUrl}/dashboard" : $"{baseUrl}/onboarding");
    }

    protected virtual async Task<Volo.Abp.Identity.IdentityUser?> FindUserAsync(string usernameOrEmail)
    {
        return await UserManager.FindByNameAsync(usernameOrEmail)
               ?? await UserManager.FindByEmailAsync(usernameOrEmail);
    }
}

}

Logout

using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Volo.Abp.Account.Public.Web.Pages.Account; using Volo.Abp.DependencyInjection; using Volo.Saas.Tenants;

namespace Hon.IFS.DepartmentHost.Pages.Account { [ExposeServices(typeof(CustomLogoutModel), typeof(LogoutModel))] public class CustomLogoutModel : LogoutModel { private readonly ITenantRepository _tenantRepository;

    public CustomLogoutModel(ITenantRepository tenantRepository)
    {
        _tenantRepository = tenantRepository;
    }

    public override async Task<IActionResult> OnGetAsync()
    {
        // Resolved before base signs out, while the cookie still carries the tenant.
        var tenantHostName = await GetTenantHostNameAsync(CurrentTenant.Id ?? CurrentUser.TenantId);

        var result = await base.OnGetAsync();

        if (!string.IsNullOrWhiteSpace(tenantHostName) &&
            result is RedirectToPageResult redirect &&
            redirect.PageName?.EndsWith("Login", StringComparison.OrdinalIgnoreCase) == true)
        {
            return RedirectToPage("/Account/Login", new { tenantName = tenantHostName });
        }

        return result;
    }

    private async Task<string?> GetTenantHostNameAsync(Guid? tenantId)
    {
        if (!tenantId.HasValue)
        {
            return null;
        }

        var tenant = await _tenantRepository.FindByIdAsync(tenantId.Value);
        return tenant?.ExtraProperties.TryGetValue("tenantHostName", out var tenantHostName) == true
            ? tenantHostName?.ToString()
            : null;
    }
}

}

Abp text-templating language Package 'Scriban' 6.3.0 is troughing severity vulnerability Current Abp version 10.0.0 While updating Scriban to 7 this package is causing error on sending Emails

I have tried to reproduce the issue in New fresh Abp Template project I did not found any issue. I have done the customizations Change password Screen I am adding Latest code of Change password model Please verify does it need any change

using Hon.IFS.SiteHost; using Hon.IFS.SiteHost.EmailNotifications; using Hon.IFS.SiteHost.Pages.Account; using Hon.IFS.SiteHost.StringEncryption; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Serilog.Core; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Volo.Abp.Account.Public.Web.Pages.Account; using Volo.Abp.Auditing; using Volo.Abp.Identity; using Volo.Abp.Security.Encryption; using Volo.Abp.Validation; using Volo.Saas.Tenants; using static Volo.Abp.Identity.Settings.IdentitySettingNames; using static Volo.Saas.Host.SaasHostPermissions; using IdentityUser = Volo.Abp.Identity.IdentityUser;

namespace Volo.Abp.Account.Web.Pages.Account;

public class ChangePasswordModel : AccountPageModel { private readonly SignInManager<IdentityUser> _signInManager; private readonly UserManager<IdentityUser> _userManager; public IIdentityUserRepository _identityUserRepository; private readonly IConfiguration _configuration; private readonly IEmailNotificationAppService? _emailService; private readonly ITenantRepository _tenantRepository; private readonly ILogger<ChangePasswordModel> _logger;

[BindProperty]
[Required(ErrorMessage = "Current password is required.")]
[DataType(DataType.Password)]
public string CurrentPassword { get; set; } = null!;

[BindProperty]
[Required(ErrorMessage = "New password is required.")]
[DataType(DataType.Password)]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
public string NewPassword { get; set; } = null!;

[Required]
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid UserId { get; set; }

[Required]
[HiddenInput]
[BindProperty(SupportsGet = true)]
public string TenantId { get; set; } = null!;

[BindProperty]
[Required(ErrorMessage = "Confirm new password is required.")]
[DataType(DataType.Password)]
[Compare("NewPassword", ErrorMessage = "Passwords do not match.")]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
public string ConfirmNewPassword { get; set; } = null!;

[HiddenInput]
[BindProperty(SupportsGet = true)]
public string ReturnUrl { get; set; } = null!;

[HiddenInput]
[BindProperty(SupportsGet = true)]
public string ReturnUrlHash { get; set; } = null!;

[BindProperty(SupportsGet = false)]
public string? TimezoneId { get; set; }

public ChangePasswordModel(
    UserManager&lt;IdentityUser&gt; userManager,
    SignInManager&lt;IdentityUser&gt; signInManager, IIdentityUserRepository identityUserRepository,
    IConfiguration configuration, ITenantRepository tenantRepository, IEmailNotificationAppService emailService, ILogger&lt;ChangePasswordModel&gt; logger)
{
    _userManager = userManager;
    _signInManager = signInManager;
    _identityUserRepository = identityUserRepository;
    _configuration = configuration;
    _tenantRepository = tenantRepository;
    _emailService = emailService;
    _logger = logger;
    
}

public IActionResult OnGet([FromQuery] Guid userId, [FromQuery] Guid tenantId)
{
    UserId = userId;

    return Page();
}

public async Task&lt;IActionResult&gt; OnPostAsync()
{
 
    using (CurrentTenant.Change(new Guid(TenantId)))
    {
        var user = await _identityUserRepository.FindAsync(UserId);
        if (user == null)
        {
            throw new AbpException("User not found.");
        }

        if (string.IsNullOrWhiteSpace(NewPassword))
        {
            ModelState.AddModelError(nameof(NewPassword), "Password cannot be empty.");
            return Page();
        }

        if (string.IsNullOrWhiteSpace(ConfirmNewPassword))
        {
            ModelState.AddModelError(nameof(ConfirmNewPassword), "Confirm password cannot be empty.");
            return Page();
        }

        if (!string.IsNullOrWhiteSpace(NewPassword) && !string.IsNullOrWhiteSpace(ConfirmNewPassword))
        {
            if (NewPassword.Trim() != ConfirmNewPassword.Trim())
            {
                ModelState.AddModelError(
                    nameof(ConfirmNewPassword),
                    L["Password and Confirm password do not match."]
                );
                return Page();
            }
            
        }

        var changePasswordResult = await _userManager.ChangePasswordAsync(user, CurrentPassword, NewPassword);

        // Only proceed if password change was successful
        if (changePasswordResult.Succeeded)
        {
            _logger.LogInformation("Password successfully changed for user {Username} (ID: {UserId})", user.UserName, user.Id);

            // Update user to not require password change on next login
            user.SetShouldChangePasswordOnNextLogin(false);
            await UserManager.UpdateAsync(user);

            // Get tenant information for email
            var tenant = await _tenantRepository.FindAsync(new Guid(TenantId));
            if (tenant == null)
            {
                _logger.LogWarning("Tenant not found for TenantId: {TenantId}", TenantId);
                return BadRequest("Tenant not found");
            }

            var tenantHostName = tenant.ExtraProperties
                                .FirstOrDefault(kvp => kvp.Key.Equals("tenantHostName", StringComparison.OrdinalIgnoreCase))
                                .Value?.ToString();

            // Send confirmation email after successful password change
            _logger.LogInformation("Preparing to send password change confirmation email to {Email} for user {Username}", user.Email, user.UserName);

            var emailModel = new EmailNotificationDto
            {
                MailTo = user.Email,
                MailSubject = EmailNotificationConstants.EmailTypes.ChangePassword,
                IsBodyHtml = true,
                Username = user.UserName,
                Name = user.Name,
                TenantName = tenant.Name,
                TimezoneId = TimezoneId  // Use timezone from client
            };

            try
            {
                if (_emailService == null)
                {
                    _logger.LogWarning("Email service is not available. Skipping email notification.");
                }
                else
                {
                    _logger.LogDebug("Calling email service to send notification...");
                    var sendTask = _emailService.EmailNotification(emailModel);

                    if (await Task.WhenAny(sendTask, Task.Delay(TimeSpan.FromSeconds(30))) == sendTask)
                    {
                        await sendTask; 
                        _logger.LogInformation("Password change email successfully sent to {Email}", user.Email);
                    }
                    else
                    {
                        _logger.LogWarning("Email sending timed out after 30 seconds for {Email}", user.Email);
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to send password change email to {Email}. Error: {ErrorMessage}", user.Email, ex.Message);
            }

            // Refresh the sign-in
            await _signInManager.RefreshSignInAsync(user);

            return RedirectToPage("/Account/Login", new
            {
                tenantName = tenantHostName,
                returnUrl = ReturnUrl,
                returnUrlHash = ReturnUrlHash
            });
        }

        // Password change failed - add errors to ModelState
        _logger.LogWarning("Password change failed for user {Username} (ID: {UserId})", user.UserName, user.Id);
        foreach (var error in changePasswordResult.Errors)
        {
            _logger.LogDebug("Password change error: {ErrorCode} - {ErrorDescription}", error.Code, error.Description);
            ModelState.AddModelError(string.Empty, error.Description);
        }
        
        return Page();
    }
}

}

Please refer the Abp Setting Table in db

Still it is asking for the 6 characters input for the Tenant Setting as well updated to 8

my Abp Version 10 , Angular

I have Updated the Host setting to 8 Characters Password Input but it still reflecting 6 Characters in model validation

Showing 1 to 10 of 64 entries
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on October 21, 2026, 11:09
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.