Active Directory in Generated Applications: Authenticating Against an LDAP Server

“In this article, we will discuss a more complicated business requirement where the user's password also needs to be verified; in this case, against an LDAP server.”

—Jim Murphy, Owner of River City Software Development LLC

Business Need

My last article discussed how to lightly integrate Active Directory authentication by using the Windows user name and getting the associated user's record. No password verification was performed in that simple example.

In this article, we will discuss a more complicated business requirement where the user's password also needs to be verified; in this case, against an LDAP server. Since an LDAP server doesn't allow the searching of a user by their username AND password, the only cost-effective way to get this to work is to use an LDAP server that requires authentication. Then the password check actually happens when asking the LDAP server for permission to search the directory, rather than actually searching the directory based on the user's name and password.

This solution applies to both Microsoft LDAP and Novel eDirectory, since the .NET DirectoryServices namespace handles the implementation for both.

Prerequisites

It is assumed that you are familiar with the basic features of Iron Speed Designer and have made several applications. Also, a fair understanding of how the Role- Based Security Wizard works is required. More information relating to the use of the Role-Based Security Wizard can be found here.

This article also assumes that you are familiar with the previous article on integrating Active Directory with an Iron Speed Designer generated application. This article can be found here.

Step 1 — Database

Fig. 1 - Database schema. This indicates that no password is stored for any User.

Iron Speed Designer's Role-Based Security Wizard will need to have a field selected as the Password field; therefore we will pick the IsActive field as the Password field. However, this field will never really be used since we will write code to override Iron Speed Designer's default behavior and use the LDAP server. Note that in this example, we have decided to store every user in our Users database table. However, if there may be thousands of users, we can also 'remap' the LDAP authenticated user to a more generic User which can be stored in our Users table. This customization is beyond the scope of this article.

Our plan is to simply authenticate the user against an LDAP server, and if everything is good, we'll locate the user by their UserName in the Users table, telling Iron Speed Designer to use the IsActive field as their password, and sign the user in – all without the user being aware of the details. This is all done in code, as we will see later.

Fig. 2 - Users database table contents.

This table contains a few users' names and their UserIDs that we'll use to sign the user into our application. There UserName will be looked up in the Users table only after successful authentication of their password against the LDAP server.

Step 2 — Modifying Your Application using Iron Speed Designer

Run Iron Speed Designer and generate a new application, then run the Role-Based Security Wizard. During the Role-Based Security Wizard, we must supply some sort of Password field since this is the normal way Iron Speed Designer authenticates a user. We can assign this to any database field since we will override the sign-in process and validate the UserName and Password from a completely different source than the databases' Users table. The Role-Based Security Wizard is still needed to secure all of the pages, show and hide menu items, etc.

During the sign-in process, we simply 'hook in' and LDAP-Authenticate the supplied UserName and Password ourselves, and then automatically supply Iron Speed Designer with our own UserName and Password from the database's Users table. First, though, we need to run the Role-Based Security Wizard so our application is ready for our code changes.

Fig. 3 – Role-Based Security Wizard.

We can pick any field as our Password field. In this example, we'll just use the IsActive field.

Step 3 — Adding a Directory Services Reference

