Implementing Active Directory Services in ASP.NET 2.0

By Peter Nichols

The advent of ASP.NET 2.0 and Visual Studio 2005 Express has kicked the door wide open for creating integrated applications. Many of the security tasks required for an applications authentication and authorization mechanisms to be hooked into Active Directory have been dramatically simplified.

The information below will provide a "cookbook" of recipes to perform both Active Directory integration tasks (using the "LDAP:\\" provider) as well as local member server tasks (using the "WinNT:\\" provider). Each section can be cut and pasted directly into its own aspx.vb code page, and is self sufficient. Remember that you must use challenge response for any of this code to work, so that AD or the local member server has the correct permissions to perform the action. Setting the encryption level and other such communication details between the member server running the code and the directory service domain controller is beyond the scope of this article.

Although Microsoft provides a deep mechanism for providing security into an application, your business model may not allow independent authentication and or authorization stores. All of these examples were created because a customer did not want to create extranet Sharepoint users in Active Directory (AD mode), so the local machine was the next best alternative.

Verify That a User is in a Active Directory Group

The first recipe is to verify a user that has connected to an ASP.NET 2.0 page is in an Active Directory group. Add a new item (Web Form) to your site, called "Test.aspx", placing the code in a separate file. Go to design mode, and double click on the page, which should give you a blank Page_Load subroutine. Paste this code directly over the existing code:

Imports System.DirectoryServices
Partial Class Test
    Inherits System.Web.UI.Page

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        Dim strUser As String
        Dim binFlag As Boolean

        REM This is equivalent to adsRoot=GetObject("LDAP://OU=ADSI,DC=ds,DC=microsoft,DC=com")
        REM and opens a connection to the root of the directory that you
        REM would want to search. Replace the string with your directory service root.
        Dim adsRoot As New DirectoryEntry("LDAP://OU=ADSI,DC=ent,DC=ds,DC=microsoft,DC=com")

        REM This sets up the filter to be used in searching for the user in AD.
        Dim adsSearch As DirectorySearcher = New DirectorySearcher(adsRoot)

        REM Grab the User ID of the person pulling the page, or the sAMAccountName
        strUser = Page.User.Identity.Name

        REM Strip off domain name (we already know it, although in multi-domain
        REM environments you might find that useful.
        strUser = Mid(strUser, InStr(1, strUser, "\") + 1)

        REM Search Active Directory For the user via
        REM System.DirectoryServices.DirectorySearcher
        Try
            REM We'll load the filter with the items we want to fetch,
            REM similar to a SQL statement.
            REM The first is what we are looking for, the sAMAccountName.
            adsSearch.PropertiesToLoad.Add("sAMAccountName")
            REM We will also need the group membership of the user once
            REM we have found the user.
            adsSearch.PropertiesToLoad.Add("memberof")
            REM We are likely to also need the common name, although
            REM it's not needed for this example.
            adsSearch.PropertiesToLoad.Add("cn")
            REM We don't need the .FullName property for this example,
            REM but you might, so I show it here.
            adsSearch.PropertiesToLoad.Add("FullName")
            REM build the search filter (looking for the user with a login
            REM name that matches who connected to the page.
            adsSearch.Filter = "sAMAccountName=" & strUser

            REM Get some variables ready to receive the results
            Dim oResult As SearchResult
            Dim RetArray As New Hashtable()
            Dim adsGrpcn As String
            binFlag = False

            REM Now get the results (just one), what you get back is
            REM an object that points to the found user
            oResult = adsSearch.FindOne
            REM You can now loop through the list of groups
            For Each adsGrpcn In oResult.GetDirectoryEntry().Properties("memberof").Value
                REM You'll want to splice this string a bit to match a specific group
                REM Then test to see if it matches your application group. Make sure to
                REM use TRIM() to avoid embedded spaces in the common name of the group.
                Response.Write(adsGrpcn)
                If adsGrpcn = "MyGroup" Then binFlag = True
            Next
        Catch ex As Exception
            Response.Write("I got the following error while trying to authenticate you: " _                 & ex.Message)
            Response.End()
        End Try
        If binFlag Then
            Response.Write("You are authorized!")
        Else
            Response.Write("You are not authorized!")
        End If
    End Sub
End Class

List All Users

This recipe is for iterating all users on a local machine, which can then populate a list box. This would also likely find its way to the "Page_Load" sub, but is shown separately. It presumes a list control called 'lstUser' is on the page. It will populate the list showing the Full Name, but have the User ID as the value.

Dim lblDMBase As String
lblDMBase = "WinNT://LocalMachine"
REM This is equivalent to adsUser=GetObject("WinNT://LocalMachine/User") and
REM opens a connection to the local machine. It does not necessarily need to be
REM the machine the web page is running on.
Dim adsComputer As New DirectoryEntry(lblDMBase)
Dim adsUser As DirectoryEntry
REM This works identically to classic asp, iterate through the parent object.
For Each adsUser In adsComputer.Children
    REM If it is a user, then add it to the list box.
    If adsUser.SchemaClassName = "User" Then
        lstUser.Items.Add(New ListItem(adsUser.Properties("FullName").Value, adsUser.Name))
    End If
Next

Create a New User

The next recipe is for creating a user on the local machine, easily tested by dropping a button on the previous page and double clicking on it. Remember that the user running the page has to have the permissions create a user (be in the local Administrators group).

Dim lblDMBase As String
lblDMBase = "WinNT://LocalMachine"
REM This is equivalent to adsUser=GetObject("WinNT://LocalMachine/User") and
REM opens a connection to the local machine. It does not necessarily need to be
REM the machine the web page is running on.
Dim adsComputer As New DirectoryEntry(lblDMBase)
Dim adsUser As DirectoryEntry
REM Open a connection to the Group
Dim adsGroup As New DirectoryEntry(lblDMBase & "/Users")
REM You can also open the object by:
'Dim adsGroup As DirectoryEntry
'adsGroup.Path = lblDMBase & "/Users"
Try
    REM Add a user to the defined computer object
    adsUser = adsComputer.Children.Add("TestUser", "User")
    REM Populate the FullName and Description Properties
    adsUser.Properties("FullName").Add("Test Full Name")
    adsUser.Properties("Description").Add("Test Description")
    REM Set the password (a random password function would be good here).
    adsUser.Invoke("SetPassword", "password")
    REM Identical to .SetInfo
    adsUser.CommitChanges()

    REM Add the User to the Group
    adsGroup.Invoke("Add", New Object() {adsUser.Path.ToString()})
    adsGroup.CommitChanges()
    Response.Write("TestUser created.")
Catch ex As Exception
    Response.Write("Error in Create User: " & ex.Message)
End Try

View a User's Attributes

The next recipe is to view attributes of a local user. This example builds upon the previous example by using the user selected in the previous examples list box. Create a "View User" button on the form, double click on the button, and add this code into the sub:

Dim lblDMBase As String
lblDMBase = "WinNT://LocalMachine"
Dim strProp As String
Dim binIL As Boolean

REM This is equivalent to adsUser=GetObject("WinNT://LocalMachine/User") and
REM opens a connection to the local machine. It does not necessarily need to be
REM the machine the web page is running on.
Try
    Dim adsUser As New DirectoryEntry(lblDMBase & "/" & lstUser.SelectedItem.Value)
    strProp = adsUser.Name & " (" & adsUser.Properties("FullName").Value & "), "
    Response.Write(strProp & adsUser.Properties("Description").Value)
    binIL = adsUser.InvokeGet("IsAccountLocked")
    If binIL Then
        strProp = "Account is Locked."
    Else
        strProp = "Account is not Locked."
    End If
    strProp = strProp & " Last Login: " & adsUser.InvokeGet("LastLogin")
Catch ex As Exception
    strProp = strProp & " Last Login N/A"
End Try
Response.Write(strProp)

Check for a User in a Local Group

Simple check for a user in a local group, it presumes you've added the user iteration code above and created another button:

Dim lblDMBase As String
REM The object opened here is a local group, but could be an AD Group or OU
lblDMBase = "WinNT://LocalMachine"
REM This is equivalent to adsUser=GetObject("WinNT://LocalMachine/User") and
REM opens a connection to the local machine. It does not necessarily need to be
REM the machine the web page is running on.
Dim adsUser As New DirectoryEntry(lblDMBase & "/" & lstUser.SelectedItem.Value)
REM This is equivalent to adsUser=GetObject("WinNT://LocalMachine/Group")
Dim adsGroup As New DirectoryEntry(lblDMBase & "/Administrators")
If adsGroup.Invoke("IsMember", adsUser.Name.ToString()) Then
    Response.Write "You're a Member!"
Else
    Response.Write "You're a not a Member!"
End If

Iterate All Computers in a Container

This next recipe is more for auditing. It iterates all the computers in a container and then connects to the local administrator of that server to get the account policy.

Dim adsRoot As New DirectoryEntry("LDAP://OU=ADSI,DC=ent,DC=ds,DC=microsoft,DC=com")
Dim adsUser, adsComputer As DirectoryEntry
Dim strCompName As String
Dim ocnt As Integer

For Each adsComputer In adsRoot.Children
    If adsComputer.SchemaClassName = "computer" Then
        strCompName = Mid(adsComputer.Name, 4)
        Response.Write(strCompName & " (")
        Response.Write(adsComputer.Properties("OperatingSystemVersion").Value & "): ")
        ocnt = ocnt + 1
        Try
            adsUser = New DirectoryEntry("WinNT://"&Trim(strCompName)&"/Administrator")
            Response.Write(adsUser.Properties("MaxPasswordAge").Value / 86400 & ", ")
            Response.Write(adsUser.Properties("MinPasswordLength").Value & ", ")
            Response.Write(adsUser.Properties("PasswordHistoryLength").Value & ", ")
            Response.Write(adsUser.Properties("MaxBadPasswordsAllowed").Value & ", ")
            Response.Write(adsUser.Properties("AutoUnlockInterval").Value & ", ")
            Response.Write(adsUser.Properties("LockOutObservationInterval").Value & ", ")
        Catch ex As Exception
            Response.Write(ex.Message & "<br>")
        End Try
        Response.Write("<br>")
    End If
Next
Response.Write(ocnt & " Servers Flagged.")

Properties

There are lots of properties you can pull from the local (WinNT) user account some must be read or written via ".Properties":

UserFlags HomeDirectory MaxPasswordAge
MaxStorage LoginScript MinPasswordAge
PasswordAge Profile PasswordHistoryLength
PasswordExpired HomeDirDrive AutoUnlockInterval
LoginHours Parameters LockoutObservationInterval
FullName PrimaryGroupID MaxBadPasswordsAllowed
Description Name RasPermissions
BadPasswordAttempts MinPasswordLength objectSid

These local (WinNT) or AD (LDAP) user attributes that are "computed" and so must be obtained by "InvokeGet" or written by "InvokeSet":

SetPassword
AccountDisabled
AccountExpirationDate
IsAccountLocked
LastLogin

These are some of the AD (LDAP) user attributes readable via ".Properties". If they are not mandatory, get the property with a "Try".

Attribute Description/Comments Mandatory
memberOf Returns a string array of group membership. Yes
cn The common name, make sure to have a search of samAccountName return this attribute in case you need to rebind to the user object. Yes
department,
description,
division,
employeeID,
givenName,
telephoneNumber,
facsimileTelephoneNumber,
postalAddress,
postalCode,
homePhone,
mobile,
title
Returns a string containing user information. If the field in the account has never been populated, the code will return an error. No
displayName FullName No
lockoutTime IsAccountLocked but returns a date/time. For the flag, use '.InvokeGet("IsAccountLocked")'. No
lastLogoff Returns a date indicating the last time the user logged off. Although this attribute would appear mandatory, a freshly created user will not have this. No
lastLogon Returns a date indicating the last time the user logged on (which is NOT when then authenticated to a web page). No
mail Returns the string containing the user's email address.  
objectClass For users will always return the string "User". Handy for filtering by object type. Yes
sAMAccountName Returns the login name, which will match what you get when challenging for credentials on a web page. Yes
userAccountControl Returns flags that control the behavior of the user account. You're much better off using the "invoke" commands to get info here. Yes
whenChanged Returns a date for the last time the object was modified. Yes
whenCreated Returns a date for when the object was created. Yes

These are the local (WinNT) group attributes readable via ".Properties":

groupType
Name
Description
objectSid

These are the AD (LDAP) group attributes readable via ".Properties":

member distinguishedName sAMAccountType
cn objectCategory sIDHistory
description objectClass uSNChanged
dSCorePropagationData objectGUID uSNCreated
groupType objectSid whenChanged
instanceType name whenCreated
nTSecurityDescriptor sAMAccountName

These are the AD (LDAP) OU attributes readable via ".Properties":

dSCorePropagationData objectClass uSNCreated
instanceType objectGUID whenChanged
nTSecurityDescriptor ou whenCreated
distinguishedName name
objectCategory uSNChanged

References

Mapping Between IADsUser Properties and Active Directory Attributes:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ldap/ldap/ldap.asp

DirectoryServices Namespace:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/sds/sds/directoryservices_namespace.asp

Querying Active Directory using .NET classes and LDAP queries:
http://www.codeproject.com/dotnet/activedirquery.asp?df=100&forumid=15636&exp=0&select=847485



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • The first phase of API management was about realizing the business value of APIs. This next wave of API management enables the hyper-connected enterprise to drive and scale their businesses as API models become more complex and sophisticated. Today, real world product launches begin with an API program and strategy in mind. This API-first approach to development will only continue to increase, driven by an increasingly interconnected web of devices, organizations, and people. To support this rapid growth, …

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds