Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Custom MembershipProvider and RoleProvider Implementations that use Web Services

0.00/5 (No votes)
4 Oct 2009 6  
Custom MembershipProvider and RoleProvider implementations that use web services in order to separate the application and database servers.

Latest

If you happen to be using Visual Studio 2008, then you would probably be better off using the WCF Authentication Services provided in .NET 3.5.

WCF Authentication Service http://msdn.microsoft.com/en-us/library/bb398990.aspx[^]

WCF Role http://msdn.microsoft.com/en-us/library/bb398911.aspx[^]

WCF Profile http://msdn.microsoft.com/en-us/library/bb514968.aspx[^]

I'd like to thank Chris Mankowski for prompting[^] me to do something I've been meaning to do for some time.

Introduction

In the latest release of ASP.NET 2.0, we have been introduced to a new set of controls that allow the developer to add a reasonably rich security model and UI to an application with very little effort. All these controls use providers that are loaded via configuration in the web.config file.

By default, the developer is directed to using a local SQLEXPRESS database, however she/he can easily redirect the default SQL based providers to use a different database via a connection string. And it is simple to use the aspnet_regsql tool to create a new (or update an existing) database with the database schema required.

Other third party providers have been created so that developers can use MySql[^], ODBC[^] or even the good old web.config[^] to store the role/membership data and there are plenty of resources out there for writing providers that will work with other databases or alternate database schemas. However all of the providers published to date, AFAIK, require that the application that is to be secured can access the database directly, this is not always ideal or even possible in a commercial environment which has implemented an n-tier structure and that the application can only access databases via a web service.

What I intend to demonstrate here is how to write your own set of providers that will be compatible with the supplied ASP.NET controls and will be able to use a provider that is being handled by a web service.

Requirements

Since we are implementing a web service, it would be nice if more than one application could use the web service but use it to talk to different databases or even a different set of providers. Also some implementations of the providers, e.g. the default SQL providers SqlRoleProvider[^] and SqlMembershipProvider[^], allow you to store the roles and users for more than one application within the same database.

With these requirements in mind, we need to make sure that we design our provider to allow the developer to control which provider to use when the request reaches the web service and the URL to the web service, choosing the application name is already a property of the base provider classes. We are also lazy and wish to write as little code as possible and use the framework to do the grunt work, that way we can concentrate on the more exacting customer requirements which never stay still.

Implementing the RoleProvider Web Service

The role provider is the simplest of the two providers that we need to implement. Below is a list of methods that the base role provider class supports that we need to support in our web service:

public abstract class RoleProvider : ProviderBase
{
      // Methods
      protected RoleProvider();
      public abstract void AddUsersToRoles(string[] usernames,
                                           string[] roleNames);
      public abstract void CreateRole(string roleName);
      public abstract bool DeleteRole(string roleName,
                                    bool throwOnPopulatedRole);
      public abstract string[] FindUsersInRole(string roleName,
                                        string usernameToMatch);
      public abstract string[] GetAllRoles();
      public abstract string[] GetRolesForUser(string username);
      public abstract string[] GetUsersInRole(string roleName);
      public abstract bool IsUserInRole(string username,
                                               string roleName);
      public abstract void RemoveUsersFromRoles(string[] usernames,
                                                string[] roleNames);
      public abstract bool RoleExists(string roleName);

      // Properties
      public abstract string ApplicationName { get; set; }
}

The property is our only bit of state so we can pass that as a parameter in each call. Since we want to be able to use any provider to implement this service, we need to have some sort of configuration to allow developers to add new providers to a mix. Luckily this is already provided to us by the framework and the entries that can be added in the web.config of the web service application:

<roleManager enabled="true">
    <providers>
        <clear />
        <add applicationName="/"
            connectionStringName="TrustSecurity"
            name="AspNetSqlRoleProvider"
            type="System.Web.Security.SqlRoleProvider" />
    </providers>
</roleManager>

In order for us to use this configuration, we can use the System.Web.Security.Roles.Providers collection with a provider name as set by the name attribute in the web.config. Given that we have the provider name that we wish to use and the application name we wish to use with that provider, we can use the following method to select a provider and set the application name of that provider:

protected System.Web.Security.RoleProvider GetProvider(
                       string providerName, string applicationName)
{
    System.Web.Security.RoleProvider provider;
    if ((providerName != null) &&
        (System.Web.Security.Roles.Providers[providerName] != null))
    {
      provider = System.Web.Security.Roles.Providers[providerName];
    }
    else
    {
      provider = System.Web.Security.Roles.Provider;
    }

    if (applicationName != null)
    {
      provider.ApplicationName = applicationName;
    }

    return provider;
}

Using the above method now makes it very simple to define our web service interface:

  [WebMethod(Description="")]
  public void AddUsersToRoles(string providerName,
      string applicationName, string[] usernames, string[] roleNames)
  {
    GetProvider(providerName, applicationName).AddUsersToRoles(
                                            usernames, roleNames);
  }

  [WebMethod(Description = "")]
  public void CreateRole(string providerName, string applicationName,
                                                      string roleName)
  {
    GetProvider(providerName, applicationName).CreateRole(roleName);
  }

  [WebMethod(Description = "")]
  public bool DeleteRole(string providerName, string applicationName,
                            string roleName, bool throwOnPopulatedRole)
  {
    return GetProvider(providerName, applicationName).DeleteRole(
                                       roleName, throwOnPopulatedRole);
  }

  [WebMethod(Description = "")]
  public string[] FindUsersInRole(string providerName,
       string applicationName, string roleName, string usernameToMatch)
  {
    return GetProvider(providerName, applicationName).FindUsersInRole(
                                              roleName, usernameToMatch);
  }

  [WebMethod(Description = "")]
  public string[] GetAllRoles(string providerName, string applicationName)
  {
    return GetProvider(providerName, applicationName).GetAllRoles();
  }

  [WebMethod(Description = "")]
  public string[] GetRolesForUser(string providerName,
                        string applicationName, string username)
  {
    return GetProvider(providerName, applicationName).GetRolesForUser(
                                                              username);
  }

  [WebMethod(Description = "")]
  public string[] GetUsersInRole(string providerName,
                                string applicationName, string roleName)
  {
    return GetProvider(providerName, applicationName).GetUsersInRole(
                                                             roleName);
  }

  [WebMethod(Description = "")]
  public bool IsUserInRole(string providerName, string applicationName,
                                        string username, string roleName)
  {
    return GetProvider(providerName, applicationName).IsUserInRole(
                                                  username, roleName);
  }

  [WebMethod(Description = "")]
  public void RemoveUsersFromRoles(string providerName,
      string applicationName, string[] usernames, string[] roleNames)
  {
    GetProvider(providerName, applicationName).RemoveUsersFromRoles(
                                                usernames, roleNames);
  }

  [WebMethod(Description = "")]
  public bool RoleExists(string providerName, string applicationName,
    string roleName)
  {
    return GetProvider(providerName, applicationName).RoleExists(
                                                           roleName);
  }

Implementing the Custom RoleProvider Class

A custom role provider implementation requires that we implement the required methods provided by the System.Web.Security.RoleProvider abstract class. We also need to add our custom configuration, as highlighted below, to the web.config and extract that via the Initialize method:

<roleManager defaultProvider="WebServiceRoleProvider" enabled="true">
    <providers>
        <clear />
        <add applicationName="/"
            name="WebServiceRoleProvider"
            type="ManyMonkeys.SecurityProviders.WebServiceRoleProvider"
            roleProviderUri="http://localhost/WTS/RoleProvider.asmx"
            remoteProviderName="AspNetSqlRoleProvider" />
    </providers>
</roleManager>
    public override void Initialize(string name,
       System.Collections.Specialized.NameValueCollection config)
    {
      if (config["roleProviderUri"] != null)
      {
        service.Url = config["roleProviderUri"];
      }

      _ApplicationName = config["applicationName"];
      if (string.IsNullOrEmpty(_ApplicationName))
      {
        _ApplicationName = ProviderUtility.GetDefaultAppName();
      }

      _RemoteProviderName = config["remoteProviderName"];

      base.Initialize(name, config);
    }

