I am about to try and replace the built-in membership mechanism in Umbraco 7 with my own. The new mechanism is based on a very simple database structure in an MS Access Database (which will later on be replaced by an other, more solid external database.) So I have a table with usernames and passwords and want to use these for member authentication in v7.
I have been trying to find some good examples (with code) of how to implement this, but I seem to always end up with articles where people are having trouble;-)
Would anyone with experience in implementing this care to share some insight and code? I would prefer to go the RoleProvider/MembershipProvider way and avoid ASP.NET Identity, which is very new in Umbraco and seems to be an overkill for my limited project here.
I should also put forward a disclaimer that when I implemented membership it was not using MS Access so cannot really account for any of the odbc stuff in the examples provided.
I did not even have access to any concrete user store at all as I had to authenticate against an external service which was not identity aware and only provided a rest interface. For that reason my values for password retrieval and reset in the web config were both false.
Whilst all methods in the examples provided need to be present in your implementation as I found it is not necessary to implement logic for all of them to get a basic login system up an running, the main methods needing logic are
I actually figured out the basics myself, and I ended up with something slightly different from the Microsoft sample:
I inherited my RoleProvider and MembershipProvider from the Umbraco ones. Because, then I could "get away" with overriding just the methods that need my logic in them. Hence much less code, since my logic is very simple.
Also, instead of using ODBC directly, I used PetaPoco (included with Umbraco 7) and Repositories to access my database, which gives a much simpler code.
Here are some snippets:
UserRepository.cs
public class UserRepository
{
private readonly Database _database;
public UserRepository()
{
_database = new Database("NewPixDatabase");
}
public IList<User> GetAll()
{
return _database.Fetch<User>("SELECT * FROM [User] ORDER BY Username Desc");
}
public IList<User> GetByUsername(string username)
{
return _database.Fetch<User>(string.Format("SELECT * FROM [User] WHERE Username = '{0}' ORDER BY Username Desc", username));
}
public IList<User> GetByEmail(string email)
{
return _database.Fetch<User>(string.Format("SELECT * FROM [User] WHERE Email = '{0}' ORDER BY Username Desc", email));
}
public User GetUser(string username)
{
return _database.First<User>(string.Format("SELECT * FROM [User] WHERE Username = '{0}' ORDER BY Username Desc", username));
}
public void Update(User user)
{
_database.Update(user);
}
}
MyMembersMembershipProvider.cs
public class MyMembersMembershipProvider : Umbraco.Web.Security.Providers.MembersMembershipProvider
{
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
{
throw new NotImplementedException();
}
public override bool DeleteUser(string username, bool deleteAllRelatedData)
{
throw new NotImplementedException();
}
public override bool ValidateUser(string username, string password)
{
var userRepository = new NewPixEditCMSApp.UserRepository();
User user = userRepository.GetUser(username);
if (user != null)
{
if (password == user.Password)
return true;
}
return false;
}
MyMembersRoleProvider.cs
public class MyMembersRoleProvider : Umbraco.Web.Security.Providers.MembersRoleProvider
{
public override string[] GetAllRoles()
{
return new[] { "Standard" };
}
public override bool IsUserInRole(string username, string roleName)
{
if (roleName.ToLower() == "standard")
return true;
else
return base.IsUserInRole(username, roleName);
}
public override string[] GetRolesForUser(string username)
{
return new[] { "Standard" };
}
}
As one can see, the logic is extremely basic, only one role, "Standard", member login validation and the possibility to change the password.
And by the way, in case someone is struggling with the connection string for MS Access:
Turns out I was too quick in my conclusions. Inheriting from Umbraco.Web.Security.Providers.MembersMembershipProvider and trying to get away with overriding only a few methods has proved to be an insuffcient implementation. I discovered there were still "links" to the content of the integrated membership database in Umbraco. So f.ex. if I changed a username in my Access database, it did not apply. I would have to do the change in Umbraco as well (which already had "duplicates" of all the users).
Anyway, now I have ended up inheriting from System.Configuration.Provider.MembershipProvider as you do in your example. But PetaPoco still works well, so I thought I'd share my code in case anyone else is interested.
MyMembersMembershipProvider.cs:
namespace NewPixEditCMSApp
{
public class MyMembersMembershipProvider : MembershipProvider
{
private string _ApplicationName = "UmbracoMembershipProvider";
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
base.Initialize(name, config);
}
public override string ApplicationName
{
get { return _ApplicationName; }
set
{
if (string.IsNullOrEmpty(value))
throw new ProviderException("ApplicationName Cannot be Empty");
if (value.Length > 0x100)
throw new ProviderException("Provider application name too long");
_ApplicationName = value;
}
}
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 MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
{
var userRepository = new NewPixEditCMSApp.UserRepository();
if (!userRepository.CreateUser(username, password, email))
{
status = MembershipCreateStatus.UserRejected;
return null;
}
else
{
status = MembershipCreateStatus.Success;
return new MembershipUser(_ApplicationName, username, username, email, string.Empty, string.Empty, true, false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now);
}
}
public override bool DeleteUser(string username, bool deleteAllRelatedData)
{
var userRepository = new NewPixEditCMSApp.UserRepository();
return userRepository.DeleteUser(username);
}
public override bool EnablePasswordReset
{
get
{
return false;
}
}
public override bool EnablePasswordRetrieval
{
get
{
return true;
}
}
public override int GetNumberOfUsersOnline()
{
throw new NotImplementedException();
}
public override string GetPassword(string username, string answer)
{
var userRepository = new NewPixEditCMSApp.UserRepository();
User user = userRepository.GetUser(username);
if (user != null)
return user.Password;
else
return null;
}
public override int MaxInvalidPasswordAttempts
{
get
{
return 10;
}
}
public override int MinRequiredNonAlphanumericCharacters
{
get
{
return 0;
}
}
public override int MinRequiredPasswordLength
{
get
{
return 6;
}
}
public override int PasswordAttemptWindow
{
get { throw new NotImplementedException(); }
}
public override MembershipPasswordFormat PasswordFormat
{
get
{
return MembershipPasswordFormat.Clear;
}
}
public override string PasswordStrengthRegularExpression
{
get { throw new NotImplementedException(); }
}
public override bool RequiresQuestionAndAnswer
{
get
{
return false;
}
}
public override bool RequiresUniqueEmail
{
get
{
return true;
}
}
public override string ResetPassword(string username, string answer)
{
throw new NotImplementedException();
}
public override bool UnlockUser(string userName)
{
throw new NotImplementedException();
}
public override void UpdateUser(MembershipUser user)
{
var userRepository = new NewPixEditCMSApp.UserRepository();
User _user = new User() { Username = user.UserName, Password = user.GetPassword(), Email = user.Email, Company = user.Comment };
userRepository.Update(_user);
}
public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
{
var userRepository = new NewPixEditCMSApp.UserRepository();
IList<User> users = userRepository.GetAll();
totalRecords = users.Count;
MembershipUserCollection membershipUsers = new MembershipUserCollection();
int counter = 0;
int startIndex = pageSize * pageIndex;
int endIndex = startIndex + pageSize - 1;
foreach (User user in users)
{
if (counter >= startIndex)
{
MembershipUser membershipUser = new MembershipUser(_ApplicationName, user.Username, user.Username, user.Email, string.Empty, user.Company, true, false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now);
membershipUsers.Add(membershipUser);
}
if (counter >= endIndex)
break;
counter++;
}
return membershipUsers;
}
public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
{
var userRepository = new NewPixEditCMSApp.UserRepository();
IList<User> users = userRepository.GetByUsername(usernameToMatch);
totalRecords = users.Count;
MembershipUserCollection membershipUsers = new MembershipUserCollection();
int counter = 0;
int startIndex = pageSize * pageIndex;
int endIndex = startIndex + pageSize - 1;
foreach (User user in users)
{
if (counter >= startIndex)
{
MembershipUser membershipUser = new MembershipUser(_ApplicationName, user.Username, user.Username, user.Email, string.Empty, user.Company, true, false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now);
membershipUsers.Add(membershipUser);
}
if (counter >= endIndex)
break;
counter++;
}
return membershipUsers;
}
public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
{
var userRepository = new NewPixEditCMSApp.UserRepository();
IList<User> users = userRepository.GetByEmail(emailToMatch);
totalRecords = users.Count;
MembershipUserCollection membershipUsers = new MembershipUserCollection();
int counter = 0;
int startIndex = pageSize * pageIndex;
int endIndex = startIndex + pageSize - 1;
foreach (User user in users)
{
if (counter >= startIndex)
{
MembershipUser membershipUser = new MembershipUser(_ApplicationName, user.Username, user.Username, user.Email, string.Empty, user.Company, true, false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now);
membershipUsers.Add(membershipUser);
}
if (counter >= endIndex)
break;
counter++;
}
return membershipUsers;
}
public override bool ValidateUser(string username, string password)
{
var userRepository = new NewPixEditCMSApp.UserRepository();
User user = userRepository.GetUser(username);
if (user != null)
{
if (password == user.Password)
return true;
}
return false;
}
public override MembershipUser GetUser(string username, bool userIsOnline)
{
var userRepository = new NewPixEditCMSApp.UserRepository();
User user = userRepository.GetUser(username);
if (user != null)
return new MembershipUser(_ApplicationName, user.Username, user.Username, user.Email, string.Empty, user.Company, true, false, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now, DateTime.Now);
return null;
}
public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
{
return GetUser(providerUserKey as string, userIsOnline);
}
public override string GetUserNameByEmail(string email)
{
var userRepository = new NewPixEditCMSApp.UserRepository();
IList<User> users = userRepository.GetByEmail(email);
if (users.Count > 0)
return users[0].Username;
else
return null;
}
}
}
Notice that the ApplicationName is set to "UmbracoMembershipProvider", which is necessary so that my provider "replaces" the Umbraco provider properly.
Luckily, MyMembersRoleProvider still works fine inheriting from Umbraco.Web.Security.Providers.MembersRoleProvider.
Just thought I'd share an additional snag when using MS Access with PetaPoco: The DateTime data type.
To map a Date/Time Field in the Access database to a poco object member, you should use a nullable DateTime, i.e. DateTime?. Also, Access is picky about its value, for instance you cannot use DateTime.Now, you will get an OleDbException when Update'ing. Instead, assign your DateTime member like this:
new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, DateTime.Now.Minute, 0);
Strange, I know, but then again, MS Access is a strange animal...
Hi Norbert and really sorry for answering so late.
I've been busy building a new site for our company, and it is up and running now:-)
And I am using basically the same MembershipProvider code as before, so this should work on Umbraco 7.7.7 which I am on now. I had to do som SQL adjustments because we have migrated from MS Access to MS SQL Server, but the principles are all the same.
Just wanted to let you know if you are still working on this.
Custom membership provider
Hi,
I am about to try and replace the built-in membership mechanism in Umbraco 7 with my own. The new mechanism is based on a very simple database structure in an MS Access Database (which will later on be replaced by an other, more solid external database.) So I have a table with usernames and passwords and want to use these for member authentication in v7.
I have been trying to find some good examples (with code) of how to implement this, but I seem to always end up with articles where people are having trouble;-)
Would anyone with experience in implementing this care to share some insight and code? I would prefer to go the RoleProvider/MembershipProvider way and avoid ASP.NET Identity, which is very new in Umbraco and seems to be an overkill for my limited project here.
Bendik
Hi Bendik
You would need to create one class which inherits from MembershipProvider - see this for an example which may work for MS Access https://msdn.microsoft.com/en-us/library/6tc47t75.aspx
Another class if you want to implement roles. See here for an Odbc example https://msdn.microsoft.com/en-us/library/317sza4k.aspx
in your site web.config the 'membership' section might look like this.
The Important thing to note is that I found it easier to keep the name as 'UmbracoMembershipProvider' but change the type to your implementation
The roleManager section would then look like this.
I should also put forward a disclaimer that when I implemented membership it was not using MS Access so cannot really account for any of the odbc stuff in the examples provided.
I did not even have access to any concrete user store at all as I had to authenticate against an external service which was not identity aware and only provided a rest interface. For that reason my values for password retrieval and reset in the web config were both false.
Whilst all methods in the examples provided need to be present in your implementation as I found it is not necessary to implement logic for all of them to get a basic login system up an running, the main methods needing logic are
MembershipProvider: GetUser & ValidateUser
RoleProvider: GetAllRoles & GetRolesForUser
Hope that helps
Thanks Ian!
I actually figured out the basics myself, and I ended up with something slightly different from the Microsoft sample:
I inherited my RoleProvider and MembershipProvider from the Umbraco ones. Because, then I could "get away" with overriding just the methods that need my logic in them. Hence much less code, since my logic is very simple.
Also, instead of using ODBC directly, I used PetaPoco (included with Umbraco 7) and Repositories to access my database, which gives a much simpler code.
Here are some snippets:
UserRepository.cs
MyMembersMembershipProvider.cs
MyMembersRoleProvider.cs
As one can see, the logic is extremely basic, only one role, "Standard", member login validation and the possibility to change the password.
And by the way, in case someone is struggling with the connection string for MS Access:
Maybe this can help others trying to roll their own membership mechanism in Umbraco 7.
Good.to hear PetaPoco is ok for use with ms access i agree there was a hell of a lot of code in those methods otherwise
Turns out I was too quick in my conclusions. Inheriting from Umbraco.Web.Security.Providers.MembersMembershipProvider and trying to get away with overriding only a few methods has proved to be an insuffcient implementation. I discovered there were still "links" to the content of the integrated membership database in Umbraco. So f.ex. if I changed a username in my Access database, it did not apply. I would have to do the change in Umbraco as well (which already had "duplicates" of all the users).
Anyway, now I have ended up inheriting from System.Configuration.Provider.MembershipProvider as you do in your example. But PetaPoco still works well, so I thought I'd share my code in case anyone else is interested.
MyMembersMembershipProvider.cs:
Notice that the ApplicationName is set to "UmbracoMembershipProvider", which is necessary so that my provider "replaces" the Umbraco provider properly.
Luckily, MyMembersRoleProvider still works fine inheriting from Umbraco.Web.Security.Providers.MembersRoleProvider.
Just thought I'd share an additional snag when using MS Access with PetaPoco: The DateTime data type.
To map a Date/Time Field in the Access database to a poco object member, you should use a nullable DateTime, i.e. DateTime?. Also, Access is picky about its value, for instance you cannot use DateTime.Now, you will get an OleDbException when Update'ing. Instead, assign your DateTime member like this:
Strange, I know, but then again, MS Access is a strange animal...
Hey, just would like to ask if it still works on Umbraco 7.5 and upwards? Thanks a lot for this code!
Hi Norbert and really sorry for answering so late.
I've been busy building a new site for our company, and it is up and running now:-)
And I am using basically the same MembershipProvider code as before, so this should work on Umbraco 7.7.7 which I am on now. I had to do som SQL adjustments because we have migrated from MS Access to MS SQL Server, but the principles are all the same.
Just wanted to let you know if you are still working on this.
is working on a reply...