Search code examples
.netactive-directoryldapactive-directory-group

Is there a way to make UserPrincipal.GetGroups() and UserPrincipal.GetAuthorizationGroups() calls use LDAPS (port 636) instead of LDAP (port 389)?


We are preparing for Microsoft's March AD update to only allow secure calls using LDAPS, and while checking our .Net code, I discovered that calls to UserPrincipal.GetGroups() and UserPrincipal.GetAuthorizationGroups() appear to use LDAP (port 389) rather than LDAPS (port 636), even if the UserPrincipal object was created with a PrincipalContext established over LDAPS, like so:

    // Explicitly using LDAPS (port 636)
    PrincipalContext principalContext = new PrincipalContext(ContextType.Domain, "our.corpdomain.com:636", "DC=our,DC=corpdomain,DC=com", ContextOptions.Negotiate);
    UserPrincipal userPrincipal = UserPrincipal.FindByIdentity(principalContext, "someuser");

    // These calls still use LDAP (port 389)
    var groups = userPrincipal.GetAuthorizationGroups();
    var groups2 = userPrincipal.GetGroups();

Does anyone know why this might happen and, if so, how to force these calls to use LDAPS? If they can't be forced, are there any workarounds for them?


Solution

  • This is certainly a bug in the .NET code, and I'll answer your question, but like I mentioned in your other question, the March update will not "only allow secure calls using LDAPS". The normal LDAP port of 389 will still work after that update. I've seen no evidence that they ever plan to disable it.

    But if you want to ensure it never uses port 389 anyway, you will have no not use UserPrincipal. Use DirectoryEntry and/or DirectorySearcher directly, which is what UserPrincipal uses in the background anyway. This isn't the first bug I've found in the AccountManagement namespace.

    I wrote an article about finding all of a user's groups, which has some sample code for different scenarios. You will have to modify any case where you create a new DirectoryEntry object and specify port 636, like this:

    new DirectoryEntry("LDAP://example.com:636/CN=whatever,DC=example,DC=com")
    

    You can actually omit the domain name if you want (just :636 instead of example.com:636).

    One case I didn't cover in that article is the equivalent to GetAuthorizationGroups, which is to read the tokenGroups attribute. That gives you a list of SIDs of the groups, which you can then look up to find the name of the group. Here is a method that will do that:

    private static IEnumerable<string> GetTokenGroups(DirectoryEntry de) {
        var groupsFound = 0;
    
        //retrieve only the tokenGroups attribute from the user
        de.RefreshCache(new[] {"tokenGroups"});
    
        while (true) {
            var tokenGroups = de.Properties["tokenGroups"];
            foreach (byte[] groupSidByte in tokenGroups) {
                groupsFound++;
                var groupSid = new SecurityIdentifier(groupSidByte, 0);
                var groupDe = new DirectoryEntry($"LDAP://:{de.Options.PasswordPort}/<SID={groupSid}>");
    
                groupDe.RefreshCache(new[] {"cn"});
                yield return (string) groupDe.Properties["cn"].Value;
            }
    
            //AD only gives us 1000 or 1500 at a time (depending on the server version)
            //so if we've hit that, go see if there are more
            if (tokenGroups.Count != 1500 && tokenGroups.Count != 1000) break;
    
            try {
                de.RefreshCache(new[] {$"memberOf;range={groupsFound}-*"});
            } catch (COMException e) {
                if (e.ErrorCode == unchecked((int) 0x80072020)) break; //no more results
    
                throw;
            }
        }
    }
    

    That will use whatever port you used for creating the DirectoryEntry object that you pass in. However, this breaks if you have more than one domain in your environment. Things can get complicated in that case if you want to always use port 636.