Now that the custom configuration has been extracted and our web service consumer has been instantiated and initialized, the implementation of the rest of the required methods looks like this:

public override void AddUsersToRoles(string[] usernames,
                                         string[] roleNames)
{
  service.AddUsersToRoles(_RemoteProviderName,
              _ApplicationName, usernames, roleNames);
}

public override void CreateRole(string roleName)
{
  service.CreateRole(_RemoteProviderName,
                     _ApplicationName, roleName);
}

public override bool DeleteRole(string roleName,
                         bool throwOnPopulatedRole)
{
  return service.DeleteRole(_RemoteProviderName,
     _ApplicationName, roleName, throwOnPopulatedRole);
}

public override string[] FindUsersInRole(string roleName,
                                    string usernameToMatch)
{
  return service.FindUsersInRole(_RemoteProviderName,
                 _ApplicationName, roleName, usernameToMatch);
}

public override string[] GetAllRoles()
{
  return service.GetAllRoles(_RemoteProviderName,
                                    _ApplicationName);
}

public override string[] GetRolesForUser(string username)
{
  return service.GetRolesForUser(_RemoteProviderName,
                              _ApplicationName, username);
}

public override string[] GetUsersInRole(string roleName)
{
  return service.GetUsersInRole(_RemoteProviderName,
                              _ApplicationName, roleName);
}

public override bool IsUserInRole(string username,
                                       string roleName)
{
  return service.IsUserInRole(_RemoteProviderName,
                    _ApplicationName, username, roleName);
}

public override void RemoveUsersFromRoles(string[] usernames,
  string[] roleNames)
{
  service.RemoveUsersFromRoles(_RemoteProviderName,
                     _ApplicationName, usernames, roleNames);
}

public override bool RoleExists(string roleName)
{
  return service.RoleExists(_RemoteProviderName,
                        _ApplicationName, roleName);
}

Implementing the MembershipProvider Web Service

All of the techniques used for the role provider implementation can be used to help implement the MembershipProvider. However, by looking at the requirements of the MembershipProvider, it is obvious this going to be a bit more complicated:

public abstract class MembershipProvider : ProviderBase
{
  // Methods
  public abstract bool ChangePassword(string username,
                   string oldPassword, string newPassword);
  public abstract bool ChangePasswordQuestionAndAnswer(
                         string username, string password,
                         string newPasswordQuestion,
                         string newPasswordAnswer);
  public abstract MembershipUser CreateUser(string username,
      string password, string email, string passwordQuestion,
      string passwordAnswer, bool isApproved,
      object providerUserKey, out MembershipCreateStatus status);
  public abstract bool DeleteUser(string username,
                                      bool deleteAllRelatedData);
  public abstract MembershipUserCollection FindUsersByEmail(
              string emailToMatch, int pageIndex, int pageSize,
              out int totalRecords);
  public abstract MembershipUserCollection FindUsersByName(
         string usernameToMatch, int pageIndex, int pageSize,
         out int totalRecords);
  public abstract MembershipUserCollection GetAllUsers(
         int pageIndex, int pageSize, out int totalRecords);
  public abstract int GetNumberOfUsersOnline();
  public abstract string GetPassword(string username,
                                             string answer);
  public abstract MembershipUser GetUser(
      object providerUserKey, bool userIsOnline);
  public abstract MembershipUser GetUser(string username,
                                         bool userIsOnline);
  public abstract string GetUserNameByEmail(string email);
  public abstract string ResetPassword(string username,
                                           string answer);
  public abstract bool UnlockUser(string userName);
  public abstract void UpdateUser(MembershipUser user);
  public abstract bool ValidateUser(string username,
                                      string password);

  // Properties
  public abstract string ApplicationName { get; set; }
  public abstract bool EnablePasswordReset { get; }
  public abstract bool EnablePasswordRetrieval { get; }
  public abstract int MaxInvalidPasswordAttempts { get; }
  public abstract int MinRequiredNonAlphanumericCharacters
       { get; }
  public abstract int MinRequiredPasswordLength { get; }
  public abstract int PasswordAttemptWindow { get; }
  public abstract MembershipPasswordFormat PasswordFormat
       { get; }
  public abstract string PasswordStrengthRegularExpression
       { get; }
  public abstract bool RequiresQuestionAndAnswer { get; }
  public abstract bool RequiresUniqueEmail { get; }
}

