Sunday, February 7, 2010

Extending the membership provider.

Lots of people ask this question on how to extend the “MembershipProvider” to make use of custom authentication like using existing database or make use of some other way of authentication. Many blogs show you how to extend the membership provider class but don’t give you what other class they have used in the backend. With this blog I would not only show how to extend MembershipProvider class but will give you the full source code of the web service which is used instead of a database as a backend.

I have made use of the MembershipProvider many a times but I would like to share my experience in one of my project where I had to extend the MembershipProvider class to make use of an already existing web service of the client to validate user, create user, change password etc. In this blog I will show how to extend MembershipProvider class and write your custom MembershipProvider class.

Since I cannot reveal what webserivce was used in the website developed for the client I will create my own webservice to mimic the exact behavior as in the website. So lets start by creating a webservice which will have methods to create a user, validate user credentials and finally change the user’ password. Once the webservice is done we will extend the membership class. Below is the code for the webservice.

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class WebServiceAuthentication : System.Web.Services.WebService
{
    string xmlFilePath = string.Empty;
    public WebServiceAuthentication()
    {
        xmlFilePath = Server.MapPath("data/users.xml");       
    }

    [WebMethod]
    public bool ChangePassword(string username, string newpassword)
    {
        XDocument users = XDocument.Load(xmlFilePath);
        var user = from u in users.Elements("user")
                   where u.Element("username").Value.ToLower() == username.ToLower()
                   select u;
        if (user != null && user.Count() > 0)
        {
            user.First().Element("password").Value = newpassword;
            return true;
        }

        return false;
    }

    [WebMethod]
    public bool ValidateUser(string userName, string passWord)
    {
        XDocument users = XDocument.Load(xmlFilePath);
        var user = from u in users.Element("users").Elements("user")
                   where u.Element("username").Value.ToLower() == userName.ToLower() && u.Element("password").Value == passWord
                   select u;
        if (user != null && user.Count() > 0)
            return true;
        return false;
    }

    [WebMethod]
    public Customer CreateUser(string username, string password, string emailID)
    {
        XDocument users = XDocument.Load(xmlFilePath);
        var user = from u in users.Elements("user")
                   where u.Attribute("email").Value.ToLower() == username.ToLower()
                   select u.Attribute("email").Value;
        if (user.Count() <= 0)
            throw new Exception("Email already in user");
        else
        {
            users.Element("users").Add(new XElement("user",
                new XAttribute("email", emailID),
                new XElement("userName", username),
                new XElement("password", password)));
            users.Save(xmlFilePath);
        }

        return new Customer { UserName = username };
    }

    [WebMethod]
    public string ResetPassword(string username)
    {
        string newPass = string.Empty;
        XDocument users = XDocument.Load(xmlFilePath);
        var user = from u in users.Elements("user")
                   where u.Element("username").Value.ToLower() == username.ToLower()
                   select u;
        if (user != null && user.Count() > 0)
        {
            newPass = Guid.NewGuid().ToString().Substring(0, 10);
            user.First().Element("password").Value = newPass;
        }
        else
            throw new Exception("User not found.");

        return newPass;
    }

    [WebMethod]
    public bool UpdateCustomer(Customer customerToBeUdpated)
    {       
        XDocument users = XDocument.Load(xmlFilePath);
        var userToBeUpdated = (from u in users.Elements("user")
                              where u.Attribute("email").Value.ToLower() == customerToBeUdpated.UserName.ToLower()
                              select u);
        if (userToBeUpdated != null && userToBeUpdated.Count() > 0)
        {
            XElement user = userToBeUpdated.First();
            user.Element("password").Value = customerToBeUdpated.Password;
            if (user.Element("firstname") != null)
                user.Element("firstname").Value = customerToBeUdpated.FirstName;
            else
                user.Add(new XElement("firstname", customerToBeUdpated.FirstName));
            if (user.Element("lastname") != null)
                user.Element("lastname").Value = customerToBeUdpated.LastName;
            else
                user.Add(new XElement("lastname", customerToBeUdpated.LastName));
            if (user.Element("address") != null)
            {
                user.Element("address").Element("street").Value = customerToBeUdpated.Address.Street;
                user.Element("address").Element("city").Value = customerToBeUdpated.Address.City;
                user.Element("address").Element("state").Value = customerToBeUdpated.Address.State;
                user.Element("address").Element("country").Value = customerToBeUdpated.Address.Country;
            }
            else
            {
                user.Add(new XElement("address", new XElement("street", customerToBeUdpated.Address.Street),
                    new XElement("city", customerToBeUdpated.Address.City),
                    new XElement("state", customerToBeUdpated.Address.State),
                    new XElement("country", customerToBeUdpated.Address.Country)));
            }

            return true;
        }

        return false;
    }
}

