Tutorial - Performing Web Authentication and Administration
with LDAP
By Lance Robinson - Technical Evangelist, /n software.
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! 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 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:
CN=People, DC=Server
The employee directory has a base DN of:
CN=Users, DC=Server
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 isntead 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.
ldap.Add()
"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
kb@nsoftware.com.