First, the read only properties look like they could cause trouble if we have to pass them to the web service in each method call or fetch them from the web service each time they are requested, however it seems that these properties are only applicable to the UI and so there is no real need to pass these values to and from the web service in this exercise. Secondly, there is an object called MembershipUser and collections of these objects that we are going to find some way of passing back and forth. Unfortunately, when we look at the MembershipUser object we see that this is not going to be as simple as exposing the object in the WebMethod due to the number of read-only properties that are to be transferred back and forth and can only be set by the constructor. A simple class that contains all of the required properties can be used for this purpose:

public class MembershipUser
{
  public MembershipUser()
  {
  }

  private string comment;

  public string Comment
  {
    get { return comment; }
    set { comment = value; }
  }

  private DateTime creationDate;

  public DateTime CreationDate
  {
    get { return creationDate; }
    set { creationDate = value; }
  }

  private string email;

  public string Email
  {
    get { return email; }
    set { email = value; }
  }

  private bool isApproved;

  public bool IsApproved
  {
    get { return isApproved; }
    set { isApproved = value; }
  }

  private bool isLockedOut;

  public bool IsLockedOut
  {
    get { return isLockedOut; }
    set { isLockedOut = value; }
  }

  private bool isOnline;

  public bool IsOnline
  {
    get { return isOnline; }
    set { isOnline = value; }
  }

  private DateTime lastActivityDate;

  public DateTime LastActivityDate
  {
    get { return lastActivityDate; }
    set { lastActivityDate = value; }
  }

  private DateTime lastLockoutDate;

  public DateTime LastLockoutDate
  {
    get { return lastLockoutDate; }
    set { lastLockoutDate = value; }
  }

  private DateTime lastLoginDate;

  public DateTime LastLoginDate
  {
    get { return lastLoginDate; }
    set { lastLoginDate = value; }
  }

  private DateTime lastPasswordChangedDate;

  public DateTime LastPasswordChangedDate
  {
    get { return lastPasswordChangedDate; }
    set { lastPasswordChangedDate = value; }
  }

  private string passwordQuestion;

  public string PasswordQuestion
  {
    get { return passwordQuestion; }
    set { passwordQuestion = value; }
  }

  private string providerName;

  public string ProviderName
  {
    get { return providerName; }
    set { providerName = value; }
  }

  private object providerUserKey;

  public object ProviderUserKey
  {
    get { return providerUserKey; }
    set { providerUserKey = value; }
  }

  private string userName;

  public string UserName
  {
    get { return userName; }
    set { userName = value; }
  }
}