Open the project in Visual Studio 2005 (File → Open Website from within Visual Studio 2005). When the project opens, we need to add a Reference to the Directory Services library (DLL) that handles all of the LDAP logic. To do this, right-click the project name (in my case, it is http://localhost/AdLdapCs/) from within Solution Explorer and then select "Add Reference..."

Fig. 4 – Adding a DirectoryServices Reference to our project.

Step 4 - Adding the Code in Visual Studio

After running the Role-Based Security Wizard and rebuilding your application, open the App_Code\Shared\SignIn_Control.Controls.cs page in Visual Studio 2005. (Fig. 5)

Fig. 5 – Empty SignIn_Control class.

Add "using" clauses at the top and override the Login method by typing the word "override" within the public class in Section 1. Then, press the spacebar, and a list of all the routines that can be overridden will pop up. Since there are two overrides for the Login method, be sure to override the signature that has bRedirectOnSuccess as a parameter. After highlighting the proper signature, press Enter and Visual Studio will automatically add the basic code 'hook' where we can place our custom LDAP logic.

Fig. 6 – Empty Login Override.

We've just added to the existing code that will be executed after the user clicks the SignIn button on the SignIn page. It is currently nearly empty – with only a single call to the 'base' (overridden) login code. The login code in that is overridden is located in Section 2, so the call to the base.Login method can be viewed by expanding the Section 2 Region.

Next, we need to fill in the Login routine with the code that hits the LDAP server. If we just call the base.Login routine (as is coded now), we will loose control and the Iron Speed Designer standard authentication will take place based on the rules and fields configured in the Role-Based Security Wizard. During this default process, the user is automatically redirected. This is not what we want, so we will locate and copy the base.Login contents and paste it in our override Login method, then modify the overridden code as needed to look at the LDAP server first.

IMPORTANT NOTE: We will not make any changes to the Section 2 code at the bottom of the page; we will only COPY the code from Section 2, paste it into our overridden method which is in Section 1 and modify the copy in Section 1. We will not even call the original code located in Section 2, since we have pasted the same code into Section 1 and is soon to be modified to work the way we want.

Next, we'll mix in the DirectoryServices (LDAP) code to authenticate and perform the directory search. This will be done in the Section 1 Login code.

public override void Login(bool bRedirectOnSuccess)
{
	string strUserName = this.UserName.Text;
	string strPassword = this.Password.Text;
	string errMessage = "";

	if (strUserName == "" | strPassword == "")
	{
		ProcessLoginFailed(ERR_INVALID_LOGIN_INFO, strUserName);
	}

	string clientIPAddress = this.Page.Request.ServerVariables["REMOTE_ADDR"] + " (HTML)"; string strUserId = "";

	// THIS IS WHERE WE NEED TO INSERT OUR LDAP CODE; AFTER WE COLLECT
	// THE USERNAME AND PASSWORD FROM THE SCREEN, BUT BEFORE THE CALL
	// TO SetLoginInfo (below) WHICH IS IRON SPEED DESIGNER'S ROLE BASED
	// SECURITY METHOD OF SIGNING A USER IN.

	bool bSuccess = false;

	try
	{
		bSuccess = ((BaseApplicationPage)(this.Page)).SystemUtils.SetLoginInfo(strUserName, "1", ref errMessage);
	}
	catch (System.Threading.ThreadAbortException ex)
	{
		throw;
	}
	catch (System.Exception e)
	{
		ProcessLoginFailed(ERR_INTERNAL_ERROR + " " + e.Message, "");
	}

	if (bSuccess)
	{
		// WE MUST REMOVE THE LINES BELOW IN RED.
		if (LoginSucceeded != null)
		{
			LoginSucceeded(this, new System.EventArgs());
		}

		if (bRedirectOnSuccess)
		{
			RedirectOnSuccess();
		}
	}
	else
	{
		if (!(errMessage == null && errMessage != ""))
		{
			ProcessLoginFailed(errMessage, strUserName);
		}
		else
		{
			ProcessLoginFailed(ERR_INVALID_LOGIN_INFO, strUserName);
		}
	}
}

Example 1 — Login Override that is a Copy of the Section 2 Login Routine

Instead of typing in the code as it appears in Fig. 7, it is much safer to copy your own application's Login routine from Section 2. This will ensure that you will have the properly running, latest code if Iron Speed Designer makes any future changes to the logic herein.

If the user cannot be authenticated on the LDAP server with the UserName and Password that they supplied, an exception will be thrown and the code below the red box will NOT be executed. They will only be signed-in and redirected to the proper page after a successful authentication against the LDAP server.

Fig. 7 — Insert DirectoryServices (LDAP) Logic

We must mix our new logic right in with the existing code in the Section 1 Login override.

If your LDAP server requires a specific type of Authentication (Secure Sockets, FastBind, etc.) you can provide that parameter AFTER the strPassword parameter (e.g. AuthenticationTypes.SecureSocketsLayer). If you are not sure which Authentication Type your server uses, either contact your IT department or do a little trial-and-error. Be careful though – if your network's policy locks your user account after three bad login attempts, you could be kicked off the network!

Remove the LoginSucceeded logic since this is no longer necessary (and will prevent the project from compiling since the LoginSucceeded objects are only in Section 2).

Fig. 8 – LoginSucceeded Logic that Needs Removing.

Remove the following code, which can be found in the Section 1 Login override method:

public override void Login(bool bRedirectOnSuccess)
{
	string strUserName = this.UserName.Text;
	string strPassword = this.Password.Text;
	string errMessage = "";
	
	if ((strUserName == "" | strPassword == ""))
	{
		ProcessLoginFailed(ERR_INVALID_LOGIN_INFO, strUserName);
	}

	string clientIPAddress = this.Page.Request.ServerVariables["REMOTE_ADDR"] + " (HTML)";
	string strUserId = "";
	
	// Here is where the users Active Directory name and password are
	// checked against the LDAP server.
	DirectoryEntry entry = new DirectoryEntry("LDAP://engineering.mycompany.com", strUserName, strPassword);
	DirectorySearcher mySearcher = new DirectorySearcher(entry);
	
	try
	{
		// Search the directory - simply searches by username, not
		// password.
		SearchResultCollection results = mySearcher.FindAll();
	
		// If we have any results, then we successfully authenticated to
		// the LDAP server (meaning that the supplied users name and
		// password are valid on the network) and we found the unique
		// username on the LDAP server.
	
		if (results.Count == 0)
		{
		throw new ApplicationException("Unknown User Name or bad password for user: " + strUserName);
		}
	}
	catch (System.Exception ex)
	{
		throw new ApplicationException("Unknown User Name or bad password for user: " + strUserName);
	}
	
	bool bSuccess = false;
	try
	{
		bSuccess = ((BaseApplicationPage)(this.Page)).SystemUtils.SetLoginInfo(strUserName, "1", ref errMessage);
	}
	catch (System.Threading.ThreadAbortException ex)
	{
		throw;
	}
	catch (System.Exception e)
	{
		ProcessLoginFailed(ERR_INTERNAL_ERROR + " " + e.Message, "");
	}
	
	if ((bSuccess))
	{
		if (bRedirectOnSuccess)
		{
			RedirectOnSuccess();
		}
	}
	else
	{
		if (!(errMessage == null && errMessage != ""))
		{
			ProcessLoginFailed(errMessage, strUserName);
		}
		else
		{
			ProcessLoginFailed(ERR_INVALID_LOGIN_INFO, strUserName);
		}
	}
}

Example 2 — Login Override that is Complete Including LDAP Logic

Build and Run your application. When the SignIn page appears, test your network username and network password. If you are afraid that any user can just type a "1" as the password (in the event the user somehow knows we tied the Role-Based Security to the IsActive database field), you can test now and see that no sign-in is granted with such a password. This is because the sign-in will throw an exception if this UserName and Password combination fails to authenticate against the LDAP server. This exception will cause the SetLoginInfo (the Iron Speed Designer Role-Based Security code) to be bypassed. Again, be careful not to lock your Windows account.

NOTE: If, after testing your application with some bad passwords, you find that you get an error when you try to print to your office printer, or get a strange error when trying to access your network drives, this is likely because your account got locked while you were testing.

Conclusion

This Article addresses a user requirement for checking the user's UserName and Password to make sure it is the same as the Windows/Network account. This avoids the complexity of our application having to maintain a password that will surely get out of sync with Windows over time.

This method of authentication does require a secure LDAP server, since an LDAP server that uses the Anonymous or None AuthenticationType will throw an error when the application attempts to connect to it with a network password.

This sample project can be downloaded at http://www.rcsdev.com/files/AdLdapCs.zip. This zip file contains only a C# version of the sample application, as well as a database script that creates Users, Users_Role and Role tables and populates them with sample data.

About the Author

Jim Murphy is the Owner and Chief Application Designer and Developer for River City Software Development LLC. Jim has been programming professionally for over 16 years in fields that include Health Care, Insurance, Banking/Lending, Real Estate, Oil & Gas, Manufacturing, and Government.

Jim was an early adopter of Iron Speed Designer and worked closely with Iron Speed Technical Engineers while the product was maturing. Jim's major strengths are in Microsoft SQL Server, Microsoft Access, C#, Visual Basic .NET, XML, and Reporting.

[Contact the author]