In the above web service we have methods to change the password (ChangePassword), to validate user credential based on user name and password (ValidateUser), method to create a user (CreateUser), method to change the password (ResetPassword) and finally method to update user details (UpdateCustomer). All these methods work on a XML file and make use of LINQ to XML to create user, change password etc. As mentioned before all the data is stored and retrieved from a XML file called Users.xml. If a new user has to be added or user details needs to be updated or password needs to be changed everything is done in the xml. The webservice makes use of a XML as a storage medium.The XML is pasted below.

<?xml version="1.0" encoding="utf-8" ?>
<users>
  <user email="a@a.com">
    <username>sandeep</username>
    <password>pass</password>
    <firstname>Sandeep</firstname>
    <lastname>P.R</lastname>
    <address>
      <street>Blah blah</street>
      <city>City</city>
      <state>State</state>
      <country>Country</country>
    </address>
  </user>
  <user email="sndppr@gmail.com">
    <username>sndppr@gmail.com</username>
    <password>pass</password>
    <firstname>Sandeep</firstname>
    <lastname>P.R</lastname>
    <address>
      <street>Blah blah</street>
      <city>City</city>
      <state>State</state>
      <country>Country</country>
    </address>
  </user>
</users>

The xml is pretty much straight forward. The XML stores the user name, password and other user related details like his address and email id. In the XML the password is saved as plain text which is not a good way of storing password. So while storing password in a physical file do use some sort of encryption.

Some of the methods of the web service return “Customer” as an object. The “Customer” class is also very straight forward. It derives from the “System.Web.Security.MembershipUser” class and adds some of its own properties. The “Customer” class code is pasted below.

public class Customer : System.Web.Security.MembershipUser
{
    public Customer()
    {

    }

    private string userName = string.Empty;

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string UserName { get; set; }
    public string EMail { get; set; }
    public string Password { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Country { get; set; }
}

That’s about web serivce and the XML file which form the backend, now lets try to extend System.Web.Security.MembershipProvider to work in conjunction with the above web service. Before that lets keep some things in mind. If you are not planning to make use of question and answer to reset the password then make sure that you return false from “RequiresQuestionAndAnswer” property. If you leave the default implementation then the system will throw “Sysmtem.NotImplementedException” with the following error message.

The method or operation is not implemented.

Now lets see the code where I have extended the System.Web.Security.MembershipProvider class. Some of the methods which have not been implemented have been removed for brevity.

public class CustomMembershipProvider : System.Web.Security.MembershipProvider
{
    public CustomMembershipProvider() : base()
    {
    }

    public override string ApplicationName
    {
        get
        {
            return "Authentication";
        }
        set
        {
            throw new NotImplementedException();
        }
    }
    //Non implemented methods have been removed for brevity.
    public override bool ChangePassword(string username, string oldPassword, string newPassword)
    {
        WebServiceAuthentication wsa = new WebServiceAuthentication();
        return wsa.ChangePassword(username, newPassword);
    }

    public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
    {
        MembershipUser cust = this.GetUser(username, false);       

        if (cust == null)
        {
            WebServiceAuthentication wsa = new WebServiceAuthentication();
            cust = wsa.CreateUser(username, password, email);
            status = cust != null ? MembershipCreateStatus.Success : MembershipCreateStatus.UserRejected;
        }
        else
            status = MembershipCreateStatus.DuplicateUserName;
        return cust;
    }

    public override MembershipUser GetUser(string username, bool userIsOnline)
    {
        WebServiceAuthentication wsa = new WebServiceAuthentication();
        MembershipUser mu = wsa.GetUser(username);
        return mu;
    }

