Sunday, September 27, 2009

Updated Custom SharePoint Membership/Role Provider

 

Note:  This is an update to my previous post describing a custom SharePoint Membership/Role Provider.  I have left the other post intact in the event you need to see what I did that didn’t work.  This solution seems to work better.

The Problem

Wiring up a membership provider in SharePoint is fairly straight forward.  Especially if you have worked with .net membership providers in the past.  A quick Google search for SharePoint custom membership provider will return numerous examples on how to get this going.

We had a need to use an ASP.NET SQL Membership Provider to expose a secure SharePoint site externally to people who did not have an account in our Domain.  The problem was, people with domain accounts also needed to be able to access this sight externally and we didn’t want our users to have to remember two usernames and passwords.  Isn’t that thoughtful of us?  In an asp.net web application this would not be a big deal.  Define two membership providers in the web.config, set one as the default and if authentication failed on the default provider it would automatically try the other providers.  It doesn’t exactly work that way in SharePoint.  Apparently SharePoint is only aware of one membership provider.  We wired up a asp.net sql membership provider and we were able to make SharePoint authenticate to that, then we changed it to the built in SharePoint LDAP membership provider and we were able to authenticate against our Domain.  We just couldn’t get both to work simultaneously.

The Solution

After mucking around with config files for an entire day, it became very evident that to accomplish our goal, it would require a custom membership and role provider that would try to authenticate to the sql database and if that failed, hit the domain for authentication.  If they both fail, well, you’re not supposed to be here, go away.  As I mentioned earlier, the built in .net SQL membership provider worked well and the built in SharePoint LDAP membership provider worked well on it’s own.  However, when I tried to programmatically instantiate the SharePoint LDAP Provider, there were a couple of methods that I could not get to work.  Apparently this class wasn’t meant to used in another provider.  Our custom membership provider would inherit the SQLMemberShipProvider and SQLRoleProvider classes, so that would be our base class and our first attempt at authentication.  We would then programmatically instantiate an instance of a custom developed LDAPMembershipProvider and LDAPRoleProvider if the first attempt at authentication failed and give that a try.  Below is the code for our custom providers.

Custom Membership Provider

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Security;
using Microsoft.SharePoint;
using System.DirectoryServices.AccountManagement;
using System.Collections.Specialized;


namespace FBA.Extranet.MembershipProvider
{
    public class FBAMembershipProvider : SqlMembershipProvider
    {

        public override string ApplicationName
        {
            get
            {
                return SPContext.Current.Site.ID.ToString();
            }
        }

        private FBALDAPMembershipProvider _fbaLDAPProvider = null;
        private FBALDAPMembershipProvider FBALDAPProvider
        {
            get
            {
                if (_fbaLDAPProvider == null)
                    _fbaLDAPProvider = new FBALDAPMembershipProvider();
                return _fbaLDAPProvider;
            }
        }

        public override bool ValidateUser(string username, string password)
        {
            if (base.ValidateUser(username, password))
                return true;
            else if (FBALDAPProvider.ValidateUser(username, password))
                return true;
            else
                return false;
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            if (base.GetUser(username, userIsOnline) != null)
                return base.GetUser(username, userIsOnline);
            else
                return FBALDAPProvider.GetUser(username, userIsOnline);
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            if (base.GetUser(providerUserKey, userIsOnline) != null)
                return base.GetUser(providerUserKey, userIsOnline);
            else
                return FBALDAPProvider.GetUser(providerUserKey, userIsOnline);
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            if (base.FindUsersByEmail(emailToMatch, pageIndex, pageSize, out totalRecords).Count > 0)
                return base.FindUsersByEmail(emailToMatch, pageIndex, pageSize, out totalRecords);
            else
                return FBALDAPProvider.FindUsersByEmail(emailToMatch, pageIndex, pageSize, out totalRecords);
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            if (base.FindUsersByName(usernameToMatch, pageIndex, pageSize, out totalRecords).Count > 0)
                return base.FindUsersByName(usernameToMatch, pageIndex, pageSize, out totalRecords);
            else
                return FBALDAPProvider.FindUsersByName(usernameToMatch, pageIndex, pageSize, out totalRecords);
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            if (base.GetAllUsers(pageIndex, pageSize, out totalRecords).Count > 0)
                return base.GetAllUsers(pageIndex, pageSize, out totalRecords);
            else
                return FBALDAPProvider.GetAllUsers(pageIndex, pageSize, out totalRecords);

        }

        public override string GetUserNameByEmail(string email)
        {
            if (!string.IsNullOrEmpty(base.GetUserNameByEmail(email)))
                return base.GetUserNameByEmail(email);
            else
                return FBALDAPProvider.GetUserNameByEmail(email);

        }

    }
}

Custom Role Provider

using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using System.Linq;
using System.Text;
using System.Web.Security;
using Microsoft.SharePoint;

namespace FBA.Extranet.MembershipProvider
{
    public class FBARoleProvider : SqlRoleProvider
    {
        public override string ApplicationName
        {
            get
            {
                return SPContext.Current.Site.ID.ToString();
            }
        }

        private FBALDAPRoleProvider _fbaLDAPRoleProvider = null;
        private FBALDAPRoleProvider FBALDAPRoleProvider
        {
            get
            {
                if (_fbaLDAPRoleProvider == null)
                    _fbaLDAPRoleProvider = new FBALDAPRoleProvider();
                return _fbaLDAPRoleProvider;
            }
        }

        public override string[] GetRolesForUser(string username)
        {
            List<string> test = new List<string>();
            test.AddRange(base.GetRolesForUser(username));
            test.AddRange(FBALDAPRoleProvider.GetRolesForUser(username));
            return test.ToArray();
        }

        public override bool RoleExists(string roleName)
        {
            if (base.RoleExists(roleName))
                return true;
            else
                return FBALDAPRoleProvider.RoleExists(roleName);
        }

        public override string[] GetUsersInRole(string roleName)
        {
            if (base.GetUsersInRole(roleName).Length > 0)
                return base.GetUsersInRole(roleName);
            else
                return FBALDAPRoleProvider.GetUsersInRole(roleName);
          
        }
    }
}

Custom LDAP Membership Provider

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.DirectoryServices.AccountManagement;
using Microsoft.SharePoint;
using System.Web.Security;

namespace FBA.Extranet.MembershipProvider
{
    public class FBALDAPMembershipProvider : System.Web.Security.MembershipProvider
    {

        private string _applicationName = string.Empty;
        public override string ApplicationName
        {
            get
            {
                if (_applicationName == string.Empty)
                {
                    _applicationName = SPContext.Current.Site.ID.ToString();
                }
                return _applicationName;
            }
            set
            {
                this._applicationName = SPContext.Current.Site.ID.ToString();
            }
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            try
            {
                MembershipUserCollection coll = new MembershipUserCollection();
                int resultRecords = 0;
                int startIndex = pageIndex * pageSize;
                int endIndex = startIndex + pageSize;
                SPSecurity.RunWithElevatedPrivileges(new SPSecurity.CodeToRunElevated(delegate()
                {
                    using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
                    {
                        using (UserPrincipal userFilter = new UserPrincipal(context))
                        {
                            userFilter.EmailAddress = emailToMatch.Replace("%", "*");
                            using (PrincipalSearcher search = new PrincipalSearcher(userFilter))
                            {
                                PrincipalSearchResult<Principal> users = search.FindAll();
                                resultRecords = users.Count();
                                if (resultRecords < endIndex + 1)
                                {
                                    startIndex = 0;
                                    endIndex = resultRecords - 1;
                                }
                                for (int i = startIndex; i <= endIndex; i++)
                                {
                                    coll.Add(GetMembershipUser(users.ElementAt(i) as UserPrincipal));
                                }
                            }
                        }
                    }
                }));
                totalRecords = resultRecords;
                return coll;
            }
            catch (Exception ex)
            {
                //TODO: Log Something
                totalRecords = 0;
                return null;
            }
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            try
            {
                MembershipUserCollection coll = new MembershipUserCollection();
                int resultRecords = 0;
                int startIndex = pageIndex * pageSize;
                int endIndex = startIndex + pageSize;
                SPSecurity.RunWithElevatedPrivileges(new SPSecurity.CodeToRunElevated(delegate()
                {
                    using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
                    {
                        using (UserPrincipal userFilter = new UserPrincipal(context))
                        {
                            userFilter.SamAccountName = usernameToMatch.Replace("%", "*");
                            using (PrincipalSearcher search = new PrincipalSearcher(userFilter))
                            {
                                PrincipalSearchResult<Principal> users = search.FindAll();
                                resultRecords = users.Count();
                                if (resultRecords < endIndex + 1)
                                {
                                    startIndex = 0;
                                    endIndex = resultRecords - 1;
                                }
                                for (int i = startIndex; i <= endIndex; i++)
                                {
                                    coll.Add(GetMembershipUser(users.ElementAt(i) as UserPrincipal));
                                }
                            }
                        }
                    }
                }));
                totalRecords = resultRecords;
                return coll;
            }
            catch (Exception ex)
            {
                //TODO: Log Something.
                totalRecords = 0;
                return null;
            }
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            try
            {
                MembershipUserCollection memUserCollection = new MembershipUserCollection();
                int resultRecords = 0;
                int startIndex = pageIndex * pageSize;
                int endIndex = startIndex + pageSize;
                SPSecurity.RunWithElevatedPrivileges(new SPSecurity.CodeToRunElevated(delegate()
                {
                    using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
                    {
                        using (UserPrincipal userFilter = new UserPrincipal(context))
                        {
                            using (PrincipalSearcher search = new PrincipalSearcher(userFilter))
                            {
                                PrincipalSearchResult<Principal> user = search.FindAll();
                                resultRecords = user.Count();
                                if (resultRecords < endIndex + 1)
                                {
                                    startIndex = 0;
                                    endIndex = resultRecords - 1;
                                }
                                for (int i = startIndex; i <= endIndex; i++)
                                {
                                    memUserCollection.Add(GetMembershipUser(user.ElementAt(i) as UserPrincipal));
                                }
                            }
                        }
                    }
                }));
                totalRecords = resultRecords;
                return memUserCollection;
            }
            catch (Exception ex)
            {
                //Log something.
                totalRecords = 0;
                return null;
            }
            
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            try
            {
                MembershipUser memUser = null;
                SPSecurity.RunWithElevatedPrivileges(new SPSecurity.CodeToRunElevated(delegate()
                {
                    using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
                    {
                        using (UserPrincipal userFilter = new UserPrincipal(context))
                        {
                            userFilter.SamAccountName = username.Replace("%", "*");
                            using (PrincipalSearcher search = new PrincipalSearcher(userFilter))
                            {
                                UserPrincipal user = search.FindOne() as UserPrincipal;
                                if (!(user == null))
                                {
                                    memUser = GetMembershipUser(user);
                                }

                            }
                        }
                    }
                }));
                return memUser;
            }
            catch (Exception ex)
            {
                //Log something.
                return null;
            }
        }

        public override string GetUserNameByEmail(string email)
        {
            try
            {
                string username = string.Empty;
                SPSecurity.RunWithElevatedPrivileges(new SPSecurity.CodeToRunElevated(delegate()
                {
                    using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
                    {
                        using (UserPrincipal userFilter = new UserPrincipal(context))
                        {
                            userFilter.EmailAddress = email;
                            using (PrincipalSearcher search = new PrincipalSearcher(userFilter))
                            {
                                UserPrincipal user = search.FindOne() as UserPrincipal;
                                if (!(user == null))
                                {
                                    username = user.SamAccountName;
                                }
                            }
                        }
                    }
                }));
                return username;
            }
            catch (Exception ex)
            {
                //log something.
                return string.Empty;
            }
        }

        public override bool ValidateUser(string username, string password)
        {
            bool isValid = false;
            try
            {
                SPSecurity.RunWithElevatedPrivileges(new SPSecurity.CodeToRunElevated(delegate()
                {
                    using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
                    {
                        isValid = context.ValidateCredentials(username, password);
                    }
                }));
                
            }
            catch (Exception ex)
            {
                //log something.
                isValid = false;
            }
            return isValid;
        }

        private MembershipUser GetMembershipUser(UserPrincipal user)
        {
            return new MembershipUser("FBAMemberProvider",
                user.SamAccountName, user.DistinguishedName,
                user.EmailAddress, string.Empty,
                string.Empty, user.Enabled.Value, !user.Enabled.Value,
                DateTime.MinValue, DateTime.Now, DateTime.Now,
                DateTime.MinValue, DateTime.MinValue);
        }

        #region Not Implemented Methods
        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            throw new NotImplementedException();
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            throw new NotImplementedException();
        }

        public override System.Web.Security.MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out System.Web.Security.MembershipCreateStatus status)
        {
            throw new NotImplementedException();
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            throw new NotImplementedException();
        }

        public override bool EnablePasswordReset
        {
            get { throw new NotImplementedException(); }
        }

        public override bool EnablePasswordRetrieval
        {
            get { throw new NotImplementedException(); }
        }

        public override int GetNumberOfUsersOnline()
        {
            throw new NotImplementedException();
        }

        public override string GetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            throw new NotImplementedException();
        }

        public override int MaxInvalidPasswordAttempts
        {
            get { throw new NotImplementedException(); }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { throw new NotImplementedException(); }
        }

        public override int MinRequiredPasswordLength
        {
            get { throw new NotImplementedException(); }
        }

        public override int PasswordAttemptWindow
        {
            get { throw new NotImplementedException(); }
        }

        public override System.Web.Security.MembershipPasswordFormat PasswordFormat
        {
            get { throw new NotImplementedException(); }
        }

        public override string PasswordStrengthRegularExpression
        {
            get { throw new NotImplementedException(); }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get { throw new NotImplementedException(); }
        }

        public override bool RequiresUniqueEmail
        {
            get { throw new NotImplementedException(); }
        }

        public override string ResetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }

        public override bool UnlockUser(string userName)
        {
            throw new NotImplementedException();
        }

        public override void UpdateUser(System.Web.Security.MembershipUser user)
        {
            throw new NotImplementedException();
        }
        #endregion
    }
}

Custom LDAP Role Provider

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.DirectoryServices.AccountManagement;
using System.Web.Security;
using Microsoft.SharePoint;

namespace FBA.Extranet.MembershipProvider
{
    public class FBALDAPRoleProvider : RoleProvider
    {

        private string _applicationName = string.Empty;
        public override string ApplicationName
        {
            get
            {
                if (_applicationName == string.Empty)
                {
                    _applicationName = SPContext.Current.Site.ID.ToString();
                }
                return _applicationName;
            }
            set
            {
                this._applicationName = SPContext.Current.Site.ID.ToString();
            }
        }

        public override string[] GetRolesForUser(string username)
        {
            try
            {
                List<string> userGroups = new List<string>();
                SPSecurity.RunWithElevatedPrivileges(new SPSecurity.CodeToRunElevated(delegate()
                {
                    using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
                    {
                        using (UserPrincipal user = UserPrincipal.FindByIdentity(context, username))
                        {
                            if (!(user == null))
                            {
                                PrincipalSearchResult<Principal> results = user.GetAuthorizationGroups();
                                foreach (GroupPrincipal group in results)
                                {
                                    userGroups.Add(group.SamAccountName);
                                }
                            }
                        }
                    }
                }));
                return userGroups.ToArray();
            }
            catch (Exception ex)
            {
                //Log Something
                return null;
            }
        }

        public override string[] GetUsersInRole(string roleName)
        {
            try
            {
                List<string> groupMembers = new List<string>();
                SPSecurity.RunWithElevatedPrivileges(new SPSecurity.CodeToRunElevated(delegate()
                {
                    using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
                    {
                        using (GroupPrincipal groupFilter = new GroupPrincipal(context))
                        {
                            groupFilter.SamAccountName = roleName;
                            foreach (GroupPrincipal group in groupFilter.GetMembers(true))
                            {
                                groupMembers.Add(group.SamAccountName);
                            }
                        }
                    }
                }));
                return groupMembers.ToArray();
            }
            catch (Exception ex)
            {
                //Log something.
                return null;
            }
        }

