December 1, 2009

How can I customize my user model in my ASP.NET MVC web site? Part 3

Extending the MembershipProvider

Related Posts

So now that we’ve got our model set up and we’ve figured out which components need to be extended, where do we start?

The easiest place to start is your MembershipProvider – because this is the component that logs your user into your website. So let’s create our derived class and hook it into our ASP.NET MVC application:

public class MySiteMembershipProvider : MembershipProvider
{
}

Of course, we must implement all the abstract methods that are defined in the MembershipProvider base class, but I will come back to that momentarily. In order to tell our site that we need to use this membership provider we hook it up in the web.config:

So open your web.config and scroll down until you find the membership node of the system.web section. By default you will find a provider defined called AspNetSqlMembershipProvider… we can scrap that one because it doesn’t do what we want. We need to replace this with our derived type and give it a more befitting name – so, this is what I’ve got in my web.config:

<membership defaultProvider="MySiteMembershipProvider">
 <providers>
  <clear/>
  <add  applicationName="MySite"
    name="MySiteMembershipProvider" 
    type="SecurityPrototype.Models.CustomMembershipProvider"
    enablePasswordRetrieval="true" 
    enablePasswordReset="true" 
    requiresUniqueEmail="true" 
    passwordFormat="clear" />
 </providers>
</membership>

Some of the fields might not make sense right now, but that’s okay, they’ll make more sense later on.

So now that we’ve got our custom provider hooked up to our web.config. Thankfully the MVC Framework does the heavy lifting to defer calls your class; so that’s all you need as far as hookup goes. The Account Controller’s Logon method will now automagically find your derived class and use that for validating the user credentials.

So now that MVC is all hooked up to our custom MembershipProvider we’ve got to hook our custom MembershipProvider up to our data store.

I won’t fill out every method in this blog post, I will pick some key ones to give you the idea of what’s going on and I will provide a link to the cs file if you need to look at the whole thing.

Perhaps one of the most important methods in our MembershipProvider is the Initialize() method which does all the interpretation of the web.config and parses the values out into the properties/fields within our class:

using System;
using System.Configuration;
using System.Data;
using System.Data.Common;
using System.Security.Authentication;
using System.Web.Security;
using UserModel.Helpers;

public class MySiteMembershipProvider : MembershipProvider
{
 private string _connectionStringName;
 private ConnectionStringSettings _connectionStringSettings;
 private int _minNonAlphaNumeric = 0;
 private int _minPasswordLength = 0;
 private string _passwordStrengthRegularExpression;
 
 public override void Initialize(string name, NameValueCollection config)
 {
  if (string.IsNullOrEmpty(name))
   name = "MySiteMembershipProvider";
   
  if (config == null)
   throw new ArgumentNullException("config");
  
  //Set property defaults for optional configuration parameters
  string minPwdLenStr = config["minPasswordLength"];
  if (!string.IsNullOrEmpty(minReqPwdLenStr) && int.TryParse(minReqPwdLenStr, out _minRequiredPasswordLength))
  {
   config.Remove("minRequiredPasswordLength");
   config.Add("minRequiredPasswordLength", _minRequiredPasswordLength);
  }
 
  string minNonAlphaStr = config["minNonAlphaNumericCharacters"];
  if (!string.IsNullOrEmpty(minAlphaStr)) && int.TryParse(minNonAlphaStr, out _minNonAlphaNumeric))
  {
   config.Remove("minRequiredNonAlphaNumericCharacters");
   config.Add("minRequiredNonAlphaNumericCharacters", _minRequiredNonAlphaNumeric);
  }

  //Initialize the base provider after you've set the config defaults.
       base.Initialize(name, config);
  
  //Parse required values.
        _passwordStrengthRegularExpression = config["passwordExpression"];
  if (string.IsNullOrEmpty(_passwordStrengthRegularExpression))
   throw new ProviderException("Missing property \"passwordExpression\"");
   
  config.Remove("passwordExpression");
   
  _connectionStringName = config["connectionStringName"];
  if (string.IsNullOrEmpty(_connectionStringName))
   throw new ProviderException("Missing property connectionStringName");
   
  config.Remove("connectionStringName");
    
  _connectionStringSettings = ConfigurationManager.ConnectionStrings[_connectionString];
  if (_connectionStringSettings = null)
   throw new ProviderException(string.Format("Missing connection string {0}", _connectionString));

  if (config.Count > 0)
   throw new ProviderException(string.Format("Invalid property {0} found.", config.GetKey[0]);
 }
}

Sweet, so we’ve got our custom MembershipProvider created and hooked up to the web.config and we’ve got it reading the values provided by the web.config so that it can configure itself. Now what?

Let’s hook up the validate method which is what validates that our user is indeed a member of our site:

