[ Team LiB ] Previous Section Next Section

25.4 Binding to Objects Via Authentication

Whenever we need to access the properties of an object in Active Directory, we bind to it using VBScript's GetObject function or the ADSI method IADsOpenDSObject::OpenDSObject. The circumstances in which each method should be used to access Active Directory is very clear-cut but deserves to be outlined here, as it will be important whenever you construct ASPs.

25.4.1 When to Use VBScript's GetObject Function

By default, many of the objects and properties within Active Directory can be read by any authenticated user of the forest. As an example, here is some code to connect to an Organizational Unit called Sales under the root of the domain. This code works under the WSH:

Set objSalesOU = GetObject("LDAP://ou=Sales,dc=mycorp,dc=com")
Wscript.Echo objSalesOU.Description

Here is the same script incorporated into an ASP:

<HTML>
<HEAD>
<TITLE>Binding to an existing Organizational Unit</TITLE>
</HEAD>
   
<BODY>
<%
  Set objSalesOU = GetObject("LDAP://ou=Sales,dc=mycorp,dc=com")
  Response.Write "The Sales OU description is: " & objSalesOU.Description
%>
</BODY>
</HTML>

This mechanism works perfectly when you wish to have read-only access to properties of objects that can be read without special privileges. Using GetObject is not appropriate in the following cases:

  • You want to write properties of an object.

  • The object you are attempting to bind to requires elevated privileges to access.

While it may make little sense, it is perfectly feasible to restrict read access to the description of the Sales Organizational Unit, or more commonly the Sales Organizational Unit itself. If the Sales Organizational Unit is restricted, a GetObject will fail to bind to it. If only the description is restricted, a GetObject will successfully bind to the Sales Organizational Unit, but access to the description property will be denied.

To gain access to a restricted object or impersonate another user, you must authenticate using IADsOpenDSObject::OpenDSObject.

25.4.2 When to Use IADsOpenDSObject::OpenDSObject

Here is a simple Organizational Unit creation script that works under the WSH when an administrator is logged in:

Set objRoot=GetObject("LDAP://dc=mycorp,dc=com")
   
Set objSalesOU = objRoot.Create("organizationalUnit","ou=Sales")
objSalesOU.Description = "My new description!"
objSalesOU.SetInfo

We cannot transfer the script to an ASP as it stands. To make the script work, we must use the IADsOpenDSObject::OpenDSObject method, which does allow authentication. Here is the same example using authentication within an ASP:

<HTML>
<HEAD>
<TITLE>Successful Organizational Unit Creation</TITLE>
</HEAD>
   
<BODY>
<%
  strPath = "LDAP://dc=mycorp,dc=com"
  strUsername = "cn=Administrator,cn=Users,dc=mycorp,dc=com"
  strPassword = "my-admin-password"
   
  Set objNamespace = GetObject("LDAP:")
  Set objRoot = objNamespace.OpenDSObject(strPath, strUsername, strPassword, 0)
   
  Set objSalesOU = objRoot.Create("organizationalUnit","ou=Sales")
  objSalesOU.Description = "My new description!"
  objSalesOU.SetInfo
   
  Response.Write "The Sales OU has been created in the " & strPath & " domain."
%>
</BODY>
</HTML>

If we wanted to manipulate any of the properties of the new Sales Organizational Unit during that script, we could continue to use the objSalesOU variable to do so. If we write a new script that needs to access that Organizational Unit and to print the description, we now can use either GetObject or authenticate directly to that Organizational Unit in the same way as we did to the root of the tree:

<HTML>
<HEAD>
<TITLE>Binding to an existing Organizational Unit</TITLE>
</HEAD>
   
<BODY>
<%
  adsOUPath = "LDAP://ou=Sales,dc=mycorp,dc=com"
  strUsername = "cn=Administrator,cn=Users,dc=mycorp,dc=com"
  strPassword = "my-admin-password"
   
  Set objNamespace = GetObject("LDAP:")
  Set objSalesOU = objNamespace.OpenDSObject(adsOUPath,strUsername,strPassword,0)
   
  Response.Write "The Sales OU description is: " & objSalesOU.Description