        public override bool RoleExists(string roleName)
        {
            try
            {
                bool result = false;
                SPSecurity.RunWithElevatedPrivileges(new SPSecurity.CodeToRunElevated(delegate()
                {
                    using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
                    {
                        using (GroupPrincipal groupFilter = GroupPrincipal.FindByIdentity(context, IdentityType.SamAccountName, roleName))
                        {
                            if (groupFilter != null)
                            {
                                result = groupFilter.IsSecurityGroup.Value;
                            }
                        }
                    }
                }));
                return result;
            }
            catch (Exception ex)
            {
                //Log something.
                return false;
            }
        }

        #region Not Implemented Methods
        public override void AddUsersToRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        public override void CreateRole(string roleName)
        {
            throw new NotImplementedException();
        }

        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            throw new NotImplementedException();
        }

        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            throw new NotImplementedException();
        }

        public override string[] GetAllRoles()
        {
            throw new NotImplementedException();
        }

        public override bool IsUserInRole(string username, string roleName)
        {
            throw new NotImplementedException();
        }

        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

Implementation

Now that we have the code for the role provider it is a simple matter of deploying the .dll to the GAC and referencing the membership and role provider in the web.config.  You also need to do all the necessary configurations in SharePoint Central Admin, but as I said, that information is readily available via google.  The entries for the web.config are as follows:

Connection String
<connectionStrings>
  <add name="FBAMembershipConnectionString" connectionString="Data Source=.\OfficeServers;Initial Catalog=FBASQLMembership; Integrated Security=True" />
</connectionStrings>
Providers
<!-- membership provider -->
<membership defaultProvider="FBAMemberProvider">
  <providers>
    <add name="FBAMemberProvider" connectionStringName="FBAMembershipConnectionString" 
         enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" passwordFormat="Hashed" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="1" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" passwordStrengthRegularExpression="" type="FBA.Extranet.MembershipProvider.FBAMembershipProvider,FBA.Extranet.MembershipProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=<token>" />
  </providers>
</membership>
<!-- role provider -->
<roleManager enabled="true" defaultProvider="FBARoleProvider">
  <providers>
    <add connectionStringName="FBAMembershipConnectionString" name="FBARoleProvider" type="FBA.Extranet.MembershipProvider.FBARoleProvider,FBA.Extranet.MembershipProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=<token>" />
  </providers>
</roleManager>

Conclusion

I hope this post helps someone avoid some frustration and I look forward to your comments.

Friday, September 4, 2009

Custom SharePoint Membership/Role Provider

 

Update 09/27/2009:  I left this post here for reference but I discovered a couple of problems with this solution.  I have posted an update to this post that describes our final solution. Updated Custom Sharepoint Membership/Role Provider

The Problem

Wiring up a membership provider in SharePoint is fairly straight forward.  Especially if you have worked with .net membership providers in the past.  A quick Google search for SharePoint custom membership provider will return numerous examples on how to get this going.

We had a need to use an ASP.NET SQL Membership Provider to expose a secure SharePoint site externally to people who did not have an account in our Domain.  The problem was, people with domain accounts also needed to be able to access this sight externally and we didn’t want our users to have to remember two usernames and passwords.  Isn’t that thoughtful of us?  In an asp.net web application this would not be a big deal.  Define two membership providers in the web.config, set one as the default and if authentication failed on the default provider it would automatically try the other providers.  It doesn’t exactly work that way in SharePoint.  Apparently SharePoint is only aware of one membership provider.  We wired up a asp.net sql membership provider and we were able to make SharePoint authenticate to that, then we changed it to the built in SharePoint LDAP membership provider and we were able to authenticate against our Domain.  We just couldn’t get both to work simultaneously.

The Solution

