Mar
13
2012

How to encrypt a password reset email link in asp.Net MVC

This MVC3 application creates a membership account for visitors when they fill out a form that includes their email address.  The email address becomes their username, and a password is automatically generated and emailed to them:

public static string FriendlyPassword()
        {
            string newPassword = Membership.GeneratePassword(8, 0);
            newPassword = Regex.Replace(newPassword, @"[^a-zA-Z0-9]", m => "9");
            returnnewPassword;
        }

Users can change their password, but I needed a way to let them reset it incase they forgot it.  The application doesn’t collect security questions, and I didn’t want users to be able to enter any email and have the password reset for that account, so I came up with a solution that involved requesting an emailed password reset link that is only valid for X minutes, then clicking that link sends another email with a new password generated using the method above. 

public static string ResetPassword(MembershipUser user)
        {
            string newPass = FriendlyPassword();
            user.ChangePassword(user.ResetPassword(), newPass);
            return newPass;
        }

One advantage with the ResetPassword method above is that neither the user nor the application need know the user’s current password in order to reset it.  The method resets the password, uses that as the current password, then updates the password with a known password that uses a RegEx to remove any characters that are not numbers or letters – for easier typing.

[HttpGet]
        public ActionResult RequestResetPasswordLink()
        {
            return View();
        }

        [HttpPost]
        public ActionResult RequestResetPasswordLink(string userName)
        {
            ViewBag.Message = "Thank you for submitting your request. 
 Please check your email for a reset password link.";

            if(string.IsNullOrEmpty(userName)) return View();

            var existingUser = MembershipHelper.GetUser(userName);

            if (existingUser == null) return View();

            MembershipHelper.SendResetEmail(existingUser);

            return View();
        }

The Account Controller has two actions for requesting the password reset email.  The Get action just returns a view that has a textbox for the user to input their email address, which is their user name.  The Post action uses the viewbag to alert the user that their submission has been processed.  It tells them this same message whether it is a valid email or not so that a hacker cannot use this page to guess valid email addresses.  If the user is found in membership, then it sends the password reset email.

public static void SendResetEmail(MembershipUser user)
        {
            string encrypted = Encryption.Encrypt(String.Format("{0}&{1}",
                user.UserName,
                DateTime.Now.AddMinutes(_minExpires).Ticks),
                ConfigurationManager.AppSettings[Constants.keyEncryptionKey]);

            var passwordLink = ApplicationHelpers.GetBaseURL() + 
                  "Account/ResetPassword?digest=" + 
                  HttpUtility.UrlEncode(encrypted);

            var email = new MailMessage();

            email.From = new MailAddress(admin@domain.com);
            email.To.Add(new MailAddress(user.Email));

            email.Subject = "Password Reset";
            email.IsBodyHtml = true;

            email.Body += "<p>A request has been recieved to reset your password. 
 If you did not initiate the request, then please ignore this email.</p>";
            email.Body += "<p>Please click the following link to reset your password: 
 <a href='" + passwordLink + "'>" + passwordLink + "</a></p>";

            SmtpClient smtpClient = new SmtpClient();

            try
            {
                smtpClient.Send(email);
            }
            catch (Exception ex)
            {
                ErrorHandler.HandleError(ex, ErrorHandler.Level.Error);
            }
        }

The request reset email link uses an encrypted querystring of the format digest=3992023882&user@email.com where the first part of the digest is the ticks of a datetime expriation date that I set to 30 minutes from now, and the second part is the user’s email address.  The encryption utility below is a modification of something I found on Code Project:

