Search code examples
c#active-directorydirectoryentryprincipalcontext

Which is better PrincipalContext or DirectoryEntry for user creation in Active Directory C#


We need to create bulk users in AD using C#. Looking for something that has good performance. I found 2 ways to do it on Google.

  1. Using PrincipleContext.
  2. Using DirectoryEntry.

Was wondering, what should I use?

Tried some code using DirectoryEntry but I see that I am able to set the user password after commitChanges(), for example:

    newUser.CommitChanges();  // Commit once before setting password
    newUser.Invoke("SetPassword", new object[] { "12345" });
    newUser.CommitChanges();

Why do we need to commit twice? Will it have some performance impact?

Also, let's say I have to assign group membership too to the user and some exception occurred while assigning the group membership, in this case how to roll back the committed changes so far (i.e. user creation)?

Kindly suggest.


Solution

  • So you can do it all in one shot, but it's a little complicated, and you can't use PrincipalContext to do it, since that's too high level.

    The actual attribute used to set the password is unicodePwd. The documentation for that shows how it needs to be set, namely:

    1. The connection has to be secure.
    2. The password has to be in UTF-16 surrounded by quotes, encoded as an "octet string" (a byte array, basically).

    Using newUser.Invoke("SetPassword", new object[] { "12345" }); calls the native Windows IADsUser::SetPassword method, which handles all of that for you. It finds whatever secure method is available and uses that to set the password for you. But it can only be called on an existing account.

    To avoid that, you have to handle this all yourself. If you can manage condition 1, then condition 2 is easy once you know how to do it.

    There are a couple ways to satisfy the "secure" condition:

    1. By using AuthenticationTypes.Sealing to tell it to encrypt using Kerberos. If you're on the same network as the DC, then this is the easiest.
    var ou = new DirectoryEntry("LDAP://OU=Users,DC=example,DC=com", null, null, AuthenticationTypes.Sealing);
    
    1. By using LDAP over SSL. For this, you just need to add :636 to the domain name, but it also requires that the DC uses an SSL certificate that your computer actually trusts. If it uses a self-signed cert, then this will fail.
    var ou = new DirectoryEntry("LDAP://example.com:636/OU=Users,DC=example,DC=com");
    

    Once you have your ou object for the OU where you want to create the account, you can create it:

    var newUser = ou.Children.Add("CN=MyTestUser", "user");
    newUser.Properties["samAccountName"].Value = "MyTestUser";
    newUser.Properties["userPrincipalName"].Value = "[email protected]";
    newUser.Properties["unicodePwd"].Value = Encoding.Unicode.GetBytes("\"MyPassword2020\"");
    newUser.Properties["userAccountControl"].Value = 512; //NORMAL_ACCOUNT
    newUser.CommitChanges();
    

    Those are all the mandatory attributes for the account. I'm sure you'll want to set more. Notice the weird encoding of the password, and that it is surrounded by quotes. Setting userAccountControl is required, otherwise it will be created as disabled.

    This is actually something I just learned today! I was originally going to tell you that it wasn't possible since all these years I've been doing it in two steps. But I did some searching and found this, which had the key. It also has an example with how to do it with LdapConnection. The only benefit of using that is if you need to use LDAPS, but with a self-signed cert, since LdapConnection allows you to write your own certificate validation method.

    If you want to immediately add this new user to a group, make sure you connect to the group on the same DC where you created the user, since the other DCs won't know about the user until replication happens. It would look something like this:

    var server = newUser.Options.GetCurrentServerName();
    var group = new DirectoryEntry($"LDAP://{server}/CN=MyGroup,OU=Groups,DC=example,DC=com");
    
    group.Properties["member"].Add(newUser.Properties["distinguishedName"].Value);
    group.CommitChanges();