17

I'm looking to set up a php password recovery script, using a token which expires after 24 hours. But I'm not sure how to go about it. I have SHA1 encrypted user passwords at the moment. All I want to do I think is append a token to the URL which is sent to the user when they request a password reset. But how do I go about doing this properly and what do I need to store in the database?

Xpleria
  • 5,472
  • 5
  • 52
  • 66
martinmcw
  • 171
  • 1
  • 1
  • 3

4 Answers4

38
  1. When your user requests a password reset, generate a token and calculate its expiry date
  2. Store the token and its expiry date in separate columns in your users table for that user
  3. Send an email to the user containing the reset link, with the token appended to its URL
  4. When your user follows the link, grab the token from your URL (perhaps with $_GET['token'])
  5. Verify the token against your users table
  6. Check that it's not past its expiry date yet
    • If it has expired, invalidate it, perhaps by clearing the fields, and allow the user to resend
  7. If the token is valid and usable, present your password reset form to the user
  8. Validate and update the password and clear the token and expiry fields
BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
  • 4
    Sound advice; however it's probably worth noting here that since such tokens are 'password-equivalent', they should not be stored naked in the database: salt and hash them just as one does with users' passwords. – eggyal Jul 31 '12 at 16:07
  • @eggyal I think the main reason why one hashes passwords is that one cannot simply read the users password in the database and try to use the passowrd on other accounts. I don't see any benefit from hashing the token. Whoever has the link will be able to change the passoword - with or without using hash. – Adam May 31 '18 at 11:56
  • That said, one should make sure that the token is not guessable. Something like `$token = bin2hex(openssl_random_pseudo_bytes(16));` see https://security.stackexchange.com/a/40315/108639 – Adam May 31 '18 at 12:06
  • 1
    @Adam: if an attacker can read the database, they could request a password reset and read the token value to login as the target user. By hashing the token, this attack is no longer feasible (they will only be able to login, as you say, if they intercept the email with the unhashed link). – eggyal May 31 '18 at 12:59
  • @eggyal well if he has access to the database, then its game over anyway. He could simply change the password and login. – Adam May 31 '18 at 13:06
  • 1
    @Adam: only if he has write access, and knows the key used for hashing. – eggyal May 31 '18 at 13:37
10

I would not use a database at all. But one way encryption instead.
Just send necessary information in the hyperlink supplied in the mail, signed by the hash. Something like this

$token = sha1($user_id.$time.$user_pass.$salt).dechex(time()).dechex($user_id);
$link = "http://".$domain."/restorepass/?token=$token";

By receiving it just split and decode it back, and then check hash and timeout.

Your Common Sense
  • 156,878
  • 40
  • 214
  • 345
  • Woow, this is smart:) Not going to use it, but looks good:) Do you actually use it somewhere? – Tom Hert Apr 17 '13 at 21:35
  • Why do you "dechex(time())" ? Why is it better than doing: "?token=$token&time=$time&userid=$user_id" ? – standac Sep 13 '13 at 18:46
  • 4
    I think this is not reliable, because an attacker could modify an old token with a new dechex(time()) to reuse it. – alvaropgl Jul 08 '14 at 11:21
  • Has this method improved at all in the past eight years? Any change to methodology or improved functions? – Stephen R Jun 08 '18 at 21:38
  • @alvaropgl -- I believe what stops an attacker doing what you suggest is that the same "time" value has to be used in the hash and the dechex-ed string at the end. The attacker can't recreate a valid hash with a newer time because he doesn't have the salt or the current password hash. (I believe the $time/time() variance in his example is an error. Those should be the same value.) – Stephen R Jun 08 '18 at 22:02
  • 1
    I think the biggest fault in this method is that the token is not by definition a one-time use token. If an attacker can intercept the URL when the user clicks on it, he can duplicate the click -- and the only limit on this is whether or not it's within the valid timeframe. Password reset tokens really should involve nonces, which in turn probably require something stored in the database that is deleted upon use. – Stephen R Jun 08 '18 at 23:40
3

You need to store a unique token and a token expiry timestamp. When users visits the unique URL you must validate the token, the username and the token expiry timestamp. If everything fine you can send a new password or display a form where user can setup a new password.

fabrik
  • 14,094
  • 8
  • 55
  • 71
  • 1
    I'll add that when the user enters the 'reset' page you get the token and expiry date from DB and check IF it exists and IF it's still valid. If too old, you delete it from DB. If any of these two, you show him error page. Well, this may be obvious :) – Tomasz Struczyński Jul 02 '10 at 10:46
  • Yep, you're right, my solution is a skeleton only so i draw: "If everything fine" – fabrik Jul 02 '10 at 10:48
0

I would go about it by setting up another database called pessword_reset_sessions.

So that you can store the following:

userid generalhash userhash timeinititated attempts

then with user id you isnert the user id obv, with general hash is a hash that is NOT shown to the user but used to create the userhash.

timeinitiated should be a UNIX Timestamp of when he first requested a new password.

once you confirm that the user who is requesting the password has entered the validation info such as email, name, secret question. you create a row within the password reset table.

and issue out an email containing the userhash.

when the hash comes back via the $_GET['hash'] you then create a another hash from the generalhash to compare with the hash that come via $_GET[], if the hash does not match then you increment the attempts

you can also check before to make sure he has not tripped the security for 2 many attempts.

RobertPitt
  • 56,863
  • 21
  • 114
  • 161