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 |
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