public override bool ValidateUser(string username, string password)
{
 using (IDbConnection con = InitConnection())
 using (IDbCommand cmd = con.CreateCommand("dbo.ValidateUser", CommandType.StoredProcedure))
 {
  cmd.AddParameters(
   cmd.CreateParameter("@Username", DbType.String, username),
   cmd.CreateParameter("@Password", DbType.String, password),
   cmd.CreateParameter("@Validated", DbType.Boolean, ParameterDirection.Output),
   cmd.CreateParameter("@ReturnValue", DbType.Int32, ParameterDirection.ReturnValue));
  
  cmd.ExecuteNonQuery();

  DbParameter param = (DbParameter)cmd.Parameters["@Validated"];
  return param.ValueOrZero() == 1;
 }
}

As you can see, the validate user method just hooks right into my database and runs the ValidateUser stored procedure. The stored procedure returns either a 0 – the user credentials are not valid, or 1 – the user credentials are valid. The method returns the corresponding boolean response.

Then I’ll implement the next most important method – GetUser(). This method returns an instance of MembershipUser which is likely a subset of the data held in your user’s profile in your database. For instance, in this prototype – my user’s profile is a single record in a single table in the database. Unlike the one provided by the AspNetSqlMembershipProvider which keeps the user’s credentials separate from their profile. In fact, in a multi-application system, this would make sense because a single user may want multiple profiles – one attached to each of the applications. For the purpose of our application though, there is only a single application and hence a single profile. Consequently there is only one table containing our user data.

public override MembershipUser GetUser(string username, bool userIsOnline)
{
 using (IDbConnection con = InitConnection())
 using (IDbCommand cmd = con.CreateCommand("dbo.GetUserByName", CommandType.StoredProcedure)
 {
  cmd.AddParameters(
   cmd.CreateParameter("@Username", DbType.String, username),
   cmd.CreateParameter("@UpdateLastActivity", DbType.Boolean, userIsOnline),
   cmd.CreateParameter("@ReturnValue", DbType.Int32, ParameterDirection.ReturnValue));

  using (IDataReader rdr = cmd.ExecuteReader(CommandBehavior.SingleResult))
  {
   DbParameter ret = ((DbParameter)cmd.Parameters["@ReturnValue"];

   if (!rdr.Read())
    return null;

   int userId = rdr.GetInt32(0);
   string userName = rdr.GetString(1);
   string email = rdr.GetString(2);
   string question = rdr.GetString(3);
   bool isApproved = rdr.GetBoolean(4);
   bool isLockedOut = rdr.GetBoolean(5);
   DateTime createDate = rdr.GetDateTime(6).ToLocalTime();
   DateTime lastLoginDate = rdr.GetDateTime(7).ToLocalTime();
   DateTime lastActivityDate = rdr.GetDateTime(8).ToLocalTime();
   DateTime lastPasswordChange = rdr.GetDateTime(9).ToLocalTime();
   DateTime lastLockout = DateTime.MinValue; //Lockout not used

   return new MembershipUser(
    Name,
    userName, 
    userId, 
    email, 
    question,
    null, 
    isApproved, 
    isLockedOut, 
    createDate, 
    lastLoginDate, 
    lastActivityDate,
    lastPasswordChange,
    lastLockout);
  }
 }
}

So as you can see from the previous examples, the MembershipProvider is actually hooking up directly to our database. You could equally easily hook this into your business layer or your DAL if that is more appropriate. The point I’m attempting to demonstrate is that the MembershipProvider is your site’s link between the HttpContext .User and your underlying application model. For instance, if I were to use a data layer in between the provider and my database, then I would use something more akin to this:

using System;
using System.Configuration;
using System.Data;
using System.Data.Common;
using System.Security.Authentication;
using System.Web.Security;
using UserModel.Helpers;

public class MySiteMembershipProvider : MembershipProvider
{
 public override MembershipUser GetUser(string username, bool userIsOnline)
 {
  ArgumentHelper.CheckParam("username", ref username, true, true, true);

    //My underlying business object is a custom class called MySiteUser
    //it contains the complete user profile, but we only need the bits
    //that pertain to the MembershipUser, so we’ll grab the profile and
    //create a new MembershipUser instance from it.
  MySiteUser user = DataAccess.GetUser(username);
  if (userIsOnline)
  {
   user.LastActivity = DateTime.Now;
   DataAccess.SaveUser(user);
  }

  return new MembershipUser(
  this.Name,
  user.UserName,
  user.UserId,
  user.Email,
  user.Question,
  null,
  user.IsApproved,
  user.IsLockedOut,
  user.CreateDate,
  user.LastLoginDate,
  user.LastActivityDate,
  user.LastPasswordChange,
  user.LastLockout);
 }
}

The rest of the provider follows the same concept. There are a couple of methods that require an instance of MembershipUserCollection returned. This is handled in exactly the same way by iterating through your model adding MembershipUser instances to the collection and returning the collection.

In the next blog post I will move on to discuss the RoleProvider which is used to provide restriction on which resources a user account has or hasn’t got access to.

No comments:

Post a Comment