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! SSL 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 UID (UserID) that matches the login name provided by the user on the form. 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 = "OU=Users,O=Server"
Ldap1.Timeout = 10 'a timeout > 0 will make the component behave synchronously
Ldap1.Search("uid=" + txtUserID.Text)
NOTE: Active Directory (and some others, if they are configured in this way) require an explicit Bind before you can browse or modify the directory. If you are using such a server, simply set a DN to bind with, a password (if applicable), and call the Bind method prior to using the code above.
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, followed by the Result event, which will let me know if the Search finished with no errors or not (The Result event fires for every LDAP method, like Add, Delete, and Modify). After the Result event fires the ResultCode, ResultDescription, and ResultDN properties will contain the values of the last communication from the LDAP server. ResultCode and ResultDescription contain the same information that comes in the Result event. ResultDN contains the DN of the last result received - in this case, the last search result.
After the Search, I'll examine the ResultDN. If its empty, display an error message and exit.
If Ldap1.ResultDN = "" Then
TextBox1.Text += "User Not found."
If the ResultDN is not empty, 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. Important: In order for this to work, the entry MUST have a userPassword attribute. If there is no userPassword attribute for this DN, the authentication will always fail.
Ldap1.DN = Ldap1.ResultDN
Ldap1.Password = txtPassword.Text
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
Section 2: Which Directory? (Root DSE Searches)
Its possible for an LDAP server to contain more than one directory. For example, a directory for customer/client contacts, and a directory 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 - each LDAP server can actually contain several different directories. One of the DSE Attributes is called "namingContexts", and this attribute is a list of base DN's (one for each directory) that one can access on this particular server. DSE Information will also tell you which versions of the LDAP protocol the server can understand.
A DSE search requires several attributes:
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
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 attribute property arrays of the LDAP component. Specifically, the AttrType() array will contain the type of each response attribute. The AttrValue() array will contain the corresponding values. AttrCount 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 = false
For i = 0 To LDAP.AttrCount - 1
'this line prints out ALL attributes
'Response.Write("ATTR " + ldap.AttrType(i) + " = " + ldap.AttrValue(i) + "<br>")
If ldap.AttrType(i) = "namingContexts" Then
foundnamingcontexts = true
Response.Write("namingContexts: " & ldap.AttrValue(i) & "<br>")
mybaseDN = ldap.AttrValue(i)
Elseif ldap.AttrType(i) = "" And foundnamingcontexts = true Then
Response.Write("namingContexts: " & ldap.AttrValue(i) & "<br>")
foundnamingcontexts = false
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 "ou=People, O=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 UID to the beginning of it. If the users loginname is "SJenkins", I can set the DN = "uid=SJenkins, ou=People, IO=Server".
baseDN = "ou=People, O=Server"
ldap.DN = "uid=" + Request("loginname") + ", " + baseDN
Every LDAP entry is required to have a set of "objectClass" type attributes. For a person, these are:
type = objectClass, value = top
type = objectClass, value = Person
type = objectClass, value = organizationalPerson
type = objectClass, value = inetorgperson
So before I add this new DN to the server, I'll need to set some base attributes, which will be required by the server. I do this using the AttrType() and AttrValue() arrays. Below I'll simply set a description, the UID and userPassword.
ldap.AttrCount = 7
ldap.AttrType(0) = "objectClass"
ldap.AttrValue(0) = "top"
ldap.AttrType(1) = "objectClass"
ldap.AttrValue(1) = "Person"
ldap.AttrType(2) = "objectClass"
ldap.AttrValue(2) = "organizationalPerson"
ldap.AttrType(3) = "objectClass"
ldap.AttrValue(3) = "inetorgperson"
ldap.AttrType(4) = "description"
ldap.AttrValue(4) = "New Account"
ldap.AttrType(5) = "uid"
ldap.AttrValue(5) = txtUserID.Text
ldap.AttrType(6) = "userPassword"
ldap.AttrValue(6) = txtPassword.Text
NOTE: Active Directory will only allow you to create a new entry with the objectClass attributes. After the new entry is added, you can go back and add new attributes via the Modify method (See section 4 for Modify sample).
Voila! Now I have all of this new users attributes set up including his uid and password. I have his DN set. Now all thats left to do is add him to the server.
SJenkins 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 = "uid=SJenkins, ou=People, dc=Server"
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.AttrCount = 1
ldap.AttrType(0) = "description"
ldap.AttrValue(0) = "Suspended"
ldap.AttrModOp(0) = 2 'Replace
After this, if I examine the description attribute for "SJenkins", 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 AttrModOp() to 0 or 1 (Add or Delete) instead of 2 (Replace).