Hello,
I have developed a multi-tenant application using ABP Framework. The application is built for a single company, and the tenant system is used to separate data across the company's different organizational units.
Current Setup:
-Tenant1 → Unit1 users (e.g., User1) -Tenant2 → Unit2 users (e.g., User2) -The company has a single LDAP service. By querying this service, we can determine which unit a user belongs to.
What I Want to Achieve:
I want to integrate LDAP authentication into the login page so that each tenant's login page only allows users belonging to that specific unit to sign in.
User1 should only be able to log in to Tenant1. If User1 attempts to log in to Tenant2, the login should be rejected. User2 should only be able to log in to Tenant2. If User2 attempts to log in to Tenant1, the login should be rejected. The host (super admin) login must not be affected by this change and should continue to work without any issues.
Questions:
What is the recommended approach for tenant-based user validation with LDAP integration? At which point in the OpenIddict login flow should I intervene to restrict a user's login to the relevant tenant based on the unit information returned from LDAP? What should I be aware of to ensure the host (super admin) login remains exempt from this restriction? Thank you for your assistance.
2 Answer(s)
-
0
Recommended approach (tenant-based validation with single LDAP):
- Keep LDAP as the credential validator (username/password against LDAP).
- After LDAP says “password is valid”, query LDAP (or your LDAP-derived mapping service) to get the user’s Unit/Org.
- Compare that Unit/Org to the current tenant context (the tenant resolved from
__tenant, subdomain, etc.). - If they don’t match, reject the sign-in (do not issue cookies/tokens).
In ABP, the cleanest place to do this for the interactive OpenIddict login is to override the login page model and inject your LDAP check there, right before calling the base sign-in logic.
Where to intervene in the OpenIddict login flow:
- For the interactive login page (
/Account/Login), intervene in the Account login page post handler (i.e., in a customLoginModeloverride) before the framework completes the sign-in and OpenIddict proceeds to generate the authorization response. - If you also need to cover custom authorize behavior, ABP support confirms you can alternatively override the default OpenIddict authorize endpoint by overriding
AuthorizeControllerin your project, and you can check authentication state with:
- For the interactive login page (
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);(This is useful when you need to enforce extra rules during `/connect/authorize`.) Source: ABP support answer for overriding authorize flow. (link below)How to ensure host (super admin) login remains exempt:
- Make the restriction conditional on tenant context:
- If
CurrentTenant.Id == null(host side), skip the LDAP Unit/Org-to-tenant validation entirely. - Only enforce the Unit/Org check when
CurrentTenant.Id != null(tenant side).
- If
- This guarantees host login (including super admin) is unaffected.
- Make the restriction conditional on tenant context:
Minimal implementation outline (custom login page model):
using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Volo.Abp.Account.Web; using Volo.Abp.Account.Web.Pages.Account; using Volo.Abp.DependencyInjection; using Volo.Abp.MultiTenancy; public class LdapTenantRestrictedLoginModel : LoginModel, ITransientDependency { private readonly ICurrentTenant _currentTenant; private readonly ILdapUnitResolver _ldapUnitResolver; // your service private readonly ITenantUnitMapping _tenantUnitMapping; // your service public LdapTenantRestrictedLoginModel( IAuthenticationSchemeProvider schemeProvider, IOptions<AbpAccountOptions> accountOptions, IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> identityOptions, IdentityDynamicClaimsPrincipalContributorCache contributorCache, ICurrentTenant currentTenant, ILdapUnitResolver ldapUnitResolver, ITenantUnitMapping tenantUnitMapping) : base(schemeProvider, accountOptions, identityOptions, contributorCache) { _currentTenant = currentTenant; _ldapUnitResolver = ldapUnitResolver; _tenantUnitMapping = tenantUnitMapping; } public override async Task<IActionResult> OnPostAsync(string action) { // Host login is exempt if (_currentTenant.Id != null) { // 1) Validate credentials + resolve unit from LDAP (implementation is yours) var unit = await _ldapUnitResolver.ResolveUnitAsync( LoginInput.UserNameOrEmailAddress, LoginInput.Password ); // 2) Compare unit to current tenant var expectedUnit = await _tenantUnitMapping.GetUnitForTenantAsync(_currentTenant.Id.Value); if (unit != expectedUnit) { Alerts.Danger("You are not allowed to sign in to this tenant."); return Page(); } } // Continue with the normal ABP login flow (cookies/OpenIddict) return await base.OnPostAsync(action); } }Register it by creating a page override in your host project under
Pages/Account/Login.cshtml.cs(same razor page route) so ABP uses your model for/Account/Login.
- https://abp.io/docs/latest/modules/openiddict
- https://abp.io/support/questions/10001
- https://abp.io/community/articles/e59qfi9n
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) -
0
hi
The recommended approach is to extend the built-in
OpenLdapManagerandLdapExternalLoginProviderto add tenant validation after LDAP authentication succeeds.Step 1: Extend
OpenLdapManagerto query the user's organizational unit from LDAPFollowing the same pattern as the existing
GetUserEmailAsyncmethod:using System.Linq; using System.Threading.Tasks; using LdapForNet; using Microsoft.Extensions.Logging; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity.ExternalLoginProviders.Ldap; using Volo.Abp.Ldap; namespace YourProject; [Dependency(ReplaceServices = true)] [ExposeServices(typeof(OpenLdapManager), typeof(ILdapManager), typeof(LdapManager))] public class CustomOpenLdapManager : OpenLdapManager { public CustomOpenLdapManager(ILdapSettingProvider ldapSettingProvider) : base(ldapSettingProvider) { } public virtual async Task<string> GetUserOrganizationalUnitAsync(string userName) { using (var conn = await CreateLdapConnectionAsync()) { await AuthenticateLdapConnectionAsync( conn, await NormalizeUserNameAsync(await LdapSettingProvider.GetUserNameAsync()), await LdapSettingProvider.GetPasswordAsync()); var searchResults = await conn.SearchAsync( await GetBaseDnAsync(), await GetUserFilterAsync(userName)); try { var userEntry = searchResults.First(); return GetUserOrganizationalUnit(userEntry); } catch (LdapException e) { Logger.LogException(e); } return null; } } protected virtual string GetUserOrganizationalUnit(LdapEntry ldapEntry) { // Option 1: Read "ou" or "department" attribute directly from the LDAP entry var ou = ldapEntry.ToDirectoryEntry().GetAttribute("ou")?.GetValue<string>(); if (!ou.IsNullOrWhiteSpace()) { return ou; } // Option 2: Parse OU from the user's DN // e.g. "uid=john,ou=Unit1,dc=company,dc=com" -> "Unit1" var dn = ldapEntry.Dn; var ouPart = dn?.Split(',') .FirstOrDefault(p => p.Trim().StartsWith("ou=", System.StringComparison.OrdinalIgnoreCase)); return ouPart?.Split('=').LastOrDefault()?.Trim(); } }Step 2: Create a tenant-restricted LDAP login provider
using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Volo.Abp.Features; using Volo.Abp.Guids; using Volo.Abp.Identity; using Volo.Abp.Identity.ExternalLoginProviders.Ldap; using Volo.Abp.Ldap; using Volo.Abp.MultiTenancy; using Volo.Abp.Settings; namespace YourProject; public class TenantRestrictedLdapExternalLoginProvider : LdapExternalLoginProvider { protected ITenantStore TenantStore { get; } public TenantRestrictedLdapExternalLoginProvider( IGuidGenerator guidGenerator, ICurrentTenant currentTenant, IdentityUserManager userManager, IIdentityUserRepository identityUserRepository, OpenLdapManager ldapManager, ILdapSettingProvider ldapSettingProvider, IFeatureChecker featureChecker, ISettingProvider settingProvider, IOptions<IdentityOptions> identityOptions, ITenantStore tenantStore) : base(guidGenerator, currentTenant, userManager, identityUserRepository, ldapManager, ldapSettingProvider, featureChecker, settingProvider, identityOptions) { TenantStore = tenantStore; } public override async Task<bool> TryAuthenticateAsync(string userName, string plainPassword) { // First, perform standard LDAP authentication var isAuthenticated = await base.TryAuthenticateAsync(userName, plainPassword); if (!isAuthenticated) { return false; } // Skip tenant validation for host users if (CurrentTenant.Id == null) { return true; } // Query LDAP for user's organizational unit var customLdapManager = (CustomOpenLdapManager)LdapManager; var userOu = await customLdapManager.GetUserOrganizationalUnitAsync(userName); if (userOu.IsNullOrWhiteSpace()) { Logger.LogWarning($"Could not determine organizational unit for LDAP user: {userName}"); return false; } // Get current tenant info var tenant = await TenantStore.FindAsync(CurrentTenant.Id.Value); if (tenant == null) { return false; } // Compare user's OU with the current tenant if (!IsUserAllowedForTenant(userOu, tenant.Name)) { Logger.LogWarning( $"LDAP user '{userName}' belongs to OU '{userOu}' and is not allowed for tenant '{tenant.Name}'"); return false; } return true; } protected virtual bool IsUserAllowedForTenant(string userOu, string tenantName) { // Simple approach: tenant name matches OU name (case-insensitive) // Customize this mapping based on your business requirements return string.Equals(userOu, tenantName, System.StringComparison.OrdinalIgnoreCase); } }Step 3: Register the custom provider in your module
public override void ConfigureServices(ServiceConfigurationContext context) { // Replace the default LdapExternalLoginProvider with our tenant-restricted version Configure<AbpIdentityOptions>(options => { options.ExternalLoginProviders.Remove(LdapExternalLoginProvider.Name); options.ExternalLoginProviders.Add<TenantRestrictedLdapExternalLoginProvider>(LdapExternalLoginProvider.Name); }); }How it works:
- User submits credentials on the login page (with a tenant selected).
AbpSignInManager.PasswordSignInAsynciterates external login providers and callsTryAuthenticateAsync.- Our custom provider first validates credentials against LDAP (via
base.TryAuthenticateAsync). - If the current context is host (
CurrentTenant.Id == null), it allows login directly. - Otherwise, it queries LDAP for the user's OU using
CustomOpenLdapManager.GetUserOrganizationalUnitAsync. - It compares the OU with the current tenant's name via
IsUserAllowedForTenant. - Returns
falseif there's a mismatch, which blocks the sign-in.
Notes:
- The
CustomOpenLdapManageris automatically injected intoTenantRestrictedLdapExternalLoginProviderbecauseOpenLdapManageris replaced in the DI container via the[Dependency(ReplaceServices = true)]attribute. - The
GetUserOrganizationalUnitmethod provides two strategies for determining the user's OU: reading a direct LDAP attribute (ou) or parsing the OU from the user's DN. You should adjust this based on your LDAP directory structure. - The
IsUserAllowedForTenantmethod uses a simple name comparison. If your tenant names don't match OU names directly, you can implement a custom mapping (e.g., from a configuration file or database table). - LDAP settings in ABP are tenant-aware, but since you use a single LDAP service, make sure the LDAP connection settings (host, port, base DN, etc.) are configured at the host level so all tenants share the same configuration.
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)