%>
</BODY>
</HTML>

What may seem strange is that you can authenticate to the root of the tree and access objects there, but you still need to authenticate again to other areas of the tree if you need to bind to them. One authentication to a point in the tree does not allow you to use GetObject happily throughout the rest of the script for all objects and containers below that point. If you need authentication, for whatever reason, to access objects in disparate areas of the tree, you need to authenticate each binding separately. As an example, this next script creates the Organizational Unit again and then sets the description for a user named Ian Harcombe in the Users container. In this example, both need authentication because we wish to update properties in both cases:

<HTML>
<HEAD>
<TITLE>Example use of Complex Authentication</TITLE>
</HEAD>
   
<BODY>
<%
  strRootPath = "LDAP://dc=mycorp,dc=com"
  strUserPath  = "LDAP://cn=Ian Harcombe,cn=Users,dc=mycorp,dc=com"
  strUsername = "cn=Administrator,cn=Users,dc=mycorp,dc=com"
  strPassword = "my-admin-password"
   
  Set objNamespace = GetObject("LDAP:")
  Set objRoot = objNamespace.OpenDSObject(strRootPath,strUsername,strPassword,0)
   
  Set objSalesOU = objRoot.Create("organizationalUnit","ou=Sales")
  objSalesOU.Description = "My new description!"
  objSalesOU.SetInfo
   
  Set objUser = objNamespace.OpenDSObject(strUserPath,strUsername,strPassword,0)
  objUser.Description = "My new description!"
  objUser.SetInfo
%>
</BODY>
</HTML>

25.4.3 When to Use IADsContainer::GetObject

We've shown that we cannot use the VBScript GetObject function to authenticate a connection to objects in Active Directory from an ASP. However, there is a method called IADsContainer::GetObject that can be used to bind to objects from a container using the preexisting authenticated connection for the container. While both GetObjects have identical names and similar functions, to save confusion, we will use the fully qualified IADsContainer::GetObject when talking about the method and GetObject when talking about VBScript's function.

The IADsContainer::GetObject method is used to retrieve items from a container. It takes two parameters, the class of the object to retrieve and the object's RDN. The fact that IADsContainer::GetObject retrieves objects using the RDN means that you do not have to bind to individual objects below a container. This saves authenticating a connection to each object if you need to work on multiple objects in a container. If the Sales Organizational Unit now has three users below it, we can authenticate to the container and then use the IADsContainer::GetObject method to manipulate the three users. Here is an example:

<HTML>
<HEAD>
<TITLE>Use of IADsContainer::GetObject</TITLE>
</HEAD>
   
<BODY>
<%
  adsOUPath = "LDAP://ou=Sales,dc=mycorp,dc=com"
  strUsername = "cn=Administrator,cn=Users,dc=mycorp,dc=com"
  strPassword = ""
   
  Set objNamespace = GetObject("LDAP:")
  Set objSalesOU = objNamespace.OpenDSObject(adsOUPath,strUsername,strPassword,0)
   
  Set objUser1 = objSalesOU.GetObject("User","cn=Simon Williams")
  Set objUser2 = objSalesOU.GetObject("User","cn=Markie Newell")
  Set objUser3 = objSalesOU.GetObject("User","cn=Jason Norton")
   
  Response.Write "Simon Williams' description is: " & objUser1.Description
  Response.Write "Markie Newell's description is: " & objUser2.Description
  Response.Write "Jason Norton's description is: " & objUser3.Description
%>
</BODY>
</HTML>

This works under WSH in exactly the same way as it does here. However, it is something that you may make much more use of in an ASP to save you from a lot of unnecessary authentication. If the class is null with IADsContainer::GetObject, the first item matching the RDN of any class is returned.