We now need methods that will allow us to convert between the different MembershipUser objects and to create an array objects that can be transferred. We need to note that when we create a System.Web.Security.MembershipUser object, we need to provide the name of the provider that the object requires in its internal methods:

  protected System.Web.Security.MembershipProvider GetProvider(
                      string providerName, string applicationName)
  {
    System.Web.Security.MembershipProvider provider;
    if ((providerName != null) &&
      (System.Web.Security.Membership.Providers[providerName] != null))
    {
      provider = System.Web.Security.Membership.Providers[providerName];
    }
    else
    {
      provider = System.Web.Security.Membership.Provider;
    }

    if (applicationName != null)
    {
      provider.ApplicationName = applicationName;
    }

    return provider;
  }

  protected MembershipUser ConvertUser(
         System.Web.Security.MembershipUser user)
  {
    if (user == null) return null;
    MembershipUser membershipUser = new MembershipUser();
    membershipUser.Comment = user.Comment;
    membershipUser.CreationDate = user.CreationDate;
    membershipUser.Email = user.Email;
    membershipUser.IsApproved = user.IsApproved;
    membershipUser.IsLockedOut = user.IsLockedOut;
    membershipUser.IsOnline = user.IsOnline;
    membershipUser.LastActivityDate = user.LastActivityDate;
    membershipUser.LastLockoutDate = user.LastLockoutDate;
    membershipUser.LastLoginDate = user.LastLoginDate;
    membershipUser.LastPasswordChangedDate =
                         user.LastPasswordChangedDate;
    membershipUser.PasswordQuestion = user.PasswordQuestion;
    membershipUser.ProviderName = user.ProviderName;
    membershipUser.ProviderUserKey = user.ProviderUserKey;
    membershipUser.UserName = user.UserName;
    return membershipUser;
  }

  protected System.Web.Security.MembershipUser
    ConvertUser(System.Web.Security.MembershipProvider provider,
        MembershipUser user)
  {
    if (user == null) return null;
    System.Web.Security.MembershipUser membershipUser =
      new System.Web.Security.MembershipUser(provider.Name,
                                user.UserName,
                                user.ProviderUserKey,
                                user.Email,
                                user.PasswordQuestion,
                                user.Comment,
                                user.IsApproved,
                                user.IsLockedOut,
                                user.CreationDate,
                                user.LastLoginDate,
                                user.LastActivityDate,
                                user.LastPasswordChangedDate,
                                user.LastLockoutDate);
    return membershipUser;
  }

  protected List<MembershipUser>
    BuildUserList(System.Web.Security.MembershipUserCollection collection)
  {
    if (collection == null) return null;
    List<MembershipUser> list = new List<MembershipUser>();
    foreach (System.Web.Security.MembershipUser user in collection)
    {
      list.Add(ConvertUser(user));
    }
    return list;
  }

With these helper classes in place, we can create our web service interface:

  [WebMethod(Description = "")]
  public bool ChangePassword(string providerName,
           string applicationName, string username,
           string oldPassword, string newPassword)
  {
    return GetProvider(providerName, applicationName).ChangePassword(
                                   username, oldPassword, newPassword);
  }

  [WebMethod(Description = "")]
  public bool ChangePasswordQuestionAndAnswer(string providerName,
           string applicationName, string username, string password,
           string newPasswordQuestion, string newPasswordAnswer)
  {
    return GetProvider(providerName,
      applicationName).ChangePasswordQuestionAndAnswer(username,
                 password, newPasswordQuestion, newPasswordAnswer);
  }

  [WebMethod(Description = "")]
  public MembershipUser CreateUser(string providerName,
    string applicationName, string username, string password,
    string email, string passwordQuestion, string passwordAnswer,
    bool isApproved, object providerUserKey,
    out System.Web.Security.MembershipCreateStatus status)
  {
    return ConvertUser(GetProvider(providerName, applicationName).CreateUser(
      username, password, email, passwordQuestion, passwordAnswer, isApproved,
      providerUserKey, out status));
  }

  [WebMethod(Description = "")]
  public bool DeleteUser(string providerName, string applicationName,
                               string username, bool deleteAllRelatedData)
  {
    return GetProvider(providerName, applicationName).DeleteUser(username,
      deleteAllRelatedData);
  }

  [WebMethod(Description = "")]
  public List<MembershipUser> FindUsersByEmail(string providerName,
                  string applicationName, string emailToMatch,
                  int pageIndex, int pageSize, out int totalRecords)
  {
    return BuildUserList(GetProvider(providerName,
      applicationName).FindUsersByEmail(emailToMatch, pageIndex,
      pageSize, out totalRecords));
  }

  [WebMethod(Description = "")]
  public List<MembershipUser> FindUsersByName(string providerName,
                   string applicationName, string usernameToMatch,
                   int pageIndex, int pageSize, out int totalRecords)
  {
    return BuildUserList(GetProvider(providerName,
      applicationName).FindUsersByName(usernameToMatch, pageIndex,
      pageSize, out totalRecords));
  }

  [WebMethod(Description = "")]
  public List<MembershipUser> GetAllUsers(string providerName,
                             string applicationName, int pageIndex,
                             int pageSize, out int totalRecords)
  {
    return BuildUserList(GetProvider(providerName,
      applicationName).GetAllUsers(pageIndex, pageSize,
                                        out totalRecords));
  }

  [WebMethod(Description = "")]
  public int GetNumberOfUsersOnline(string providerName,
                                    string applicationName)
  {
    return
     GetProvider(providerName, applicationName).GetNumberOfUsersOnline();
  }

  [WebMethod(Description = "")]
  public string GetPassword(string providerName,
          string applicationName, string username, string answer)
  {
    return GetProvider(providerName, applicationName).GetPassword(
                                                   username, answer);
  }

  [WebMethod(Description = "")]
  public MembershipUser GetUserByUserName(string providerName,
      string applicationName, string username, bool userIsOnline)
  {
    return ConvertUser(GetProvider(providerName,
      applicationName).GetUser(username, userIsOnline));
  }

  [WebMethod(Description = "")]
  public MembershipUser GetUserByKey(string providerName,
            string applicationName, object providerUserKey,
            bool userIsOnline)
  {
    return ConvertUser(GetProvider(providerName,
      applicationName).GetUser(providerUserKey, userIsOnline));
  }

  [WebMethod(Description = "")]
  public string GetUserNameByEmail(string providerName,
                       string applicationName, string email)
  {
    return
     GetProvider(providerName, applicationName).GetUserNameByEmail(email);
  }

  [WebMethod(Description = "")]
  public string ResetPassword(string providerName,
      string applicationName, string username, string answer)
  {
    return GetProvider(providerName,
      applicationName).ResetPassword(username, answer);
  }

  [WebMethod(Description = "")]
  public bool UnlockUser(string providerName,
              string applicationName, string userName)
  {
    return
      GetProvider(providerName, applicationName).UnlockUser(userName);
  }

  [WebMethod(Description = "")]
  public void UpdateUser(string providerName,
      string applicationName, MembershipUser user)
  {
    System.Web.Security.MembershipProvider provider =
              GetProvider(providerName, applicationName);
    provider.UpdateUser(ConvertUser(provider, user));
  }

  [WebMethod(Description = "")]
  public bool ValidateUser(string providerName,
     string applicationName, string username, string password)
  {
    return GetProvider(providerName, applicationName).ValidateUser(
                                                 username, password);
  }

