83

I'm working on an application in ASP.NET, and was wondering specifically how I could implement a Password Reset function if I wanted to roll my own.

Specifically, I have the following questions:

  • What is a good way of generating a Unique ID that is hard to crack?
  • Should there be a timer attached to it? If so, how long should it be?
  • Should I record the IP address? Does it even matter?
  • What information should I ask for under the "Password Reset" screen ? Just Email address? Or maybe email address plus some piece of information that they 'know'? (Favorite team, puppy's name, etc)

Are there any other considerations I need to be aware of?

NB: Other questions have glossed over technical implementation entirely. Indeed the accepted answer glosses over the gory details. I hope that this question and subsequent answers will go into the gory details, and I hope by phrasing this question much more narrowly that the answers are less 'fluff' and more 'gore'.

Edit: Answers that also go into how such a table would be modeled and handled in SQL Server or any ASP.NET MVC links to an answer would be appreciated.

Community
  • 1
  • 1
George Stocker
  • 57,289
  • 29
  • 176
  • 237
  • ASP.NET MVC uses the default ASP.NET authentication provider, so any code samples you find around that sshould till be relevant for your purposes. – paulwhit Apr 03 '09 at 17:04

7 Answers7

68

EDIT 2012/05/22: As a follow-up to this popular answer, I no longer use GUIDs myself in this procedure. Like the other popular answer, I now use my own hashing algorithm to generate the key to send in the URL. This has the advantage of being shorter as well. Look into System.Security.Cryptography to generate them, which I usually use a SALT as well.

First, do not immediately reset the user's password.

First, do not immediately reset the user's password when they request it. This is a security breach as someone could guess email addresses (i.e. your email address at the company) and reset passwords at whim. Best practices these days usually include a "confirmation" link sent to the user's email address, confirming they want to reset it. This link is where you want to send the unique key link. I send mine with a link like: example.com/User/PasswordReset/xjdk2ms92

Yes, set a timeout on the link and store the key and timeout on your backend (and salt if you are using one). Timeouts of 3 days is the norm, and make sure to notify the user of 3 days at the web level when they request to reset.

Use a unique hash key

My previous answer said to use a GUID. I'm now editing this to advise everyone to use a randomly generated hash, e.g. using the RNGCryptoServiceProvider. And, make sure to eliminate any "real words" from the hash. I recall a special 6am phone call of where a woman received a certain "c" word in her "suppose to be random" hashed key that a developer did. Doh!

Entire procedure

  • User clicks "reset" password.
  • User is asked for an email.
  • User enters email and clicks send. Do not confirm or deny the email as this is bad practice as well. Simply say, "We have sent a password reset request if the email is verified." or something cryptic alike.
  • You create a hash from the RNGCryptoServiceProvider, store it as a separate entity in an ut_UserPasswordRequests table and link back to the user. So this so you can track old requests and inform the user that older links has expired.
  • Send the link to the email.

User gets the link, like http://example.com/User/PasswordReset/xjdk2ms92, and clicks it.

If the link is verified, you ask for a new password. Simple, and the user gets to set their own password. Or, set your own cryptic password here and inform them of their new password here (and email it to them).

Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
eduncan911
  • 17,165
  • 13
  • 68
  • 104
  • 1
    I Was Wondering, If the actual user password is hashed, why generate a new HASH Key? Wouldn't be correct to send an email to user with a link to reset password passing the Hashed password? The hashed password can't be reverted, when the user clicks on the link, the server will receive the hashed password, compare with the actual stored, and then allow the user to change password. – Daniel Sep 02 '15 at 20:49
  • And Another prettly good thing about it, is that you don't need to set Timeout, once the user has changed the password, the old link will automatically not be valid any more, because the hashed password stored in database was changed. – Daniel Sep 02 '15 at 20:52
  • 1
    @Daniel that's a really bad idea. I think you need to Google the term "brute force attacks." Also, the reason you DO want it to expire is in case someone's email is compromised a year down the road (and they never reset it), the hacker gains rights to change password. – eduncan911 Sep 03 '15 at 00:31
  • @educan911. I Know brute force attacks, but also, to have access to the Hashed key, the Bad Intented person, has to have access to the email, and if he has access to that, there is no need to revert the hashed password. Also, to make it almost impossible, you could hash the hashed password, or even better, hash the password with something more. I´m not disagreeing with you, I´m just trying to do a Brainstorm about it – Daniel Sep 03 '15 at 15:29
66

Lots of good answers here, I wont bother repeating it all...

Except for one issue, which is repeated by almost every answer here, even though its wrong:

Guids are (realistically) unique and statistically impossible to guess.

This is not true, GUIDs are very weak identifiers, and should NOT be used to allow access to a user's account.
If you examine the structure, you get a total of 128 bits at most... which is not considered a lot nowadays.
Out of which the first half is typical invariant (for the generating system), and half of whats left is time-dependant (or something else similar).
All in all, its a very weak and easily bruteforced mechanism.

So don't use that!

Instead, simply use a cryptographically strong random number generator (System.Security.Cryptography.RNGCryptoServiceProvider), and get at least 256 bits of raw entropy.

All the rest, as the numerous other answers provided.

Machado
  • 8,965
  • 6
  • 43
  • 46
AviD
  • 12,944
  • 7
  • 61
  • 91
  • 6
    Absolutely agree, as far as I know, GUIDs were never designed to be cryptographically strong and impossible to guess. – Jan Soltis Apr 04 '09 at 07:51
  • 5
    well said, AFAIK MSDN clearly states that GUID should not be used for security. – dr. evil Apr 04 '09 at 10:30
  • 2
    Version 4 UUIDs have been used in Windows since 2000: [How are .NET 4 GUIDs generated? - Stack Overflow](http://stackoverflow.com/questions/2757910/how-are-net-4-guids-generated). They have 122 random bits in them, which I think conforms with NIST recommendations. There was a very bad vulnerability to a local attack, which according to [CryptGenRandom - Wikipedia](http://en.wikipedia.org/wiki/CryptGenRandom#Hebrew_University_Cryptanalysis) was fixed in Vista and XP by 2008. So where do you see problems with current use of GUIDs? – nealmcb Jan 30 '11 at 17:45
  • Hi @nealmcb, that's 122 bits of *uniqueness*, and **not** randomness. Linked from that question you linked, is a very good breakdown of the structure, which has basically *. **14 bits** .* of randomness. Not nearly enough, not by a long shot... As one of the commenters said on Raymond's blog there: "GUIDs are not defined to be random; they are defined to BE UNIQUE in space & time." – AviD Jan 30 '11 at 18:44
  • I'm not seeing what you're referring to. But the standard is pretty clear: "[except for 6 bits] Set all the other bits to randomly (or pseudo-randomly) chosen values." http://tools.ietf.org/html/rfc4122#page-14 And from what I've seen, on a patched XP and later at least the values come from CryptGenRandom which seems like the same underlying source you refer to. So how much does 256 vs 122 bits gain you? – nealmcb Jan 30 '11 at 18:57
  • I was refering to this: http://blogs.msdn.com/b/oldnewthing/archive/2008/06/27/8659071.aspx. And while I consider CryptGenRandom to be (relatively) secure, it affects only a small part of GUID, as far as I know. – AviD Jan 30 '11 at 19:06
  • See also MS on the subject: "4: Random version. Use random numbers for all sections" at [Generating GUIDs on the Pocket PC](http://msdn.microsoft.com/en-us/library/aa446557.aspx) – nealmcb Jan 30 '11 at 19:06
  • 4
    That "Old New Thing" blog is describing deprecated version 1 UUIDs, and cites an Internet Draft (something you're never supposed to do) which expired in 1998, 10 years before the blog post. I'd be skeptical of them in the future. We fought those battles long ago, and seem to have won most of them. I still agree that using a clean API call to a crypto-random source is much better, but don't be quite so hard on GUID/UUIDs at version 4. – nealmcb Jan 30 '11 at 19:11
  • @nealmcb, hmm, now I have conflicting information... Will look into this some more. – AviD Jan 30 '11 at 19:11
  • @nealmcb, this is odd - that pocketpc article (which is also WELL out of date, and oddly incongruous with typical MSDN recommendations) has its own internal contradictions - it first cites the fixed structure, including leaving just 6 bytes for "spatial uniqueness", and all the rest predefined - and then it goes on to randomize the whole thing except for version. So... As you say, v4 UUID is not as bad as all that, but I'm still not sure that `Guid.NewGUID()` uses v4... – AviD Jan 30 '11 at 19:18
  • 1
    For what its worth, this does not answer the question, "how how to reset a password". You just vomited great points about GUIDs. – Rex Whitten Jul 15 '14 at 15:01
  • @starfighterxyz the word `vomited` made me feel bad. He posted this answer to point out a potential security hole of using GUID, and did mention that all other answers have good flow, but just to change the random number generator to something designed for security. I don't see anything improper here. – nevets Apr 12 '16 at 02:59
8

First, we need to know what you already know about the user. Obviously, you have a username and an old password. What else do you know? Do you have an email address? Do you have data regarding the user's favorite flower?

Assuming you have a username, password and working email address, you need to add two fields to your user table (assuming it is a database table): a date called new_passwd_expire and a string new_passwd_id.

Assuming you have the user's email address, when someone requests a password reset, you update the user table as follows:

new_passwd_expire = now() + some number of days
new_passwd_id = some random string of characters (see below)

Next, you send an email to the user at that address:

Dear so-and-so

Someone has requested a new password for user account <username> at <your website name>. If you did request this password reset, follow this link:

http://example.com/yourscript.lang?update=&lt;new\_password\_id>

If that link does not work you can go to http://example.com/yourscript.lang and enter the following into the form: <new_password_id>

If you did not request a password reset, you may ignore this email.

Thanks, yada yada

Now, coding yourscript.lang: This script needs a form. If the var update passed on the URL, the form just asks for the user's username and email address. If update is not passed, it asks for username, email address, and the id code sent in the email. You also ask for a new password (twice of course).

To verify the user's new password, you verify the username, email address, and the id code all match, that the request has not expired, and that the two new passwords match. If successful, you change the user's password to the new password and clear the password reset fields from the user table. Also be sure to log the user out/clear any login related cookies and redirect the user to the login page.

Essentially, the new_passwd_id field is a password that only works on the password reset page.

One potential improvement: you could remove <username> from the email. "Someone has request a password reset for an account at this email address...." Thus making the username something only the user knows if the email is intercepted. I didn't start off that way because if someone is attacking the account, they already know the username. This added obscurity stops man-in-the-middle attacks of opportunity in case someone malicious happens to intercept the email.

As for your questions:

generating the random string: It doesn't need to be extremely random. Any GUID generator or even md5(concat(salt, current_timestamp())) is sufficient, where salt is something on the user record like timestamp account was created. It has to be something the user can't see.

timer: Yes, you need this just to keep your database sane. No more than a week is really necessary but at least 2 days since you never know how long an email delay might last.

IP Address: Since the email could be delayed by days, IP address is only useful for logging, not for validation. If you want to log it, do so, otherwise you don't need it.

Reset Screen: See above.

Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
jmucchiello
  • 18,754
  • 7
  • 41
  • 61
  • Wouldn't a potential attacker be able to use the MD5 of the current datestamp to get in? – George Stocker Apr 02 '09 at 18:38
  • I would strongly recommend against sending a password in email over the wire. Most of the users leave these emails undeleted which is a security breach - some of them will like to just copy-paste it every time from their 'favourite' emails. What if the certificate of the users company mail server is expired and the traffic is sniffed? To minimize this possible breach is to (1) set a short expiration time of this particular password - 1 hour, and (2) force the user to update it on the next log on. – Ognyan Dimitrov Dec 05 '14 at 12:45
  • Ognyan, the password sent in email only works once. They have to change their password after login and the email does not contain the user login name. So, no they cannot just copy-paste it every time. Not deleting the email is not a security issue since it is just a meaningless string of letters/numbers that will gain the attacker NOTHING after the password is reset. – jmucchiello Dec 05 '14 at 18:44
3

A GUID sent to the email address of record is likely enough for most run-of-the-mill applications - with timeout even better.

After all, if the users emailbox has been compromised(i.e. a hacker has the logon/password for the email address), there is not much you can do about that.

E.J. Brennan
  • 45,870
  • 7
  • 88
  • 116
2

You could send an email to user with a link. This link would contain some hard to guess string (like GUID). On server side you would also store the same string as you sent to user. Now when user presses on link you can find in your db entry with a same secret string and reset its password.

Sergej Andrejev
  • 9,091
  • 11
  • 71
  • 108
2

1) For generating the unique id you could use Secure Hash Algorithm. 2) timer attached? Did you mean an Expiry for the reset pwd link? Yes you can have an Expiry set 3) You can ask for some more information other than the emailId to validate.. Like date of birth or some security questions 4) You could also generate random characters and ask to enter that also along with the request.. to make sure the password request is not automated by some spyware or things like that..

Java Guy
  • 3,391
  • 14
  • 49
  • 55
0

I think Microsoft guide for ASP.NET Identity is a good start.

https://learn.microsoft.com/en-us/aspnet/identity/overview/features-api/account-confirmation-and-password-recovery-with-aspnet-identity

Code that I use for ASP.NET Identity:

Web.Config:

<add key="AllowedHosts" value="example.com,2.example" />

AccountController.cs:

[Route("RequestResetPasswordToken/{email}/")]
[HttpGet]
[AllowAnonymous]
public async Task<IHttpActionResult> GetResetPasswordToken([FromUri]string email)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    var user = await UserManager.FindByEmailAsync(email);
    if (user == null)
    {
        Logger.Warn("Password reset token requested for non existing email");
        // Don't reveal that the user does not exist
        return NoContent();
    }

    //Prevent Host Header Attack -> Password Reset Poisoning.
    //If the IIS has a binding to accept connections on 80/443 the host parameter can be changed.
    //See https://security.stackexchange.com/a/170759/67046
    if (!ConfigurationManager.AppSettings["AllowedHosts"].Split(',').Contains(Request.RequestUri.Host)) {
            Logger.Warn($"Non allowed host detected for password reset {Request.RequestUri.Scheme}://{Request.Headers.Host}");
            return BadRequest();
    }

    Logger.Info("Creating password reset token for user id {0}", user.Id);

    var host = $"{Request.RequestUri.Scheme}://{Request.Headers.Host}";
    var token = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
    var callbackUrl = $"{host}/resetPassword/{HttpContext.Current.Server.UrlEncode(user.Email)}/{HttpContext.Current.Server.UrlEncode(token)}";

    var subject = "Client - Password reset.";
    var body = "<html><body>" +
               "<h2>Password reset</h2>" +
               $"<p>Hi {user.FullName}, <a href=\"{callbackUrl}\"> please click this link to reset your password </a></p>" +
               "</body></html>";

    var message = new IdentityMessage
    {
        Body = body,
        Destination = user.Email,
        Subject = subject
    };

    await UserManager.EmailService.SendAsync(message);

    return NoContent();
}

[HttpPost]
[Route("ResetPassword/")]
[AllowAnonymous]
public async Task<IHttpActionResult> ResetPasswordAsync(ResetPasswordRequestModel model)
{
    if (!ModelState.IsValid)
        return NoContent();

    var user = await UserManager.FindByEmailAsync(model.Email);
    if (user == null)
    {
        Logger.Warn("Reset password request for non existing email");
        return NoContent();
    }

    if (!await UserManager.UserTokenProvider.ValidateAsync("ResetPassword", model.Token, UserManager, user))
    {
        Logger.Warn("Reset password requested with wrong token");
        return NoContent();
    }

    var result = await UserManager.ResetPasswordAsync(user.Id, model.Token, model.NewPassword);

    if (result.Succeeded)
    {
        Logger.Info("Creating password reset token for user id {0}", user.Id);

        const string subject = "Client - Password reset success.";
        var body = "<html><body>" +
                   "<h1>Your password for Client was reset</h1>" +
                   $"<p>Hi {user.FullName}!</p>" +
                   "<p>Your password for Client was reset. Please inform us if you did not request this change.</p>" +
                   "</body></html>";

        var message = new IdentityMessage
        {
            Body = body,
            Destination = user.Email,
            Subject = subject
        };

        await UserManager.EmailService.SendAsync(message);
    }

    return NoContent();
}

public class ResetPasswordRequestModel
{
    [Required]
    [Display(Name = "Token")]
    public string Token { get; set; }

    [Required]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 10)]
    [DataType(DataType.Password)]
    [Display(Name = "New password")]
    public string NewPassword { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm new password")]
    [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}
Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
Ogglas
  • 62,132
  • 37
  • 328
  • 418