public class Encryption
    {
        private const string _defaultKey = "*3ld+43j";


        public static string Encrypt(string toEncrypt, string key)
        {
            var des = new DESCryptoServiceProvider();
            var ms = new MemoryStream();

            VerifyKey(ref key);

            des.Key = HashKey(key, des.KeySize / 8);
            des.IV = HashKey(key, des.KeySize / 8);
            byte[] inputBytes = Encoding.UTF8.GetBytes(toEncrypt);

            var cs = new CryptoStream(ms, des.CreateEncryptor(), CryptoStreamMode.Write);
            cs.Write(inputBytes, 0, inputBytes.Length);
            cs.FlushFinalBlock();

            return HttpServerUtility.UrlTokenEncode(ms.ToArray());
        }

        public static string Decrypt(string toDecrypt, string key)
        {
            var des = new DESCryptoServiceProvider();
            var ms = new MemoryStream();

            VerifyKey(ref key);

            des.Key = HashKey(key, des.KeySize / 8);
            des.IV = HashKey(key, des.KeySize / 8);
            byte[] inputBytes = HttpServerUtility.UrlTokenDecode(toDecrypt);

            var cs = new CryptoStream(ms, des.CreateDecryptor(), CryptoStreamMode.Write);
            cs.Write(inputBytes, 0, inputBytes.Length);
            cs.FlushFinalBlock();

            var encoding = Encoding.UTF8;
            return encoding.GetString(ms.ToArray());

        }

        /// <summary>
        /// Make sure key is exactly 8 characters
        /// </summary>
        /// <param name="key"></param>
        private static void VerifyKey(ref string key)
        {
            if (string.IsNullOrEmpty(key)) 
                key = _defaultKey;

            key = key.Length > 8 ? key.Substring(0, 8) : key;

            if (key.Length < 8)
            {
                for (int i = key.Length; i < 8; i++)
                {
                    key += _defaultKey[i];
                }
            }
        }

        private static byte[] HashKey(string key, int length)
        {
            var sha = new SHA1CryptoServiceProvider();
            byte[] keyBytes = Encoding.UTF8.GetBytes(key);
            byte[] hash = sha.ComputeHash(keyBytes);
            byte[] truncateHash = new byte[length];
            Array.Copy(hash, 0, truncateHash, 0, length);
            return truncateHash;
        }
    }

}

 

When the user gets the email, they click on a link that routes them back to the Account Controller into a ResetPassword action.  And in the MembershipHelper, the validation method decrypts the digest and validates the expiration and username.

[RequireHttps]
        [HttpGet]
        public ActionResult ResetPassword(string digest)
        {
            var parts = MembershipHelper.ValidateResetCode(HttpUtility.UrlDecode(digest));

            if (!parts.IsValid)
            {
                ViewBag.Message = "Invalid or expired link. Please try again";
                return View();
            }

            string newPass = MembershipHelper.ResetPassword(parts.User);

            MembershipHelper.SendNewPasswordEmail(parts.User, newPass);

            ViewBag.Message = "Thank you. Your new password has been emailed to you. 
You may change it after you log in.";
            return View();
        }
 
        public static ResetPasswordParts ValidateResetCode(string encryptedParam)
        {
            string decrypted = "";
            var results = new ResetPasswordParts();

            try
            {
                decrypted = Encryption.Decrypt(encryptedParam, ConfigurationManager
                   .AppSettings[Constants.keyEncryptionKey]);
            }
            catch (Exception ex)
            {
                ErrorHandler.HandleError(ex, ErrorHandler.Level.Information);
                return results;
            }

            var parts = decrypted.Split('&');

            if(parts.Length != 2) return results;

            var expires = DateTime.Now.AddHours(-1);


            results.User = Membership.GetUser(parts[0]);
            if (results.User == null) return results;

            long ticks = 0;
            if(!long.TryParse(parts[1], out ticks)) return results;
            expires = new DateTime(ticks);
            results.Expires = expires;

            if (expires < DateTime.Now) return results;
            results.IsValid = true;

            return results;
        }
 

Finally, the user’s new password is emailed to them with a link to the login page.

 
public static void SendNewPasswordEmail(MembershipUser user, string newPassword)
        {
            MailMessage m = new MailMessage();
            m.From = new MailAddress("admin@domain.com");
            m.To.Add(user.UserName);
            m.Subject = "Your Credentials";
            var body = @"Your password has been reset. You can change this password once you have 
logged in. Your user name is: {0} Your reset password is: {1} To login, 
go to: http://domain.com/Account/LogOn";

            m.Body = string.Format(body, user.UserName, newPassword);

            SmtpClient c = new SmtpClient();
            try
            {
                c.Send(m);
            }
            catch (Exception ex)
            {
                ErrorHandler.HandleError(string.Format(

"Error sending welcome email to {0}: {1}",
                    user.UserName, ex.Message), ErrorHandler.Level.Error);
            }
            finally
            {
                m.Dispose();
                c.Dispose();
            }
        }
blog comments powered by Disqus

Tweets

Note: For Customization and Configuration, CheckOut Recent Tweets Documentation

Creative Commons License