Open Closed

Hook points for On-Prem Windows AD authentication #10529


User avatar
0
rwright-ruhealth created

Using ABP 8.3.3. Have not yet upgraded to the latest 10 version due to breaking changes. Currently on ABP Enterprise subscription.

Provide us with the following info: 🧐 Hint: If you are using the ABP Studio, you can see all the information about your solution from the configuration window, which opens when you right-click on the solution and click on the Solution Configuration button.

  • Where can we intercept the call to ActiveDirectory in order to check login and also determine if user is authenticated? Note: setting the AD server in settings only partly gets you there. It still does not work, nor does it create an account when a usser registers. Tried this in ABP dotnetcore 10 and dotnet core 8 versions. We had to write code to authenticate the user in AD, but where to after this?
Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

7 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • In ABP 8.3.3, there isn’t a built-in “intercept the call to ActiveDirectory” hook in the login pipeline for on-prem Windows AD / LDAP the same way there is for OIDC/OAuth external providers. The “AD server in settings” you mentioned is not enough by itself to make ABP automatically authenticate against AD and auto-provision users.
    • The supported/ABP-native place to plug in “authenticate elsewhere, then create/sign-in an ABP user” is the External Login pipeline (i.e., treat AD as an external identity provider). In practice for on-prem AD, you typically implement your own authentication handler/provider (LDAP bind / Kerberos / etc.), then sign in using ABP Identity by creating/finding an IdentityUser and issuing the normal ABP auth cookie/token.

    What to do (ABP 8.3.3, MVC, tiered)

    1. Do AD authentication in the Auth Server (or IdentityServer/OpenIddict host)

      • In a tiered solution, the “login happens” in the AuthServer/IdentityServer app (depending on your template/version).
      • Put your AD credential validation in the login flow there (e.g., custom login page model / custom action that validates username+password against AD).
    2. After AD says “OK”, create or find the ABP user and sign in

      • The “where to after this?” part is: create/find IdentityUser and sign in via ASP.NET Core Identity (so ABP can do permissions/auditing/tenancy/etc.).
      • Conceptually:
        • Look up user in ABP by username/email.
        • If not found, create it (and set email/name fields from AD if you have them).
        • Optionally add an external login record (provider name like "LDAP" and provider key like AD’s GUID/UPN) if you want it tracked like an external provider.
        • Then call the normal sign-in manager to issue the auth cookie / token.
    3. Do NOT expect “Register” to create AD accounts

      • ABP’s register page creates users in the ABP database/Identity store, not in AD.
      • If you need “registration” to mean “create in AD”, that is a separate provisioning process (and usually not allowed in many enterprises). You’d have to customize registration to call your AD provisioning logic.
    4. If you want automatic user creation on first login

      • Use the same logic in the login callback (after AD validation) to create the ABP IdentityUser record the first time that AD user signs in.

    Verified references (closest applicable ABP guidance)

    • ABP account module overview (login/registration are ABP Identity-based): https://abp.io/docs/latest/modules/account
    • ABP’s general statement about authentication capabilities (mentions AD/LDAP as an integration scenario, but not a built-in “hook point” API): https://abp.io/docs/latest/others/why-abp-platform

    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)
  • User Avatar
    0
    rwright-ruhealth created

    Where in code do we start?

    If you want automatic user creation on first login

    Use the same logic in the login callback (after AD validation) to create the ABP IdentityUser record the first time that AD user signs in.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    rwright-ruhealth created

    Another issue is it seems the AD being looked for is some generic AD, not windows AD. Many code changes for AD had to be made to finally authenticate. Even then, it seems something is missing. Shouldn't be this much work.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    enisn created
    Support Team .NET Developer

    Hi, Thanks for the follow-up. If you are using the built-in LDAP login in Identity Pro, the place to start is not the register page. The username/password login flow already calls the LDAP provider automatically:

    1. The login page posts the username and password.
    2. ABP calls SignInManager.PasswordSignInAsync(...).
    3. AbpSignInManager checks the registered external login providers.
    4. LdapExternalLoginProvider.TryAuthenticateAsync(...) performs the LDAP bind.
    5. If LDAP authentication succeeds, ABP creates or updates the IdentityUser and signs the user in. So, for your scenario, the main hook points are:
    • LdapExternalLoginProvider: customize how the username is normalized before authentication.
    • OpenLdapManager: customize how ABP searches the directory and reads user information. This is important because the built-in LDAP implementation is OpenLDAP-oriented by default (uid=..., cn=..., and (&(uid=...))). For Windows AD, you usually need a Windows AD-specific format such as DOMAIN\\user, user@domain, or a search based on sAMAccountName / userPrincipalName. So changing only the LDAP server settings is usually not enough. Also, the register page is not the correct place for LDAP provisioning:
    • normal registration creates a local ABP user
    • it does not create an AD account
    • for LDAP, ABP user creation happens after a successful LDAP login, or by using the Identity Pro external user import feature So the smallest practical next step is:
    1. Keep the login page as is.
    2. Replace OpenLdapManager for your Windows AD bind/search behavior.
    3. Replace LdapExternalLoginProvider so the username format matches your AD format.
    4. Make sure the AD user has a readable email value, because ABP needs that while creating the IdentityUser. If you mean true integrated Windows Authentication (IIS/Negotiate) rather than LDAP username/password, that is a different flow and goes through the external login callback path instead of the LDAP provider. If login succeeds but the ABP user is still not created, please share these details and we can narrow it down quickly:
    • the exact username format you are submitting (jsmith, DOMAIN\\jsmith, or jsmith@domain)
    • whether the AD user has the mail attribute populated
    • your custom username normalization / LDAP search filter changes
    • the exact exception or log entry right after the successful bind Best regards, ABP Support Team
    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    rwright-ruhealth created

    Thank you for the quick reply. NO: Not using IIS\WINDOWS authentication.

    1. I modified LdapExternalLoginProvider NormalizeUserNameAsync method:
    2. protected virtual async Task<string> NormalizeUserNameAsync(string userName)
    {
        string usertoLogin=$"mydomain\\{userName}, {await LdapSettingProvider.GetBaseDcAsync()}";
        return usertoLogin;
       // not used: return $"uid={userName}, {await LdapSettingProvider.GetBaseDcAsync()}";
    }
    
    When I login with the built-in admin account: 
    OpenLdapManager calls ConnectAsync
    schema var gets set to LDAP
    Next,`ldapConnection.Connect(await LdapSettingProvider.GetServerHostAsync(), await LdapSettingProvider.GetServerPortAsync(), schema);` is called.
    I expect this to fail, of course. I get an invalid username and password. The login does not fall back to a local ABP account. Can't login at all.
    
    Next. trying a domain account in format mydomain\myusername and I get further:
    In Method  DetermineProviderCultureResult(HttpContext httpContext)
    and I get to this point:
    `string returnUrl = request.Query[ReturnUrl];  // this is empty.
    if (returnUrl.IsNullOrWhiteSpace())
    {
        return await NullProviderCultureResult;
    }`
    action returns and same message invalid username and password.
    At some point the code keeps moving along and I get the authservermodule
    
       `.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, options =>
       {
           //Personal Microsoft accounts as an example.
           options.AuthorizationEndpoint = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize";
           options.TokenEndpoint = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
    
           options.ClaimActions.MapCustomJson("picture", _ => "https://graph.microsoft.com/v1.0/me/photo/$value");
           options.SaveTokens = true;
       })`
    
    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    rwright-ruhealth created

    And ultimately never logs in.
    After coding and modifying a console app as provided in one of the examples, I can authenticate to On-Prem Windows Server AD. Here's the working code for the console app, slightly modified. Note using static LdapForNet... was necessary and all LdapForNet.Native was required.

    using System;
    using System.Diagnostics;
    using System.Linq;
    using System.Threading.Tasks;
    
    using LdapForNet;
    using LdapForNet.Native;
    using static LdapForNet.Native.Native;
    
    /**********************
     cmd to find logon DC
    systeminfo | find /i "logon server"
    
    distinguished name (DN) = CN=Doe\, Michael,OU=DEVELOPERS,OU=MarketSt,OU=MYOU,DC=mydomain,DC=local (DN ldap entry for Michael Doe)
    Filter example: ds.Filter = "(&(objectCategory=User)(objectClass=person)(name=" + userName + "*))";
    
     ********************/
    
    
    namespace LDAPTester
    {
        public class Program
        {
            static async Task Main(string[] args)
            {
                var server = "10.10.10.11";
                var serverPort = 389;
                var baseDc = "OU=users,DC=mydomain,DC=local";
                
    
                using (var cn = new LdapConnection())
                {
                    try
                    {
                        cn.Connect("ldap://10.10.10.11:389");
                        // bind using userdn and password
                        cn.Bind(LdapAuthMechanism.SIMPLE, "mydomain\\michaeldoe", "Password111444");
        var entries = cn.Search("DC=mydomain,DC=local", "(&(objectCategory=user)(objectClass=person)                        (sAMAccountName=michaeldoe))").Take(10);
                        foreach (var entry in entries)
                        {
                            var x = entry;  
                            // key/value pairs of attributes  directoryAttributes are the LDAP                                             //field category names, givenName, surName etc.
                            var de = x.ToDirectoryEntry();
                            Console.WriteLine(entry);
                            //Debug.WriteLine(x.Dn["name"][0].ToString());
                        }
                        string s = "";
                    }
                    catch (Exception exception)
                    {
                        string msg = exception.Message;
                        bool stopHere = true;
                    }
                }
    
                ```
    
    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    enisn created
    Support Team .NET Developer

    Hi,

    Thanks for the extra details. We re-checked this in a fresh ABP 8.3.3 commercial MVC/tiered solution and validated that the following customization shape compiles against the generated template.

    There are two separate points here:

    1. The local admin fallback should still work.
    2. The Windows AD username/search format still needs to be aligned with your directory.

    For the first point: in ABP 8.3.3, the login flow first tries the LDAP external provider, and if that returns false, it falls back to the normal local PasswordSignInAsync. So a failed LDAP bind should not block the built-in local admin user.

    Because of that, if admin also fails, please verify these two items first:

    • local login is still enabled
    • the local admin password is still valid

    As a quick confirmation step, temporarily disable LDAP login and test the built-in admin user alone.

    For the LDAP customization itself, the important correction is this: the bind username should not be mydomain\\user, <BaseDc>. For Windows AD simple bind it should generally be only one of these formats:

    • mydomain\\userName
    • userName@mydomain.local

    So LdapExternalLoginProvider.NormalizeUserNameAsync should return only the bind name and should not append BaseDc.

    For this scenario, the register page is not the correct customization point. In ABP 8.3.3 the relevant flow is:

    • LoginModel.OnPostAsync(...)
    • AbpSignInManager.PasswordSignInAsync(...)
    • LdapExternalLoginProvider.TryAuthenticateAsync(...)
    • after successful LDAP authentication, ABP creates or updates the IdentityUser

    So for Windows AD you will usually need to customize both:

    • LdapExternalLoginProvider
    • OpenLdapManager

    Below is the exact code shape we validated. You should adapt the namespace and class names to your solution.

    WindowsAdLdapExternalLoginProvider.cs

    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.Extensions.Options;
    using Volo.Abp.DependencyInjection;
    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.Identity;
    
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(WindowsAdLdapExternalLoginProvider), typeof(LdapExternalLoginProvider))]
    public class WindowsAdLdapExternalLoginProvider : LdapExternalLoginProvider
    {
        protected WindowsAdLdapManager WindowsAdLdapManager { get; }
    
        public WindowsAdLdapExternalLoginProvider(
            IGuidGenerator guidGenerator,
            ICurrentTenant currentTenant,
            IdentityUserManager userManager,
            IIdentityUserRepository identityUserRepository,
            OpenLdapManager ldapManager,
            ILdapSettingProvider ldapSettingProvider,
            IFeatureChecker featureChecker,
            ISettingProvider settingProvider,
            IOptions<IdentityOptions> identityOptions,
            WindowsAdLdapManager windowsAdLdapManager)
            : base(
                guidGenerator,
                currentTenant,
                userManager,
                identityUserRepository,
                ldapManager,
                ldapSettingProvider,
                featureChecker,
                settingProvider,
                identityOptions)
        {
            WindowsAdLdapManager = windowsAdLdapManager;
        }
    
        protected override Task<string> NormalizeUserNameAsync(string userName)
        {
            return WindowsAdLdapManager.NormalizeForActiveDirectoryAsync(userName);
        }
    }
    

    WindowsAdLdapManager.cs

    using System.Text;
    using System.Threading.Tasks;
    using LdapForNet;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Identity.ExternalLoginProviders.Ldap;
    using Volo.Abp.Ldap;
    
    namespace YourProject.Identity;
    
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(WindowsAdLdapManager), typeof(OpenLdapManager), typeof(ILdapManager), typeof(LdapManager))]
    public class WindowsAdLdapManager : OpenLdapManager
    {
        public WindowsAdLdapManager(ILdapSettingProvider ldapSettingProvider)
            : base(ldapSettingProvider)
        {
        }
    
        protected override async Task<string> NormalizeUserNameAsync(string userName)
        {
            return await NormalizeForActiveDirectoryAsync(userName);
        }
    
        protected override Task<string> GetUserEmailAsync(LdapEntry ldapEntry)
        {
            var directoryEntry = ldapEntry.ToDirectoryEntry();
    
            return Task.FromResult(
                directoryEntry.GetAttribute("mail")?.GetValue<string>()
                ?? directoryEntry.GetAttribute("userPrincipalName")?.GetValue<string>()
                ?? string.Empty
            );
        }
    
        protected override Task<string> GetUserFilterAsync(string userName)
        {
            return Task.FromResult($"(&(objectClass=user)(sAMAccountName={EscapeFilterValue(userName)}))");
        }
    
        public virtual async Task<string> NormalizeForActiveDirectoryAsync(string userName)
        {
            if (userName.Contains("@") || userName.Contains("\\"))
            {
                return userName;
            }
    
            var domain = await LdapSettingProvider.GetDomainAsync();
            if (string.IsNullOrWhiteSpace(domain))
            {
                return userName;
            }
    
            return $"{userName}@{domain}";
        }
    
        protected virtual string EscapeFilterValue(string value)
        {
            var builder = new StringBuilder(value.Length);
    
            foreach (var character in value)
            {
                builder.Append(character switch
                {
                    '\\' => "\\5c",
                    '*' => "\\2a",
                    '(' => "\\28",
                    ')' => "\\29",
                    '\0' => "\\00",
                    _ => character
                });
            }
    
            return builder.ToString();
        }
    }
    

    Then register the custom provider in your Domain module:

    Configure<AbpIdentityOptions>(options =>
    {
        options.ExternalLoginProviders.Add<WindowsAdLdapExternalLoginProvider>(LdapExternalLoginProvider.Name);
    });
    

    Notes:

    • if your LDAP settings UserName is already stored as mydomain\\serviceUser or serviceuser@mydomain.local, this code keeps it as-is and does not prefix it again
    • GetUserFilterAsync(...) uses sAMAccountName instead of uid
    • GetUserEmailAsync(...) falls back from mail to userPrincipalName
    • DetermineProviderCultureResult(...) with an empty ReturnUrl is not the root cause here
    • AddMicrosoftAccount(...) is unrelated to the LDAP username/password flow

    One more thing to verify: after LDAP bind succeeds, ABP still needs to read the user's email to create the IdentityUser. If the AD mail attribute is empty, or your directory uses a different attribute, then adjust GetUserEmailAsync(...) accordingly.

    If you share your current OpenLdapManager override and the exact format stored in the LDAP settings UserName field, we can narrow down the last missing part.

    Best regards,

    ABP Support Team

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
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 September 21, 2026, 06:18
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.