Implementing the Custom MembershipProvider Class

A custom membership provider implementation requires that we implement the required methods provided by the System.Web.Security.MembershipProvider abstract class. We also need to add our custom configuration, as highlighted below, to the web.config and extract that via the Initialize method:

<membership defaultProvider="WebServiceMembershipProvider"
  userIsOnlineTimeWindow="20">
    <providers>
        <clear />
        <add name="WebServiceMembershipProvider"
            type="ManyMonkeys.SecurityProviders.WebServiceMembershipProvider"
            enablePasswordRetrieval="false"
            enablePasswordReset="true"
            requiresQuestionAndAnswer="true"
            passwordFormat="Hashed"
            applicationName="/"
            roleProviderUri="http://localhost/WTS/MembershipProvider.asmx"
            remoteProviderName="AspNetSqlMembershipProvider"/>
    </providers>
</membership>
public override void Initialize(string name,
      System.Collections.Specialized.NameValueCollection config)
{
  if (config["roleProviderUri"] != null)
  {
    service.Url = config["roleProviderUri"];
  }
  _ApplicationName = config["applicationName"];
  if (string.IsNullOrEmpty(_ApplicationName))
  {
    _ApplicationName = ProviderUtility.GetDefaultAppName();
  }

  _EnablePasswordRetrieval = ProviderUtility.GetBooleanValue(
                        config, "enablePasswordRetrieval", false);
  _EnablePasswordReset = ProviderUtility.GetBooleanValue(config,
                                     "enablePasswordReset", true);
  _RequiresQuestionAndAnswer =
         ProviderUtility.GetBooleanValue(config,
         "requiresQuestionAndAnswer", true);
  _RequiresUniqueEmail = ProviderUtility.GetBooleanValue(config,
                                     "requiresUniqueEmail", true);
  _MaxInvalidPasswordAttempts = ProviderUtility.GetIntValue(config,
                         "maxInvalidPasswordAttempts", 5, false, 0);
  _PasswordAttemptWindow = ProviderUtility.GetIntValue(config,
                             "passwordAttemptWindow", 10, false, 0);
  _MinRequiredPasswordLength = ProviderUtility.GetIntValue(config,
                       "minRequiredPasswordLength", 7, false, 0x80);
  _MinRequiredNonalphanumericCharacters =
             ProviderUtility.GetIntValue(config,
             "minRequiredNonalphanumericCharacters", 1, true, 0x80);
  _PasswordStrengthRegularExpression =
             config["passwordStrengthRegularExpression"];

  if (config["passwordFormat"] != null)
  {
    _PasswordFormat =
      (MembershipPasswordFormat)Enum.Parse(
         typeof(MembershipPasswordFormat), config["passwordFormat"]);
  }
  else
  {
    _PasswordFormat = MembershipPasswordFormat.Hashed;
  }

  _RemoteProviderName = config["remoteProviderName"];

  base.Initialize(name, config);
}