After mucking around with config files for an entire day, it became very evident that to accomplish our goal, it would require a custom membership and role provider that would try to authenticate to the sql database and if that failed, hit the domain for authentication.  If they both fail, well, you’re not supposed to be here, go away.  As I mentioned earlier, the built in .net SQL membership provider worked well and the built in SharePoint LDAP membership provider worked well.  Our custom membership provider would inherit the SQLMemberShipProvider and SQLRoleProvider classes, so that would be our base class and our first attempt at authentication.  We would then programmatically instantiate an instance of the SharePoint LDAPMembershipProvider and LDAPRoleProvider if the first attempt at authentication failed and give that a try.  Below is the code for our custom providers.

Custom Membership Provider

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Security;
using Microsoft.SharePoint;
using System.DirectoryServices.AccountManagement;


namespace FBA.Extranet.MembershipProvider
{
    // Notice that our custom Forms Based Authentication Membership Provider inherits the .net SQLMembershipProvider.  This means that if we override no methods our provider will behave exactly as a sqlmembershipprovider.
    public class FBAMembershipProvider : SqlMembershipProvider
    {
        //In our case we overrode the ApplicationName property to always return the ID of the SiteCollection we were running in.  In SharePoint this will allow us to use this custom membership provider on multiple site collections if necessary and maintain all of the user information in one ASP.NET SQL Membership Database.
        public override string ApplicationName
        {
            get
            {
                return SPContext.Current.Site.ID.ToString();
            }
        }

        //This is where we programmatically instantiate the SharePoint LDAP Membership provider.
        private Microsoft.Office.Server.Security.LdapMembershipProvider _ldapProvider = null;
        private Microsoft.Office.Server.Security.LdapMembershipProvider ldapProvider
        {
            get
            {
                if (_ldapProvider == null)
                {
                    _ldapProvider = new Microsoft.Office.Server.Security.LdapMembershipProvider();
                    System.Collections.Specialized.NameValueCollection nvc = new System.Collections.Specialized.NameValueCollection();
                    nvc.Add("server", "domain.com");
                    nvc.Add("useSSL", "false");
                    nvc.Add("userDNAttribute", "distinguishedName");
                    nvc.Add("userNameAttribute", "sAMAccountName");
                    nvc.Add("userContainer", "DC=domain,DC=com");
                    nvc.Add("userObjectClass", "person");
                    nvc.Add("userFilter", "(|(ObjectCategory=group)(ObjectClass=person))");
                    nvc.Add("scope", "Subtree");
                    nvc.Add("otherRequiredUserAttributes", "sn,givenname,cn");
                    _ldapProvider.Initialize("LdapMemberProvider", nvc);
                }
                return _ldapProvider;
            }

        }

        //Now we override the necessary methods that SharePoint needs.  As you can see, each method attempts to perform its base functionality before trying to authenticate via the LDAP membership provider.
        public override bool ValidateUser(string username, string password)
        {
            if (base.ValidateUser(username, password))
                return true;
            else if (ldapProvider.ValidateUser(username, password))
                return true;
            else
                return false;
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            if (base.FindUsersByEmail(emailToMatch, pageIndex, pageSize, out totalRecords).Count > 0)
                return base.FindUsersByEmail(emailToMatch, pageIndex, pageSize, out totalRecords);
            else if (ldapProvider.FindUsersByEmail(emailToMatch, pageIndex, pageSize, out totalRecords).Count > 0)
                return ldapProvider.FindUsersByEmail(emailToMatch, pageIndex, pageSize, out totalRecords);
            else
                return new MembershipUserCollection();
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            if (base.FindUsersByName(usernameToMatch, pageIndex, pageSize, out totalRecords).Count > 0)
                return base.FindUsersByName(usernameToMatch, pageIndex, pageSize, out totalRecords);
            else if (ldapProvider.FindUsersByName(usernameToMatch, pageIndex, pageSize, out totalRecords).Count > 0)
                return ldapProvider.FindUsersByName(usernameToMatch, pageIndex, pageSize, out totalRecords);
            else
                return new MembershipUserCollection();    
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            if (base.GetAllUsers(pageIndex, pageSize, out totalRecords).Count > 0)
                return base.GetAllUsers(pageIndex, pageSize, out totalRecords);
            else if (ldapProvider.GetAllUsers(pageIndex, pageSize, out totalRecords).Count > 0)
                return ldapProvider.GetAllUsers(pageIndex, pageSize, out totalRecords);
            else
                return new MembershipUserCollection();    
        }

