Just to quickly wrap up what we’ve done so far:
We created new project using ASP.NET MVC 2 Web Application to make use of the layout (login pages, master page, login controls) and speed up our development.
We created Custom Membership Provider and we implemented custom logic into the ValidateUser method to show how we can override Membership Provider’s methods.
In this part of the tutorial we’re going to implement a database, create UserRepository class which will contain data logic (to keep the provider as simple as possible and to leave Entity Framework model untouched so we can make modifications to the database without breaking things), and implement some of the Membership Provider’s basic methods.
First of all, I would like to say that this is first time I’m doing this too. Although I’m few steps ahead of the tutorial I’m open to suggestions on how the implementation can be improved (both, coding and the way it works).
While I’m at it, I’ll just quickly (sorry for all the delay) mention where I see this tutorial going in the next few parts.
We’re going to implement all default Membership Provider methods to make it tidy. Even if it means just returning hard-coded value like password format (for example: we don’t need the option between text/hashed passwords).
We will also implement email validation system that will request clicking auto-generated link in an email to register, reset password and change email address.
After we’ve done that, we will implement Role Provider and we will create admin section of the application.
So, coming back to this part of the tutorial, first we will need a database table to store all membership information. The table I’m proposing is basically a hybrid of
aspnet_membership table from ASPNETDB database and
Userstable from
Tank_Auth authentication library for CodeIgniter PHP framework (Check out the following link on Stack Overflow to find out more about Tank_Auth, it’s very informative –
What Code Igniter authentication library is best? – Stack Overflow).
Let’s start by adding the database to our project. Right click App_Data folder, Add -> New Item…, Select SQL Server Database, call it CustomMembership.mdf and click Add.
In Database Explorer, right click on the Tables node and Add New Table. Create the following table:
Make UserId column a Primary Key and an Identity Column and save the table with the name Users.
Back in the Solution Explorer, right click Models folder Add -> New Item…, Select ADO.NET Entity Data Model, call it CustomMembership.edmx and click Add.
Select Generate from Database and click Next.
Change the connection name to CustomMembershipDB and click Next.
Select Tables, change Model Namespace to CustomMembership.Models and Finish the wizard.
Back in Solution Explorer open Web.config file and delete default ASPNETDB connection. We had to keep it there because Membership Provider needs connectionStringName parameter. Now, we have our DB connection which by the way Membership Provider is not going to use directly anyway, we can delete the ASPNETDB connection.
Change connectionStringName parameter value in Membership, Role and Profile provider nodes in Web.configfile like on the screenshot below.
We can quickly run our application to confirm that we didn’t break anything… yet.
While we have the application running see what happens if we click Register link on the login page.
We could just hard-code MinRequiredPasswordLength propery into Custom Membership Provider if we wanted to.
What happens is that when the Membership Provider is initialized, the Initialize() method is called. It readsWeb.config file and sets Membership Provider object’s properties. So, lets implement Initialize method inMyMembershipProvider class
First, lets add a reference to System.Collections.Specialized assembly:
using System.Collections.Specialized;
Next, create helper function which we will use to read config values.
//
// A helper function to retrieve config values from the configuration file.
//
private string GetConfigValue(string configValue, string defaultValue)
{
if (string.IsNullOrEmpty(configValue))
return defaultValue;
return configValue;
}
Next, lets add properties from Web.config file. We’re going to hard code some of the provider’s default properties. We will want unique email and hashed password and we don’t want to let users to retrieve their passwords (not even, using QuestionAndAnswer)
//
// Properties from web.config, default all to False
//
private string _ApplicationName;
private bool _EnablePasswordReset;
private bool _EnablePasswordRetrieval = false;
private bool _RequiresQuestionAndAnswer = false;
private bool _RequiresUniqueEmail = true;
private int _MaxInvalidPasswordAttempts;
private int _PasswordAttemptWindow;
private int _MinRequiredPasswordLength;
private int _MinRequiredNonalphanumericCharacters;
private string _PasswordStrengthRegularExpression;
private MembershipPasswordFormat _PasswordFormat = MembershipPasswordFormat.Hashed;
And finally, lets add Initialize method.
public override void Initialize(string name, NameValueCollection config)
{
if (config == null)
throw new ArgumentNullException("config");
if (name == null || name.Length == 0)
name = "CustomMembershipProvider";
if (String.IsNullOrEmpty(config["description"]))
{
config.Remove("description");
config.Add("description", "Custom Membership Provider");
}
base.Initialize(name, config);
_ApplicationName = GetConfigValue(config["applicationName"],
System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
_MaxInvalidPasswordAttempts = Convert.ToInt32(
GetConfigValue(config["maxInvalidPasswordAttempts"], "5"));
_PasswordAttemptWindow = Convert.ToInt32(
GetConfigValue(config["passwordAttemptWindow"], "10"));
_MinRequiredNonalphanumericCharacters = Convert.ToInt32(
GetConfigValue(config["minRequiredNonalphanumericCharacters"], "1"));
_MinRequiredPasswordLength = Convert.ToInt32(
GetConfigValue(config["minRequiredPasswordLength"], "6"));
_EnablePasswordReset = Convert.ToBoolean(
GetConfigValue(config["enablePasswordReset"], "true"));
_PasswordStrengthRegularExpression = Convert.ToString(
GetConfigValue(config["passwordStrengthRegularExpression"], ""));
}
I think the function is pretty much self explanatory. If you remove all unnecessary line breaks it will look much tidier.
We can edit Web.config‘s membership node and delete all the hardcoded parameters.
<membership defaultProvider="CustomMembershipProvider">
<providers>
<clear />
<add name="CustomMembershipProvider"
type="MyMembershipProvider"
connectionStringName="CustomMembershipDB"
enablePasswordReset="true"
maxInvalidPasswordAttempts="5"
minRequiredPasswordLength="6"
minRequiredNonalphanumericCharacters="0"
passwordAttemptWindow="10"
applicationName="/" />
</providers>
</membership>
Next, let’s implement the properties fully into our Custom Membership Provider.
Open MyMembershipProvider file and starting from the top edit properties and methods as below:
public override string ApplicationName
{
get { return _ApplicationName; }
set { _ApplicationName = value; }
}
Because we will not implement password reset using security question and answer we can change:
public override bool ChangePasswordQuestionAndAnswer(string username,
string password,
string newPasswordQuestion,
string newPasswordAnswer)
{
return false;
}
public override bool EnablePasswordReset
{
get { return _EnablePasswordReset; }
}
public override bool EnablePasswordRetrieval
{
get { return _EnablePasswordRetrieval; }
}
public override int MaxInvalidPasswordAttempts
{
get { return _MaxInvalidPasswordAttempts; }
}
public override int MinRequiredNonAlphanumericCharacters
{
get { return _MinRequiredNonalphanumericCharacters; }
}
public override int MinRequiredPasswordLength
{
get { return _MinRequiredPasswordLength; }
}
public override int PasswordAttemptWindow
{
get { return _PasswordAttemptWindow; }
}
public override MembershipPasswordFormat PasswordFormat
{
get { return _PasswordFormat; }
}
public override string PasswordStrengthRegularExpression
{
get { return _PasswordStrengthRegularExpression; }
}
public override bool RequiresQuestionAndAnswer
{
get { return _RequiresQuestionAndAnswer; }
}
public override bool RequiresUniqueEmail
{
get { return _RequiresUniqueEmail; }
}
At this point, we can run the application and go to Log In -> Register page and we will see:
with minimum password length being taken from Web.config file.
In case I’ve made some typos while copy and pasting the code here you can downloadMyMembershipProvider.cs file up to this point of the tutorial:
If we try to register new account we will get an error about CreateUser method not being implemented.
Before we implement CreateUser method we need to implement 2 other methods which are used by CreateUsermethod: GetUser and GetUsernameByEmail.
We need them both to check for existing usernames and email addresses in our database.
Let’s start by creating UserRepository class.
Right click Models folder, Add -> Class…, call it UserRepository and click Add.
In UserRepository class, add reference to System.Web.Security assembly:
using System.Web.Security;
We’re going to implement 3 methods at this stage:
CreateUser:
public MembershipUser CreateUser(string username, string password, string email)
{
using (CustomMembershipDB db = new CustomMembershipDB())
{
User user = new User();
user.UserName = username;
user.Email = email;
user.Password = password;
user.PasswordSalt = "1234";
user.CreatedDate = DateTime.Now;
user.IsActivated = false;
user.IsLockedOut = false;
user.LastLockedOutDate = DateTime.Now;
user.LastLoginDate = DateTime.Now;
db.AddToUsers(user);
db.SaveChanges();
return GetUser(username);
}
}
We can see that our UserRepository CreateUser method uses Entity Data Model (CustomMembershipDB) we created earlier. We will come back to hasing and salting the password in the next part of the tutorial.
It creates User object, saves it to DB and returns user object of MembershipUser type.
In case you’re wondering why we assign DateTime.Now values to LastLockedOutDate and LastLoginDate. This is because in default MembershipUser type these properties are not Nullable, which means that they can’t store NULL values. There is a way to implement DateTime? nullable type but for the simplicity we’ll just populate the fields with current time on user creation.
GetUserNameByEmail:
public string GetUserNameByEmail(string email)
{
using (CustomMembershipDB db = new CustomMembershipDB())
{
var result = from u in db.Users where (u.Email == email) select u;
if (result.Count() != 0)
{
var dbuser = result.FirstOrDefault();
return dbuser.UserName;
}
else
{
return "";
}
}
}
GetUser:
public MembershipUser GetUser(string username)
{
using (CustomMembershipDB db = new CustomMembershipDB())
{
var result = from u in db.Users where (u.UserName == username) select u;
if (result.Count() != 0)
{
var dbuser = result.FirstOrDefault();
string _username = dbuser.UserName;
int _providerUserKey = dbuser.UserId;
string _email = dbuser.Email;
string _passwordQuestion = "";
string _comment = dbuser.Comments;
bool _isApproved = dbuser.IsActivated;
bool _isLockedOut = dbuser.IsLockedOut;
DateTime _creationDate = dbuser.CreatedDate;
DateTime _lastLoginDate = dbuser.LastLoginDate;
DateTime _lastActivityDate = DateTime.Now;
DateTime _lastPasswordChangedDate = DateTime.Now;
DateTime _lastLockedOutDate = dbuser.LastLockedOutDate;
MembershipUser user = new MembershipUser("CustomMembershipProvider",
_username,
_providerUserKey,
_email,
_passwordQuestion,
_comment,
_isApproved,
_isLockedOut,
_creationDate,
_lastLoginDate,
_lastActivityDate,
_lastPasswordChangedDate,
_lastLockedOutDate);
return user;
}
else
{
return null;
}
}
}
Again, we can see that some of the properties get DateTime.Now value, and again this is becauseMembershipUser constructor needs the values and can’t accept NULLs.
So basically what the method does is; It connects to database and reads the user data; It then generates newMembershipUser object with properties from database and finally it returns the object.
UserRepository class can be downloaded here:
Now, let’s go back to MyMembershipProvider.cs, add the reference to CustomMembership.Models namespace
using CustomMembership.Models;
and implement the following methods:
GetUserNameByEmail:
public override string GetUserNameByEmail(string email)
{
UserRepository _user = new UserRepository();
return _user.GetUserNameByEmail(email);
}
GetUser(string username, bool userIsOnline) - (not the one with userProviderKey argument):
public override MembershipUser GetUser(string username, bool userIsOnline)
{
UserRepository _user = new UserRepository();
return _user.GetUser(username);
}
and finally CreateUser itself:
public override MembershipUser CreateUser(string username,
string password,
string email,
string passwordQuestion,
string passwordAnswer,
bool isApproved,
object providerUserKey,
out MembershipCreateStatus status)
{
ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username,
password,
true);
OnValidatingPassword(args);
if (args.Cancel)
{
status = MembershipCreateStatus.InvalidPassword;
return null;
}
if (RequiresUniqueEmail && GetUserNameByEmail(email) != "")
{
status = MembershipCreateStatus.DuplicateEmail;
return null;
}
MembershipUser u = GetUser(username, false);
if (u == null)
{
UserRepository _user = new UserRepository();
_user.CreateUser(username, password, email);
status = MembershipCreateStatus.Success;
return GetUser(username, false);
}
else
{
status = MembershipCreateStatus.DuplicateUserName;
}
return null;
}
MyMembershipProvider class up to this point in the tutorial can be downloaded here:
At this point, we should be able to register using the registration form of our application (Log In -> Regsiter – or /Account/Register link) and check directly in the database if the account has been created.
It would also be easy enough to modify ValidateUser method to validate against the database but because there is still few things we need to do during user creation process (hash the passwords and generate salt for example) we will leave it for the next part of the tutorial.
Continue to: