Search code examples
c#c++asp.netactive-directoryadsi

Querying an Active Directory object property from ASP.NET application returns old results


For quite a few days now I have been trying to get some custom Active Directory based authentication to work. It all works in theory but apparently my theory is wrong. Users who are logged on to a domain write a string token (e.g. PIN code) to their own property field in Active Directory (doesn't really matter which one, but I used primaryInternationISDNNumber for this) upon logging on to the ASP.NET application This PIN is always generated and written programmatically.

To explain it roughly, the web browser loads a Java applet which then loads a native DLL written in C++, which generates and writes the PIN to current user's Active Directory field. That DLL then returns the generated PIN to the applet which then passes it to the browser, which performs an AJAX call with the data returned to initiate the authentication. The application, which has got access to the AD, reads this field value for the connecting user object and checks if it matches with the one the user supplied. If PIN codes match, the user is successfully authenticated.

This is the sample code the ASP.NET application used to read the AD:

        using (var de = new DirectoryEntry("LDAP://" + domainName))
        {
            using (var adSearch = new DirectorySearcher(de))
            {
                // Get user from active directory.
                adSearch.Filter = "(sAMAccountName=" + userName.Trim().ToLower(CultureInfo.CurrentCulture) + ")";
                var adSearchResult = adSearch.FindOne();
                var entry = adSearchResult.GetDirectoryEntry();
                var pinCodeProp = entry.Properties["primaryInternationISDNNumber"];
                return pinCodeProp != null ? pinCodeProp.Value : string.Empty;
            }
        }

This works fine, often. But often is not acceptable. It needs to always be working.

The trouble is that the ASP.NET application sometimes gets the value which was previously in that field, not the actual value. As if there is some kind of caching. I have tried to add de.UsePropertyCache = false but that yielded the same results.

I have created two Win32 console applications for test purposes. One writes the PIN code, the other reads the PIN code. They always work fine!

I thought, this gotta be some problem with IIS application pool. So I have created a native DLL which gets loaded by the ASP.NET application using Platform Invoke. This DLL creates a new thread, calls CoInitialize and reads the PIN code. This is the code:

    pszFqdn = argv[1];
    pszUserName = argv[2];
    pszPassword = argv[3];

    IADs *pObject = NULL;
    HRESULT hr = S_OK;

    hr = CoInitialize(NULL);
    if (SUCCEEDED(hr))
    {
        hr = ADsOpenObject(pszFqdn, pszUserName, pszPassword, ADS_SECURE_AUTHENTICATION, IID_IADs, (LPVOID*)&pObject);

        if (SUCCEEDED(hr) && pObject)
        {
            VARIANT var;
            VariantInit(&var);

            hr = pObject->Get(CComBSTR("primaryInternationalISDNNumber"), &var);
            if ((SUCCEEDED(hr) && var.bstrVal) || hr == 0x8000500d)
            {
                if (hr != 0x8000500d)
                {
                    // convert the BSTR received to TCHAR array
                    std::wstring wsValue(var.bstrVal, SysStringLen(var.bstrVal));
                    // copy the received value to somewhere
                    // ... not relevant
                }

                VariantClear(&var);
            }

            pObject->Release();
        }
    }

    CoUninitialize();

To my tremendous and unpleasant surprise, this code after a day of working properly, started returning the previous values, just like the managed code before!

So now I thought, alright, I wasn't able to escape the IIS application pool and since this gotta be a problem with IIS application pool, I will create a native Windows application which I will execute by using Process.Start method. I will return my PIN code by means of process exit code (since it's an integer anyway). The application uses the similar C++ code as the DLL above.

So I start my application, wait for it to finish, read the exit code. Returns the bad value!

But okay, I'd say, the process is started using the current user credentials, which is again IIS application pool. So I start the application under different credentials. And guess what..... it returns the old value again (?!?!?!).

And I thought Java was hell... Anyone has got any idea about what could possibly be going on here?


Solution

  • It was the replication indeed. As I didn't want to force the replication before reading the field (that would have been a time-expensive operation probably anyway), I came to an idea to read this field from each domain controller and check if any of them matches with the value supplied.

    As it might prove helpful to someone, I did that using the following code.

            var ctx = new DirectoryContext(
                DirectoryContextType.DirectoryServer,
                ipAddress,
                userName, // in the form DOMAIN\UserName or else it would fail for a remote directory server
                password);
            var domain = Domain.GetDomain(ctx);
            var values = new List<string>();
            foreach (DomainController dc in domain.DomainControllers)
            {
                using (var entry =
                        new DirectoryEntry(
                            "LDAP://" + dc.IPAddress,
                            userName,
                            password))
                {
                    using (var search = new DirectorySearcher(entry))
                    {
                        search.Filter = "(&(primaryInternationalISDNNumber=*)(sAMaccountName=" + userName + "))";
                        var result = search.FindOne();
                        var de = result.GetDirectoryEntry();
                        if (de.Properties["primaryInternationalISDNNumber"].Value != null)
                        {
                            values.Add(de.Properties["primaryInternationalISDNNumber"].Value.ToString());
                        }
                    }
                }
            }