Performing Web Authentication and Administration with LDAP
In this document I will cover how to authenticate a user/password pair in ASP.NET with an LDAP server, how to determine which directory a user is located in (if the Directory Server contains more than one directory), how to add new users, as well as how to handle general maintenance of the users (additions, deletions, and modifications).
In order to accomplish all of this I'm going to use the LDAP component of the IP*Works! .NET Edition. While there are many different editions of IP*Works! to choose from, including IP*Works! if you need to securely communicate with an SSL-Enabled LDAP server, I have chosen to use the IP*Works! .NET Edition for simplicity.
Section 1: The Basic Login
First things first - I need a login form for the users to enter a userid and password. On the form, I'll drop textboxes and labels for a "User ID" and "Password" to be submitted by the user, as well as a "Login" button for the user to click.
I'll add some code to bLogin.Click so that if the button is clicked, the authentication of the user will take place. To do this I'll perform a search for a username that matches the login name provided by the user on the form. This "username" is commonly the sAMAccountName attribute, uid attribute, or even the cn attribute (it depends on your directory server). I'll need to point the LDAP object to the LDAP server, provide a base DN on which to perform the search (see DSE Information, Section 2, for more details), and call the Search method with the search filter for this particular User ID.
Private Sub bLogin_Click(ByVal sender As System.Object,
ByVal e As System.EventArgs) Handles bLogin.Click Ldap1.ServerName = txtServer.Text Ldap1.DN = "CN=Users,DC=Server" Ldap1.Timeout = 10 'a timeout > 0 will make the component behave synchronously Ldap1.Search("sAMAccountName=" + txtUserID.Text)
Now, the SearchResult event of the LDAP component will fire with any search results received from the server. The SearchComplete event will fire when all of the results have been received. Inside the SearchResult event I will find the full DN of the matching username, so that I can use it for binding. Note that some directory servers, like Active Directory, will allow you to bind directly with the sAMAccountName instead of requiring you to use a full DN.
If no SearchResult events fire, I should display an error message like "User Not Found" and exit. If the SearchResult does fire with a matching username, I know that the Search succeeded, and the User I searched for does exist. The next step will be to attempt to authenticate this user with the password they provided.
Ldap1.DN = searchresultdn Ldap1.Password = txtPassword.Text Ldap1.Bind() If Ldap1.ResultCode = 0 Then TextBox1.Text += "Success! You have been validated." + vbCrLf Else 'login result was not "OK" TextBox1.Text += "Error: " + Ldap1.ResultDescription + vbCrLf End If End Sub
Section 2: Which Directory? (Root DSE Searches)
LDAP servers can contain multiple naming contexts. For example, a directory may contain a tree for customer/client contacts, and a tree for employees. Normally the developer will know what these are ahead of time, but not always. So if I want to have the same login interface for both of these groups of people, and I do not know what the base DN's are, I'll need a way to determine exactly which base DN's to perform the search against. Each directory on the server will have a unique base DN. For example, on my server, the customer/client contacts directory base DN is:
The employee directory has a base DN of:
If I don't know what these are - how can I determine them programmatically? This is one of the things that can be resolved by the root DSE (Directory Specific Entry) search. This is information that all LDAP servers will provide so that clients can have access to attributes of the server itself. Some of this information can be quite useful. For one example - one of the DSE Attributes is called "namingContexts", and this attribute is basically a list of base DN's that one can access on this particular server. DSE Information will also tell you which versions of the LDAP protocol the server can understand, and what extended features it supports.
A DSE search requires several attributes:
Blank DN Search Filter of "objectClass=*" Search Scope of "Base"
This can be done with the LDAP component, like so:
ldap.ServerName = SERVERNAME ldap.DN = "" ldap.SearchScope = ssBaseObject ldap.Search("objectClass=*")
The search result of a DSE search will not be like others - where I am searching for a particular DN. The only thing returned by this search will be attributes of the server, and these will arrive in the attributes collection of the LDAP component. Specifically, the Attributes(i).AttributeType will contain the type of each response attribute. The Attributes(i).Value will contain the corresponding values. Attributes.Count is the total number of attributes returned by the server. In this case, I am only interested in the namingContexts attributes, so that I can see exactly which base DN's I have on this server. I'll pick these out and write them.
Dim foundnamingcontexts foundnamingcontexts = false For i = 0 To LDAP.AttributesCount - 1 'this line prints out ALL attributes 'Response.Write("ATTR " + ldap.Attributes(i).AttributeType + " = " +
' ldap.Attributes(i).Value + "<br>") If ldap.Attributes(i).AttributeType = "namingContexts" Then foundnamingcontexts = true Response.Write("namingContexts: " & ldap.Attributes(i).Value & "<br>") mybaseDN = ldap.Attributes(i).Value Elseif ldap.Attributes(i).AttributeType = "" And foundnamingcontexts = true Then Response.Write("namingContexts: " & ldap.Attributes(i).Value & "<br>") Else foundnamingcontexts = false End if Next
The above for loop becomes a little more complicated than you might imagine. The first instance of the namingContexts type in the attribute arrays is of type "namingContexts". But for subsequent attributes of the same type, which arrive one after the other, the server doesn't specify that type, but just leaves the type as empty string. In other words, in order, the server sent attributes like so:
Type: sometype , Value: sometype value Type: namingContexts, Value: dc=siroe, dc=com Type: , Value: dc=Server Type: , Value: dc=Netscape, dc=com Type: someotherType , Value: someothertype value Type: , Value: someothertype value
So "dc=siroe, dc=com", "dc=Server", and "dc=Netscape, dc=com" are all namingContexts attributes, even though the second two have empty string as the type.
Section 3: Add New User
Now I've got a working login page for a website, but what happens when someone new drops by and wants to join or create a login? I need to programmatically add them to the LDAP server so that they can authenticate themselves.
The information that I'll need from the user is of course a UID (loginname) and a password. Just for demonstration, I'll also set a description attribute for the user. If you want other information - go for it, but keep in mind that the LDAP server allows only specific attribute types, and you'll need to stick with those. This shouldn't be a problem because there are many defined: address, phone number, description, and many others for you to use. See your server documentation for a full list.
When the user submits this information, I'll need to setup the DN and attribute arrays for this new LDAP entry first.
Before I set the DN - I need to know what base DN to add this person to. This is commonly something like "cn=People, dc=Server". If you are unsure about this DN, please consult your server documentation, or browse the directory until you find the tree where you want to add the entry and find its DN. Once I have the DN (ie "ou=People, O=Server") I'll want to modify it to create our new users dn. To do this, I just add their CN to the beginning of it. If the users common name is "Lance Robinson", I can set the DN = "cn=Lance Robinson, cn=People, dc=Server". If your server is not Active Directory, you may need to use UID instead of CN.
baseDN = "cn=People, dc=Server" ldap.DN = "cn=" + Request("commonname") + ", " + baseDN
Every LDAP entry is required to have a set of "objectClass" type attributes. For a person in Active Directory, these are:
type = objectClass, value = top type = objectClass, value = Person type = objectClass, value = organizationalPerson type = objectClass, value = inetorgperson
Before you try adding the user to your server, check its schema. You may need to set some other attributes, like cn, sn, uid, or userPassword. You can do this using the Attributes collection. Below I'll simply set a description, sAMAccountName, and userPassword.
ldap.Attributes.Add(New LDAPAttribute("objectClass", "top"))
ldap.Attributes.Add(New LDAPAttribute("", "person"))
ldap.Attributes.Add(New LDAPAttribute("", "organizationalPerson"))
ldap.Attributes.Add(New LDAPAttribute("", "inetorgperson"))
ldap.Attributes.Add(New LDAPAttribute("description", "New Account")) ldap.Attributes.Add(New LDAPAttribute("sAMAccountName", txtUserID.Text)) ldap.Attributes.Add(New LDAPAttribute("userPassword", txtPassword.Text))
Voila! Now I have all of this new users attributes set up including his user id (sAMAccountName) and password. I have his DN set. Now all that's left to do is add him to the server.
"Lance Robinson" will now be able to login with his password via the method outlined in Section 1.
Section 4: General Maintenance
Any administrator needs to have a method of manually adding, deleting, or modifying user accounts under their control. By using LDAP as an authentication and directory tool, one can perform general maintenance on these accounts by hand, in person, on the actual server itself. However, with this LDAP component, this could also be done via the same website. This would allow website administration to take place remotely.
I've already covered adding new accounts. Deleting and modifying accounts are equally as simple.
In order to delete an account - all I need to do is set the DN for that account and use the Delete method.
ldap.DN = "cn=Lance Robinson, cn=People, dc=Server" ldap.Delete()
What if I don't want to delete the account, I just want to deactivate it? Let's say I want to still have a record that this account exists, but I don't want the user to have login access any longer. To do this, I could use a description attribute that specifies account status. When the user attempts to login, I can check this attribute to verify that the user has login rights. When I added "SJenkins" to the directory, I gave his description type attribute the value of "New Account". Now if I want to suspend this account, I could change this description attribute to "Suspended". To do this I'll need to modify the existing attribute. The LDAP component includes a Modify method for doing this. The Modify method can perform different kinds of attribute modifications: Add attribute, Delete attribute, and Replace attribute. Since the description attribute already exists, I'll use the Replace option. This is defined in the AttrModOp() array. Here, I just have 1 attribute that I want to replace. So I'll set the AttrCount to 1, the first (0 index) element of the AttrType() array to the type I am looking for (description), the zeroth element of the AttrValue() array to the new value for this attribute, and the first (0 index) element of the AttrModOp() array to 2 (replace). Then I'll just call the Modify method.
ldap.Attributes.Add(New LDAPAttribute("description", "suspended", LDAPAttributeModOps.amoReplace)) ldap.Modify()
After this, if I examine the description attribute for "Lance Robinson", it will have a value of "suspended" instead of "New Account". If I wanted to delete/add the attribute type description of the value "suspended", I would use the same method, except I would set the LDAPAttributeModOp parameter of the attribute to amoAdd or amoDelete instead of amoReplace.
We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at firstname.lastname@example.org.