Stateless password reset system

It’s pretty common for sites to have a password reset system on websites, which allow you to enter your email address, and have the site email you a link you can use to reset your password.

I had a specification that demanded this kind of system, however it needed to be stateless – in that I could not store any extra information. Also, the hash could only be used once, and it was required that it needed to time out after a configurable time period. Also, the URL must be short enough such that it can be easily copied and pasted into the address bar from an email, and doesn’t wrap in the email.

The code below uses a a few calls to sfConfig::get(), this is just calls to symfony’s configuration system, you can obviously replace these with whatever you like.

As you can see, the hash generated uses the current time (which is included in HEX into the resultant hash), the users current password, the username, and a secret key.

Including these variables in the hash generation means that we can expire the hash, and it will no longer be valid once the user has used it to change a password. The one single downside of this system as I see it, is that once you show the password reset field, you need to have the user input their login name, as this is used as part of the hash generation. For me, this is acceptable.

created);
      $time = dechex((int)$time);
    }

    // we use the password as part of the hash, so that this hash can only be used once.
    $hash = substr(md5(sprintf("%s:%s:%s:%s",
                        $key,
                        $time,
                        $customer->login,
                        $customer->password))
             , 0, self::getHashLength());

    $ret = sprintf('%s%s', $hash, $time);
    return $ret;
  }

  static function validateHash(Customer $customer, $hash)
  {

    $time = substr($hash, self::getHashLength());

    $valid_for = sfConfig::get('app_passwordreset_expire');

    if ((hexdec($time) + $valid_for) < (time() - strtotime($customer->created)))
    {
      return false;
    }

    $hash1 = ForgottenPassword::generateHash($customer, $time);
    if ($hash == $hash1)
    {
      return true;
    }
    else
    {
      return false;
    }
  }
}

As a side note, in the above code I’m substr()ing a MD5 hash. After talking this though online, some people expresses a concern that this was a mistake. Having done some research, I have concluded that every bit of a MD5 hash is as cryptographically important as each other, this means if you want to compromise the strength of the hash, it doesn’t matter if you take bits from the beginning, middle or end of a MD5 hash. It goes without saying though that shortening the hash will make it less secure. The length of the hash you want to use is a configuration option in the above code, and you can use whatever you like. I use 10, as I personally think that’s long enough.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.