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);
}
}
}
2 Answer(s)
-
0
- In ABP (v8.2+ and also v10), concurrent session management (Session Management) is based on the
IdentitySessioninfrastructure. If you override/customize the login flow and it “stops working”, it’s almost always because the session record is not being created/updated (or the DB schema for sessions doesn’t exist), so ABP can’t detect/terminate other sessions.
Fix (verified, minimal steps)
- Ensure your Identity DbContext includes sessions
If your DbContext implements
IIdentityDbContext, it must have:
public class YourDbContext : AbpDbContext<YourDbContext>, IIdentityDbContext { public DbSet<IdentitySession> Sessions { get; set; } // <-- required for Session Management }Create & apply migration After adding the
SessionsDbSet, create a new EF Core migration and update the database.
Without this table, Session Management cannot function correctly.Do NOT bypass ABP’s login pipeline In your custom page you are calling
base.OnPostAsync(action)which is good. The main rule is:- Do not replace the core sign-in logic with direct
SignInManager.PasswordSignInAsync(...)unless you also reproduce ABP’s session creation/invalidation logic. - Your code performs custom password checks before calling
base.OnPostAsync(action); that is fine as long as you ultimately let ABP complete the actual sign-in (which is where session management hooks in).
- Do not replace the core sign-in logic with direct
Verify you didn’t remove/skip session-related modules Session Management is part of ABP’s Identity infrastructure (and in ABP Commercial it’s exposed via UI/settings). If you “overrode the Accounts module with source”, ensure you still have the needed Identity/Account module dependencies in your host/auth-server modules. (If you removed Identity pieces, the session feature won’t run.)
If you must fully custom sign-in If you decide to not call
base.OnPostAsync, then you must implement the equivalent ABP Session Management operations (create/updateIdentitySession, enforce the selected concurrent-login policy, terminate other sessions). Otherwise concurrent session rules will never trigger.
Sources
- https://abp.io/docs/latest/release-info/migration-guides/abp-8-2 (Session Management infrastructure:
IdentitySession+DbSet<IdentitySession>+ migration) - https://abp.io/docs/10.2/release-info/migration-guides/abp-8-2
- https://docs.abp.io/en/commercial/8.2/modules/identity/session-management
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) - In ABP (v8.2+ and also v10), concurrent session management (Session Management) is based on the
-
0
Hi,
Your code still calls
base.OnPostAsync(action), so the standard sign-in is still used. Note that this setting doesn't block the second login. It logs out the older sessions.I need more details to see where it goes wrong.
- The exact ABP version (10.0.x, 10.1.x ...).
- The steps to reproduce it with the same user and tenant in two separate browsers. After the second login, call any API from the first browser and share the HTTP status and response.
- The value of the
Prevent concurrent loginsetting, and whether you saved it on the host side or in that tenant. - That user's rows in the
AbpSessionstable before and after the second login. - The
IsDynamicClaimsEnabledconfiguration and the middleware order (UseAuthentication,UseDynamicClaims,UseAuthorization) in your AuthServer and HttpApi.Host modules. - Does it work again if you temporarily remove
CustomLoginModel? Did you change anything else in the Account module?
Please also enable Debug logs on the AuthServer and the HttpApi.Host, and share the logs that cover both logins and the request from the first browser. Remove any passwords and tokens before sending.
https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems
You can send the logs to liming.ma@volosoft.com
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)