Continued from: ASP.NET MVC 2 Custom Membership Provider Tutorial – Part 2
In the previous part of the tutorial we implemented CreateUser method which successfully creates new user in the database.
First thing we want to do now is to generate password salt.
Open UserRepository.cs, and add a reference to System.Security.Cryptography:
using System.Security.Cryptography;
and function to generate the salt (within UserRepository class):
private static string CreateSalt() { RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); byte[] buff = new byte[32]; rng.GetBytes(buff); return Convert.ToBase64String(buff); }
Finally,change the CreateUser method to generate and save the salt to database with other user data.
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 = CreateSalt(); 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 go ahead and try it. Run your application and register new user account. If you check the database you will see random salt generated during the registration.
Now, let’s hash the password. Add the following code to UserRepository class:
private static string CreatePasswordHash(string pwd, string salt) { string saltAndPwd = String.Concat(pwd, salt); string hashedPwd = FormsAuthentication.HashPasswordForStoringInConfigFile( saltAndPwd, "sha1"); return hashedPwd; }
and modify CreateUser method again to store hashed password. Because we’re going to useuser.passwordSalt property as an argument in CreatePasswordHash method we need to reorder the properties so the salt gets generated before the password is hashed:
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.PasswordSalt = CreateSalt(); user.Password = CreatePasswordHash(password, user.PasswordSalt); 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); } }
I am aware that this is probably not the best method to hash the password and that the hashing and salt generating methods can be improved but this is not the point of this tutorial so I’ll leave it as it is.
The UserRepository.cs file up to this point in the tutorial can be downloaded here:
We now have hashed password and salt stored in the database so let’s implement ValidateUser method properly.
Create ValidateUser method in UserRepository class:
public bool ValidateUser(string username, string password) { 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.First(); if (dbuser.Password == CreatePasswordHash(password, dbuser.PasswordSalt)) return true; else return false; } else { return false; } } }
and modify ValidateUser method in MyMembershipProvider class:
public override bool ValidateUser(string username, string password) { UserRepository _user = new UserRepository(); return _user.ValidateUser(username, password); }
At this point we can run the application, register new account, log off and log in with it.
By the way, let’s tweak our application so it doesn’t log newly registered users in after the registration automatically.
We will want our users to activate the account by clicking the link in an email. Until activated user will not be able to log in (we will implement that in a second too).
Let’s create a View that will be displayed to users after the registration.
Open HomeController.cs file, and add Welcome method:
public ActionResult Welcome() { return View(); }
Right click on Welcome() and Add View…
In the View, add a message to newly registered users:
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <h2>Welcome</h2> <p>Thank you for registering!</p> <p>Activation email has been sent to you. Click on the link in the email to activate your account!</p> </asp:Content>
Now, open AccountController.cs file, locate Register method and change:
if (createStatus == MembershipCreateStatus.Success) { FormsService.SignIn(model.UserName, false /* createPersistentCookie */); return RedirectToAction("Index", "Home"); }
so it looks like this:
if (createStatus == MembershipCreateStatus.Success) { return RedirectToAction("Welcome", "Home"); }
If we run our application now and register new account we will not end up being logged in and we will be redirected to the Welcome page. Great!
Let’s generate email activation key and store it with the user data in the database.
Add the following method to UserRepository class:
private static string GenerateKey() { Guid emailKey = Guid.NewGuid(); return emailKey.ToString(); }
and modify the CreateUser method so the key is saved to the database (In the NewEmailKey column):
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.PasswordSalt = CreateSalt(); user.Password = CreatePasswordHash(password, user.PasswordSalt); user.CreatedDate = DateTime.Now; user.IsActivated = false; user.IsLockedOut = false; user.LastLockedOutDate = DateTime.Now; user.LastLoginDate = DateTime.Now; user.NewEmailKey = GenerateKey(); db.AddToUsers(user); db.SaveChanges(); return GetUser(username); } }
Now, the activation key is generated we will email the activation link to the user.
For the purpose of this tutorial I will implement the email code directly into UserRepository class. In real world scenario you would ideally want to keep the email class separate. You would also want to add some sort of error handling for the email class.
You will also need an SMTP server that allows relaying messages. When you run the application on the Web Server from an ISP, usually it has SMTP role installed and you can use localhost as a server. This isn’t the case with Web Server installed with Visual Web Developer so you will need to find a server which will allow the application to send emails.
First, add a reference to System.Net.Mail namespace (UserRepository.cs):
using System.Net.Mail;
In CreateUser method, add the following code:
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.PasswordSalt = CreateSalt(); user.Password = CreatePasswordHash(password, user.PasswordSalt); user.CreatedDate = DateTime.Now; user.IsActivated = false; user.IsLockedOut = false; user.LastLockedOutDate = DateTime.Now; user.LastLoginDate = DateTime.Now; user.NewEmailKey = GenerateKey(); db.AddToUsers(user); db.SaveChanges(); string ActivationLink = "http://localhost:PORT/Account/Activate/" + user.UserName + "/" + user.NewEmailKey; var message = new MailMessage("EMAIL_FROM", user.Email) { Subject = "Activate your account", Body = ActivationLink }; var client = new SmtpClient("SERVER"); client.Credentials = new System.Net.NetworkCredential("USERNAME", "PASSWORD"); client.UseDefaultCredentials = false; client.Send(message); return GetUser(username); } }
You will need to replace PORT (with your local application port name or remove if on live server), EMAIL_FROM, SERVER, USERNAME and PASSWORD values.
If during registration process the application throws an exception like this:
That means that the server doesn’t allow relaying and you need to find another server (not easy I know).
Once you’ll get past this step (manually removing all created up to this point accounts from the database), you will receive an email with the link.
So let’s add the activation functionality to the AccountController.
Open AccountController.cs and add the following code:
// ************************************** // URL: /Account/Activate/username/key // ************************************** public ActionResult Activate(string username, string key) { UserRepository _user = new UserRepository(); if (_user.ActivateUser(username, key) == false) return RedirectToAction("Index", "Home"); else return RedirectToAction("LogOn"); }
For the activation Url to work we will need to edit Global.asax.cs file and add the following route:
routes.MapRoute( "Activate", "Account/Activate/{username}/{key}", new { controller = "Account", action = "Activate", username = UrlParameter.Optional, key = UrlParameter.Optional } );
And finally, ActivateUser method in UserRepository class:
public bool ActivateUser(string username, string key) { 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.First(); if (dbuser.NewEmailKey == key) { dbuser.IsActivated = true; dbuser.LastModifiedDate = DateTime.Now; dbuser.NewEmailKey = null; db.SaveChanges(); return true; } else { return false; } } else { return false; } } }
Now, let’s modify ValidateUser method so it will only authenticate activated users:
public bool ValidateUser(string username, string password) { 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.First(); if (dbuser.Password == CreatePasswordHash(password, dbuser.PasswordSalt) && dbuser.IsActivated == true) return true; else return false; } else { return false; } } }
Run the application and register 2 user accounts. Validate one of them using the link from the email. You will be able to log in only using the activated account.
In the next part of the tutorial we will add some error handling as well as some Views to support implemented processes.
Sorry folks! Due to new role I have taken, I won’t be able to continue this tutorial any time soon.
Files at the end of this part of the tutorial:
No comments:
Post a Comment