Methods are also required to turn the MembershipUser object from the web service consumer into a real System.Web.Security.MembershipUser object that will be recognized by the controls:

    static private MembershipProvider.MembershipUser
      ConvertUser(System.Web.Security.MembershipUser user)
    {
      if (user == null) return null;
      MembershipProvider.MembershipUser membershipUser =
        new MembershipProvider.MembershipUser();
      membershipUser.Comment = user.Comment;
      membershipUser.CreationDate = user.CreationDate;
      membershipUser.Email = user.Email;
      membershipUser.IsApproved = user.IsApproved;
      membershipUser.IsLockedOut = user.IsLockedOut;
      membershipUser.IsOnline = user.IsOnline;
      membershipUser.LastActivityDate = user.LastActivityDate;
      membershipUser.LastLockoutDate = user.LastLockoutDate;
      membershipUser.LastLoginDate = user.LastLoginDate;
      membershipUser.LastPasswordChangedDate =
                           user.LastPasswordChangedDate;
      membershipUser.PasswordQuestion = user.PasswordQuestion;
      membershipUser.ProviderName = user.ProviderName;
      membershipUser.ProviderUserKey = user.ProviderUserKey;
      membershipUser.UserName = user.UserName;
      return membershipUser;
    }

    private System.Web.Security.MembershipUser
      ConvertUser(MembershipProvider.MembershipUser user)
    {
      if (user == null) return null;
      System.Web.Security.MembershipUser membershipUser =
        new System.Web.Security.MembershipUser(this.Name,
                                  user.UserName,
                                  user.ProviderUserKey,
                                  user.Email,
                                  user.PasswordQuestion,
                                  user.Comment,
                                  user.IsApproved,
                                  user.IsLockedOut,
                                  user.CreationDate,
                                  user.LastLoginDate,
                                  user.LastActivityDate,
                                  user.LastPasswordChangedDate,
                                  user.LastLockoutDate);
      return membershipUser;
    }

    private System.Web.Security.MembershipUserCollection
      BuildUserCollection(MembershipProvider.MembershipUser[] list)
    {
      if (list == null) return null;
      System.Web.Security.MembershipUserCollection collection =
        new System.Web.Security.MembershipUserCollection();
      foreach (MembershipProvider.MembershipUser user in list)
      {
        collection.Add(ConvertUser(user));
      }
      return collection;
    }

With the help of these helper methods, it is now a trivial task to finish our implementation once we have instantiated the web service consumer:

public override bool ChangePassword(string username,
                    string oldPassword, string newPassword)
{
  return service.ChangePassword(_RemoteProviderName,
      _ApplicationName, username, oldPassword, newPassword);
}

public override bool ChangePasswordQuestionAndAnswer(
      string username, string password,
      string newPasswordQuestion, string newPasswordAnswer)
{
  return service.ChangePasswordQuestionAndAnswer(
      _RemoteProviderName, _ApplicationName, username,
      password, newPasswordQuestion, newPasswordAnswer);
}

public override MembershipUser CreateUser(string username,
     string password, string email, string passwordQuestion,
     string passwordAnswer, bool isApproved,
     object providerUserKey, out MembershipCreateStatus status)
{
  MembershipProvider.MembershipCreateStatus newStatus;
  MembershipUser user =
    ConvertUser(service.CreateUser(_RemoteProviderName,
         _ApplicationName, username, password, email,
         passwordQuestion, passwordAnswer, isApproved,
         providerUserKey, out newStatus));
  status =
   (MembershipCreateStatus)Enum.Parse(typeof(MembershipCreateStatus),
                                               newStatus.ToString());
  return user;
}