25.4.4 Authenticating from Passwords Input Via Forms

When you need to force authentication in a script, the last thing you want to do is hardcode a password into the script as we have been doing previously. That's not to say that the ASP isn't secure; it is. The script is visible only to users of the computer in which IIS is running and not to web client users, because it is parsed before being displayed. Also, if the permissions are secured, even this is not visible. However, you have to keep in mind that you will have to change the embedded password in all the ASPs every time the real password is changed. The simplest solution is to use an HTML form with a field for the password in your ASP and prompt for a password from the user. An example would look like this:

<HTML>
<HEAD>
<TITLE>Authentication Request</TITLE>
</HEAD>
   
<BODY>
<FORM ACTION="restricted.asp" METHOD=POST>
   
  <P>Name <BR><INPUT 
    NAME="Name"
    TYPE=TEXT
    VALUE="cn=Administrator,cn=Users,dc=mycorp,dc=com"
    SIZE="60">
   
  <P>Password<BR><INPUT 
    TYPE="PASSWORD"
    NAME="Password">
   
  <P><INPUT TYPE="SUBMIT" VALUE="OK"><INPUT TYPE="RESET" VALUE="Reset">
</FORM>
   
</BODY>
</HTML>

This eliminates the problem of having to embed the username and password in a script. The <INPUT TYPE="PASSWORD"> tag places asterisk (*) characters in the field whenever a character is entered. In this instance, the username and password are passed from this authentication page to your page entitled restricted.asp, which will use the credentials to perform the authentication and continue on.

However, it is still extremely cumbersome for you to have to type in the full DN you want to authenticate with. It would be much better for it to accept the simple username (i.e., Administrator) in the username box. For this script to do that, it would need to use ADO to search Active Directory for the user object with the RDN made up of the prefix CN= and the username. You know it is CN= as all users use this prefix, and you only authenticate to the tree with user objects. This example will be left until later, in Section 25.5 of this chapter.

25.4.5 A Simple Password Changer

A simple ASP example to show both server-side ADSI scripts and the use of the password attribute of the FORM tag is a password changer. Users load the page, type their usernames along with their old and new passwords into a form, and click the Submit button. Submitting the form triggers an authentication access to the user object supplied, using the user object itself and the old password. Provided that the user successfully authenticated to the user's own object, the password is then changed using the IADsUser::ChangePassword method.

This script consists of two parts: the form itself, which sits in the body of the page, and the code that interprets the submission of the form, which is located in the header. Let's start with the makeup of the form, which needs six fields:

  • A text input field for the name

  • A password input field for the current password

  • A password input field for the new password

  • A password input field to confirm the new password

  • A Submit button

  • A Reset button that sets all the input fields back to their default values

Here is the form:

<FORM ACTION="psw_changer1.asp" METHOD=POST>
   
  <P>Name <BR><INPUT 
    NAME="Name"
    TYPE=TEXT
    VALUE="cn=xxxxx,cn=Users,dc=mycorp,dc=com"
    SIZE="60">
   
  <P>Old Password<BR><INPUT 
    TYPE="PASSWORD"
    NAME="OldPassword">
   
  <P>New Password<BR><INPUT 
    TYPE="PASSWORD"
    NAME="NewPassword1">
  <P>Confirm Password<BR><INPUT 
    TYPE="PASSWORD"
    NAME="NewPassword2">
   
  <P><INPUT TYPE="SUBMIT" NAME="SetPass" VALUE="Change Password!">
  <INPUT TYPE="RESET" VALUE="Reset">
</FORM>

The username field has been given a default value that will appear in the entry box to save typing. Obviously this would be much improved if the user could just type his username and an ADO search was initiated.

Whenever the Submit button is clicked, the page is reloaded according to the value associated with the ACTION parameter. In addition, the form's fields have been set. This differs from the normal loading of a page when the fields will be empty. In the server-side code, we need to make sure that the code is triggered only when the page is loaded via the submission of the form. To do this, we can surround the code with the following section:

<%
  On Error Resume Next
  If Request.Form("SetPass") = "Change Password!" Then
    'Code goes here
  End If
%>

Once the form is submitted, the value of the SetPass button will be the button's label. Until that happens, the value is blank. This is a good way to check for the submission of a form.

Assuming that this code is being executed properly after submission, we need to check that the new passwords match. We will be using only one of the values to set the new password, so we have to make sure that both passwords are as the user intended. To do that, we can use Request::Form again to check both passwords:

<%
  On Error Resume Next
  If Request.Form("SetPass") = "Change Password!" Then
    If Request.Form("NewPassword1") = Request.Form("NewPassword2") Then
      'code goes here
    Else
      Response.Write "The two new passwords do not match. Please try again."
    End If
  End If
%>

We are now ready to fill in the rest of the code, which is fairly straightforward. First, we need to authenticate to the user, and if that is successful, attempt to change the password. Example 25-1 lists the completed ASP code incorporating the ADSI calls, which have been highlighted.

Example 25-1. ASP code incorporating the ADSI calls
<HTML>
<HEAD>
<TITLE>Simple Password Changer</TITLE>
<%
  On Error Resume Next
  If Request.Form("SetPass") = "Change Password!" Then
    If Request.Form("NewPassword1") = Request.Form("NewPassword2") Then
   
      strUsername = "LDAP://" & Request.Form("Name")
      Set objNamespace = GetObject("LDAP:")
      'Attempt to authenticate to the user object in the tree using 

      'the username and the current password
      Err.Clear
      Set objUser = objNamespace.OpenDSObject(strUsername, _
        Request.Form("Name"), Request.Form("OldPassword"), 0)
      If Err=0 Then
    
        'Attempt to change the password
        Err.Clear
        objUser.ChangePassword _
          CStr(Request.Form("OldPassword")), _
          CStr(Request.Form("NewPassword1"))
        If Err=0 Then
          Response.Write "Password has been changed."
        Else
          Response.Write "Error: the Password has not been changed."
        End If
      Else
        Response.Write "Unable to authenticate. Password or Username incorrect."
      End If
    Else
      Response.Write "The two new passwords do not match. Please try again."
    End If
  End If
%>
</HEAD>
   
<BODY>
<FORM ACTION="psw_changer1.asp" METHOD=POST>
   
  <P>Name <BR><INPUT 
    NAME="Name"
    TYPE=TEXT
    VALUE="cn=xxxxx,cn=Users,dc=mycorp,dc=com"
    SIZE="60">
   
  <P>Old Password<BR><INPUT 
    TYPE="PASSWORD"
    NAME="OldPassword">
   
  <P>New Password<BR><INPUT 
    TYPE="PASSWORD"
    NAME="NewPassword1">
  <P>Confirm Password<BR><INPUT 
    TYPE="PASSWORD"
    NAME="NewPassword2">
   
  <P><INPUT TYPE="SUBMIT" NAME="SetPass" VALUE="Change Password!">
  <INPUT TYPE="RESET" VALUE="Reset">
</FORM>
</BODY>
</HTML>

As you can see, this is not particularly difficult. You also could add an Else clause and print out Hex(Err.Number) and Err.Description if you wished.

25.4.6 Adding Users to Groups

A password changer is a good example for a simple form, but more complex forms can sometimes be necessary. In this example, we want to populate two list boxes with users and groups from the default Users container. If we select a user and a group from the two list boxes and enter a username/password that has permissions, we should be able to click the Submit button to add the user to the group.

Once again, like most ADSI ASPs that use forms, this page is split into two parts: the form itself and the server-side script. The form is a fairly simple extension of the one that we outlined earlier and it is listed in Example 25-2. Population of the list boxes is done using two sets of server-side scripts that enumerate all values in the Users container and add any items to the list box of the appropriate class. The important population code is emphasized.