    public override int MinRequiredNonAlphanumericCharacters
    {
        get { return 0; }
    }

    public override int MinRequiredPasswordLength
    {
        get { return 2; }
    }

    public override bool RequiresQuestionAndAnswer
    {
        get { return false; }
    }

    public override bool RequiresUniqueEmail
    {
        get { return false; }
    }
    //Non implemented methods have been removed for brevity

    public override void UpdateUser(MembershipUser user)
    {
        WebServiceAuthentication wsa = new WebServiceAuthentication();
        wsa.UpdateCustomer(user);
    }

    public override bool ValidateUser(string username, string password)
    {
        WebServiceAuthentication wsa = new WebServiceAuthentication();
        if (wsa.ValidateUser(username, password))
        {
            FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddMinutes(30), true, string.Empty);
            HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(fat));
            HttpContext.Current.Request.Cookies.Add(authCookie);
            return true;
        }
        else
        {
            return false;
        }
    }
}

In above code I have extended the MembershipProvider class and overridden “ChangePassword”, “CreateUser”, “GetUser”, “UpdateUser” and “ValidateUser” methods. All these methods are pretty straight forward, the methods create an instance of our web service class and call the web methods to change password, create user etc. The before mentioned membership methods are called by the various login controls provided in ASP.NET. “ChangePassword” is called by the “ChangePassword” control when you click the “Change Password” button. “CreateUser” method of the MembershipProvider class is executed when you click the “Create User” button of “CreateUserWizard” control. “ValidateUser” method is called whenever the user request needs validation or when you click the “Log in” button of the “Log in” control.

Once you have extended the MembershipProvider class add the following tag in the web.config file of your website.

<membership defaultProvider="CustomProvider" >
      <providers>       
        <add name="CustomProvider" type="CustomMembershipProvider" />       
      </providers> 
</membership>

In the above markup the "defaultProvider" is an optional parameter. If not provided it will default to "AspNetSqlProvider", the default provider provided by Microsoft. The "type" attribute has the name of the extended membership provider class. In our case, since the class is placed in the "App_Code" folder, we have only specified the class name. If your membership provider class lies in some different dll and you have added it as a reference to your website then one has to specify the full name preceded by the namespace as well. Also using the “add” tag in “providers” tag you can set the membership properties like "connectionStringName", "enablePasswordRetrieval", "enablePasswordReset" etc in the. Sample tag is pasted below.

<connectionStrings>
    <add name="connStr" connectionString="Data Source=testserver;Initial Catalog=Northwind;Persist Security Info=True;User ID=sa;Password=sa"/>
</connectionStrings>
<!--Membership provider configuration with membership properties set in web.config. The connectionStringName should match with the one provided in you web.config.-->
<membership defaultProvider="CustomProvider" >
      <providers>       
        <add name="CustomProvider" type="CustomMembershipProvider" applicationName="TestApp" connectionStringName="connStr" enablePasswordRetrieval="false" enablePasswordReset="true" maxInvalidPasswordAttempts="5"  />       
      </providers> 
</membership>

With this done you can drag and drop the various ASP.NET login controls like the CreateUserWizard, ChangePassword, Login etc and you can create users, change password and login into the application without writing any code in the code behind files where these ASP.NET login controls are used. Isn’t it so easy to use the various ASP.NET login controls once you have extended the MembershipProvider class.

Try to know more

Sandeep

6 comments:

  1. Thanx for the explanation....it is very helpful...

    ReplyDelete
  2. good work man..why don't you provide a source code.It will be helpful..

    ReplyDelete
  3. Hi Raja,
    Good suggestion. As most of the code is pasted in the blog I haven't thought of sharing it. May be in future I will try to do it.

    ReplyDelete
  4. Hi Sandeep,

    Thanks for the nice explanation, correct me If I am wrong, In this article you created a WebserviceAuthentication to use in Custom Provider. Do we need to create WebService based Authentication module? or can we create a separate Code Library and use it in Custom Provider. Please Suggest.

    Thanks
    Sunil K

    ReplyDelete
  5. It was just one of the scenarios which we have come across, where the client already had a web service for authentication and other stuff. We have used the web service in the provider. If you want code library to be used, yes you can very well use it.

    ReplyDelete

Please provide your valuable comments.