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]