Example 25-2. Adding a user to a group
<FORM ACTION = "userlist2.asp" METHOD = "POST">
  <P>Users: 
  <SELECT NAME = "user">
<%  Set objUsersContainer = _
      GetObject("LDAP://cn=Users,dc=mycorp,dc=com")
    For Each objObject in objUsersContainer
      If objObject.Class = "user" Then %>
        <OPTION><% = objObject.Name %>
<%    End If
    Next %>
  </SELECT>
  Groups: 
  <SELECT NAME = "group">
<%  For Each objObject in objUsersContainer
      If objObject.Class = "group" Then %>
        <OPTION><% = objObject.Name %>
<%    End If
    Next %>
  </SELECT></P>
   
  <P>Username: <INPUT 
    NAME="Name"
    TYPE=TEXT
    VALUE="cn=Administrator,cn=Users,dc=mycorp,dc=com"
    SIZE="60">
   
  <P>Password<INPUT 
    TYPE="PASSWORD"
    NAME="Password">
   
  <P><INPUT TYPE=SUBMIT NAME="Submit" VALUE="Add User To Group!">
</FORM>

The server-side script that interprets the results needs to make sure that the script is executed only when the form has been submitted. Once that condition is true, the script has to attempt to authenticate to the selected group using the username and password supplied in the form. Example 25-3 lists the whole script with the major ADSI calls in the server-side script emphasized.

Example 25-3. Authenticating the new user to the selected group
<HTML>
<HEAD>
<TITLE>Adding Users to Groups from the default Users Container</TITLE>
   
<%
  On Error Resume Next
  If Request.Form("Submit") <> "" Then
    strGroupPath = "LDAP://" & Request.Form("group") _
      & ",cn=Users,dc=mycorp,dc=com"
    strUserPath = "LDAP://" & Request.Form("user") _
      & ",cn=Users,dc=mycorp,dc=com"
   
    Set objNamespace = GetObject("LDAP:")
    Err.Clear
    Set objGroup = objNamespace.OpenDSObject(strGroupPath, _
                    CStr(Request.Form("Name")), _
                    CStr(Request.Form("Password")),0)
    If Err=0 Then
      If objGroup.IsMember(strUserPath) Then
        Response.Write "User is already a member of the group"
      Else
        Err.Clear
        objGroup.Add(strUserPath)
        If Err=0 Then
          Response.Write "User is now a member of the group"
        Else
          Response.Write "An error occurred when adding the user to the group."
        End If
      End If
    Else
      Response.Write "Authentication failed."
    End If
  End If
%>
</HEAD>
   
<BODY>
<P>
<FORM ACTION = "userlist2.asp" METHOD = "POST">
  <P>Users: 
  <SELECT NAME = "user">
<%  Set objUsersContainer = _
      GetObject("LDAP://cn=Users,dc=mycorp,dc=com")
    For Each objObject in objUsersContainer
      If objObject.Class = "user" Then %>
        <OPTION><% = objObject.Name %>
<%    End If
    Next %>
  </SELECT>
  Groups: 
  <SELECT NAME = "group">
<%  For Each objObject in objUsersContainer
      If objObject.Class = "group" Then %>
        <OPTION><% = objObject.Name %>
<%    End If
    Next %>
  </SELECT></P>
   
  <P>Username: <INPUT 
    NAME="Name"
    TYPE=TEXT
    VALUE="cn=Administrator,cn=Users,dc=mycorp,dc=com"
    SIZE="60">
   
  <P>Password<INPUT 
    TYPE="PASSWORD"
    NAME="Password">
   
  <P><INPUT TYPE=SUBMIT NAME="Submit" VALUE="Add User To Group!">
</FORM>
</BODY>
</HTML>

We checked to see if the user was a member just so that the script is more user friendly. You also can see in this script that we use the GetObject function to populate the initial list boxes, but we then switch to IADsOpenDSObject::OpenDSObject as soon as we need to update Active Directory.

    [ Team LiB ] Previous Section Next Section