        public override string GetUserNameByEmail(string email)
        {
            if(!string.IsNullOrEmpty(base.GetUserNameByEmail(email)))
                return base.GetUserNameByEmail(email);
            else if (!string.IsNullOrEmpty(ldapProvider.GetUserNameByEmail(email)))
                return ldapProvider.GetUserNameByEmail(email);
            else
                return string.Empty;
        }
       
    }
}

Custom Role Provider

The role provider is implemented exactly as the membership provider was.

using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using System.Linq;
using System.Text;
using System.Web.Security;
using Microsoft.SharePoint;

namespace FBA.Extranet.MembershipProvider
{
    public class FBARoleProvider : SqlRoleProvider
    {
        public override string ApplicationName
        {
            get
            {
                return SPContext.Current.Site.ID.ToString();
            }
        }

        private Microsoft.Office.Server.Security.LdapRoleProvider _ldapProvider = null;
        private Microsoft.Office.Server.Security.LdapRoleProvider ldapProvider
        {
            get
            {
                if (_ldapProvider == null)
                {
                    _ldapProvider = new Microsoft.Office.Server.Security.LdapRoleProvider();
                    System.Collections.Specialized.NameValueCollection nvc = new System.Collections.Specialized.NameValueCollection();
                    nvc.Add("server", "domain.com");
                    nvc.Add("useSSL", "false");
                    nvc.Add("groupContainer", "DC=domain,DC=com");
                    nvc.Add("groupNameAttribute", "cn");
                    nvc.Add("groupMemberAttribute", "member");
                    nvc.Add("userNameAttribute", "sAMAccountName");
                    nvc.Add("dnAttribute", "distinguishedName");
                    nvc.Add("groupFilter", "(ObjectClass=group)");
                    nvc.Add("scope", "Subtree");
                    _ldapProvider.Initialize("LdapRoleProvider", nvc);
                }
                return _ldapProvider;
            }

        }

        public override string[] GetRolesForUser(string username)
        {
            List<string> test = new List<string>();
            test.AddRange(base.GetRolesForUser(username));
            test.AddRange(ldapProvider.GetRolesForUser(username));
            return test.ToArray();
        }

        public override bool RoleExists(string roleName)
        {
            if (base.RoleExists(roleName))
                return true;
            else if (ldapProvider.RoleExists(roleName))
                return true;
            else
                return false;

        }
    }
}

Implementation

Now that we have the code for the role provider it is a simple matter of deploying the .dll to the GAC and referencing the membership and role provider in the web.config.  You also need to do all the necessary configurations in SharePoint Central Admin, but as I said, that information is readily available via google.  The entries for the web.config are as follows:

Connection String
<connectionStrings>
  <add name="FBAMembershipConnectionString" connectionString="Data Source=.\OfficeServers;Initial Catalog=FBASQLMembership; Integrated Security=True" />
</connectionStrings>
Providers
<!-- membership provider -->
<membership defaultProvider="FBAMemberProvider">
  <providers>
    <add name="FBAMemberProvider" connectionStringName="FBAMembershipConnectionString" 
         enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" passwordFormat="Hashed" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="1" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" passwordStrengthRegularExpression="" type="FBA.Extranet.MembershipProvider.FBAMembershipProvider,FBA.Extranet.MembershipProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=<token>" />
  </providers>
</membership>
<!-- role provider -->
<roleManager enabled="true" defaultProvider="FBARoleProvider">
  <providers>
    <add connectionStringName="FBAMembershipConnectionString" name="FBARoleProvider" type="FBA.Extranet.MembershipProvider.FBARoleProvider,FBA.Extranet.MembershipProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=<token>" />
  </providers>
</roleManager>

Conclusion

I hope this post helps someone avoid some frustration and I look forward to your comments.