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.