public override bool DeleteUser(string username,
                                bool deleteAllRelatedData)
{
  return service.DeleteUser(_RemoteProviderName,
             _ApplicationName, username, deleteAllRelatedData);
}

public override MembershipUserCollection FindUsersByEmail(
           string emailToMatch, int pageIndex, int pageSize,
           out int totalRecords)
{
  return BuildUserCollection(service.FindUsersByEmail(
    _RemoteProviderName, _ApplicationName, emailToMatch,
    pageIndex, pageSize, out totalRecords));
}

public override MembershipUserCollection FindUsersByName(
  string usernameToMatch,
  int pageIndex, int pageSize, out int totalRecords)
{
  return BuildUserCollection(service.FindUsersByName(
      _RemoteProviderName, _ApplicationName, usernameToMatch,
      pageIndex, pageSize, out totalRecords));
}

public override MembershipUserCollection GetAllUsers(
        int pageIndex, int pageSize, out int totalRecords)
{
  return BuildUserCollection(service.GetAllUsers(
      _RemoteProviderName, _ApplicationName, pageIndex,
      pageSize, out totalRecords));
}

public override int GetNumberOfUsersOnline()
{
  return service.GetNumberOfUsersOnline(_RemoteProviderName,
                                           _ApplicationName);
}

public override string GetPassword(string username, string answer)
{
  return service.GetPassword(_RemoteProviderName,
                               _ApplicationName, username, answer);
}

public override MembershipUser GetUser(string username,
                                         bool userIsOnline)
{
  return ConvertUser(service.GetUserByUserName(
           _RemoteProviderName, _ApplicationName,
           username, userIsOnline));
}

public override MembershipUser GetUser(object providerUserKey,
                                             bool userIsOnline)
{
  return ConvertUser(service.GetUserByKey(_RemoteProviderName,
             _ApplicationName, providerUserKey, userIsOnline));
}

public override string GetUserNameByEmail(string email)
{
  return service.GetUserNameByEmail(_RemoteProviderName,
                                   _ApplicationName, email);
}

public override string ResetPassword(string username,
                                           string answer)
{
  return service.ResetPassword(_RemoteProviderName,
                         _ApplicationName, username, answer);
}

public override bool UnlockUser(string userName)
{
  return service.UnlockUser(_RemoteProviderName,
                            _ApplicationName, userName);
}

public override void UpdateUser(MembershipUser user)
{
  service.UpdateUser(_RemoteProviderName,
                   _ApplicationName, ConvertUser(user));
}

public override bool ValidateUser(string username,
                                        string password)
{
  return service.ValidateUser(_RemoteProviderName,
                   _ApplicationName, username, password);
}

That's it, we now have a pair of providers that can use any other provider implementations via a web service and thus require that our application does not require direct access to the database server.

Security Issue

The observant amongst you will have realized that we have a security issue in that passwords are currently being transferred in plain text across the web service interface. Two approaches immediately come to the fore, as ways of mitigating this issue:

  1. Use SSL to secure the connection between the application and the web service.
  2. Use WSE 3.0[^], either with or without SSL, to secure the end points and to encrypt data within the packet.

The Provided Code

The code provided is split into two parts:

  1. The web service that is called by custom providers
  2. The custom providers that consume the web service

In order to use this code, you will need to host the web service and modify the entries added to the web.config in order for the custom providers to be utilized.

Feedback and Voting

If you have read this far then please remember to vote, if you do or don't like, or agree, with what you have read then please say so in the forum below - your feedback is important!

Revision History

  • 11th February, 2006
    • Original article
  • 13th February, 2006
    • Corrected non-obvious mis-spellings
  • 16th February, 2006
    • Added VB.NET sample code because I was feeling magnanimous - do not take it as said that I will do so for my other samples :)
  • 3rd October, 2009
    • Added links to the WCF authentication services available in .NET 3.5 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here