LDAP is not Active Directory, though Active Directory is LDAP. As someone who drinks the Microsoft Kool-Aid, I found myself using LDAP for something other than Active Directory and I thought I should share what I have found. The problem domain was to connect a new MVC application to an existing Sun One LDAP Store.
First off, authenticating an MVC application using forms mode authentication and the Membership providers is straight forward. Start with the ASP.NET MVC 2 Web Application Template that comes out of the box with Visual Studio 2010 when you create a new project and it will have the code to use forms authentication with the membership provider model built in. In the web.config, add the tag:
<authenticationmode="Forms">
<forms loginUrl="~/Account/LogOn"timeout="2880" />
</authentication>
This points to the \Controllers\AccountController.cs class which already contains the code that you need to perform the authentication.
Now for the Membership provider… If you were using Active Directory, you could just use the Active Directory Membership provider, and be done without writing a lick of C# code:
<membershipdefaultProvider="AspNetActiveDirectoryMembershipProvider">
<providers>
<addname="AspNetActiveDirectoryMembershipProvider"
type="System.Web.Security.ActiveDirectoryMembershipProvider,
System.Web, Version=2.0.3600, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a" />
</providers>
</membership>
However, since we are looking at SunOne, we need to use the Generic LDAP Membership provider. Unfortunately, this provider is only included with SharePoint. If you are working in an environment where you have access to this, check out for another code-free solution:
http://msdn.microsoft.com/en-us/library/ms517148.aspx
Otherwise, all that you need to do is implement a membership provider that speaks vanilla LDAP so it can connect to the SunOne store and there you go. Please keep in mind the scope of this blog is only to authenticate to the SunOne store, not to be able to manage it, though carrying this implementation through to that level if needed should be pretty straight forward. The general strategy for implementing the authentication check is a little bit different than I would have expected. I would have expected to authenticate to the LDAP provider using some sort of administrative credential and then calling a “Check Password” function of some kind. The actual way to do it is bind to the LDAP server using the credentials passed in for authentication, and if the bind is successful, then Authentication = true, else, especially in the case of exception is false. I don’t like programming to exceptions so if someone has a better approach, please reply.
In order to do this, create a new project with an output type of class library. Create a public class which will match your configuration file and have it implement MembershipProvider. You can then right click on MemberShipProvider and select “Implement Interface” in order to generate method stubs. Then create a helper class to do the actual work. The AspNetSunOneMemebershipProvider class listing below has been truncated to exclude the methods which I did not implement. In order to compile this you will need to implement them.
File: AspNetSunOneMembershipProvider.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.Security;
using System.DirectoryServices;
namespace SunOne
{
public class AspNetSunOneMembershipProvider : MembershipProvider
{
// Password to SunOneMembershipProviderKey.pfx is SunOne
private static SunOneHelper _helper;
private string _applicationName;
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
// Overriding the Initialize method allows us access to the NameValueCollection that
// is read from the XML attributes
// in the web.config file.
string ldapServer = config["LDAPServer"];
string userNameConnectString = config["UserNameConnectString"];
// Scott Gu says always set the app name
// http://weblogs.asp.net/scottgu/archive/2006/04/22/Always-set-the-_2200_applicationName_2200_-property-when-configuring-ASP.NET-2.0-Membership-and-other-Providers.aspx
ApplicationName = config["ApplicationName"];
_helper = new SunOneHelper(ldapServer, userNameConnectString);
base.Initialize(name, config);
}
public override bool ValidateUser(string username, string password)
{
return _helper.ValidateUser(username, password);
}
public override string ApplicationName
{
get
{
return _applicationName;
}
set
{
_applicationName = value;
}
}
public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
{
return GetUser(providerUserKey.ToString(), userIsOnline);
}
File SunOneHelper.cs
using System;
using System.Collections.Generic;
using System.Configuration;
using System.DirectoryServices.ActiveDirectory;
using System.DirectoryServices.Protocols;
using System.DirectoryServices;
using System.Security.Cryptography.X509Certificates;
using System.Net;
using System.Text;
namespace SunOne
{
public class SunOneHelper : IDisposable
{
#region Member Variables
public string Server { get; set; }
public string UserNameConnectString { get; set; }
private LdapConnection _con;
#endregion
#region Constructor
///<summary>
/// construcor
///</summary>
///<remarks>
/// Standard LDAP ports:
/// LDAP = 389
/// LDAP SSL = 636
///</remarks>
public SunOneHelper(string server, string userNameConnectString)
{
Server = server;
UserNameConnectString = userNameConnectString;
}
#endregion
#region Public Methods
///<summary>
/// Cleanup connection if we have one
///</summary>
public void Dispose()
{
try
{
if (_con != null)
{
_con.Dispose();
}
}
catch
{
}
}
///<summary>
/// Validate an LDAP user and password by attempting to log on.
///</summary>
///<remarks>
/// This method catchs log on exceptions,
/// preferably in DirectoryServicesCOMException but always in the generic
/// exception below and returns false to indicate that the logon failed.
///</remarks>
///<returns>true if logon is successful; otherwise false</returns>
public bool ValidateUser(string username, string password)
{
bool authenticated = false;
try
{
if (_con != null)
{
_con.Dispose();
_con = null;
}
string connectString = UserNameConnectString.Replace("{0}", username);
_con = new LdapConnection(new LdapDirectoryIdentifier(Server));
_con.AutoBind = false;
_con.SessionOptions.AutoReconnect = false;
// no ssl, setup the certficate callbacks
_con.SessionOptions.SecureSocketLayer = false;
_con.SessionOptions.VerifyServerCertificate = null;
_con.SessionOptions.QueryClientCertificate = null;
_con.AuthType = AuthType.Basic;
// try the connection
_con.Bind(new NetworkCredential(connectString, password));
_con.Dispose();
authenticated = true;
}
catch (DirectoryServicesCOMException cex)
{
//not authenticated; reason why is in cex
authenticated = false;
}
catch (Exception ex)
{
//not authenticated due to some other exception
authenticated = false;
}
finally
{
_con.Dispose();
_con = null;
}
return authenticated;
}
#endregion
}
}
And then install it in the GAC of the web server:
Now all that you have to do is use this configuration (note that you will need to update the Public Key Token to match your own signed assembly):
<membershipdefaultProvider="AspNetSunOneMembershipProvider">
<providers>
<addname="AspNetSunOneMembershipProvider"
type="SunOne.AspNetSunOneMembershipProvider,
SunOne, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx"
ApplicationName="/myApplication"
LDAPServer="SunOne.Domain.com"
UserNameConnectString="uid={0}, ou=People, ou=myApplication, dc=myContext,dc=com"/>
</providers>
</membership>
When determining your connection string, the LDP tool is invaluable when testing your LDAP Bindings. Check it out
It is contained in the Windows XP Service Pack 2 Support tools: