Last week, I talked about how to create a session-based login script in PHP. A common feature on these sorts of scripts is a password reset.
Now…
There’s lots of ways to do a password reset. You’ve likely seen many of them. But, I strongly recommend you use an email-based system.
Other systems are 100% dependent on your own system for their security and will make you the primary target of any attacks.
But, with an email-based system… if you secure it properly… a hacker would need to hack the user’s email client. Thus, adding an extra layer of security to your password reset.
With that said, let’s cover how to do it.
First, you need a database table to handle auth tokens for your password reset. Something like this:
<?php $db->query( "CREATE TABLE IF NOT EXISTS password_reset ( ID INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255), selector CHAR(16), token CHAR(64), expires BIGINT(20) )");
Having “selector” AND “token” is important to prevent timing attacks. See here for more info on that.
Then, you need a reset form to process the reset request. Like this:
<form action="" method="post"> <input type="text" class="text" name="email" placeholder="Enter your email address" required> <input type="submit" class="submit" value="Submit"> </form>
Next, create our auth token and save it into the database like so:
<?php // Create tokens $selector = bin2hex(random_bytes(8)); $token = random_bytes(32); $url = sprintf('%sreset.php?%s', ABS_URL, http_build_query([ 'selector' => $selector, 'validator' => bin2hex($token) ])); // Token expiration $expires = new DateTime('NOW'); $expires->add(new DateInterval('PT01H')); // 1 hour // Delete any existing tokens for this user $this->db->delete('password_reset', 'email', $user->email); // Insert reset token into database $insert = $this->db->insert('password_reset', array( 'email' => $user->email, 'selector' => $selector, 'token' => hash('sha256', $token), 'expires' => $expires->format('U'), ) );
There’s a few things going on here and this code is derived from this post, but the basic idea is to create a random token using random_bytes(), create our URL for email, then hash and store the token in our password_reset table so we can verify incoming password resets against it.
Now, we need to send the email. Like this:
<?php // Send the email // Recipient $to = $user->email; // Subject $subject = 'Your password reset link'; // Message $message = '<p>We recieved a password reset request. The link to reset your password is below. '; $message .= 'If you did not make this request, you can ignore this email</p>'; $message .= '<p>Here is your password reset link:</br>'; $message .= sprintf('<a href="%s">%s</a></p>', $url, $url); $message .= '<p>Thanks!</p>'; // Headers $headers = "From: " . ADMIN_NAME . " <" . ADMIN_EMAIL . ">\r\n"; $headers .= "Reply-To: " . ADMIN_EMAIL . "\r\n"; $headers .= "Content-type: text/html\r\n"; // Send email $sent = mail($to, $subject, $message, $headers);
At this point, we have database entry with our token (hashed) in it AND an email to the user with the token in the URL for them to click.
Now, we need the page the URL in the email points to and it needs to handle the reset request. That looks like this:
<?php // Check for tokens $selector = filter_input(INPUT_GET, 'selector'); $validator = filter_input(INPUT_GET, 'validator'); if ( false !== ctype_xdigit( $selector ) && false !== ctype_xdigit( $validator ) ) : ?> <form action="reset_process.php" method="post"> <input type="hidden" name="selector" value="<?php echo $selector; ?>"> <input type="hidden" name="validator" value="<?php echo $validator; ?>"> <input type="password" class="text" name="password" placeholder="Enter your new password" required> <input type="submit" class="submit" value="Submit"> </form> <p><a href="index.php">Login here</a></p> <?php endif; ?>
Then, we need to validate the tokens and reset the password if they’re valid:
<?php // Get tokens $results = $this->db->get_results("SELECT * FROM password_reset WHERE selector = :selector AND expires >= :time", ['selector'=>$selector,'time'=>time()]); if ( empty( $results ) ) { return array('status'=>0,'message'=>'There was an error processing your request. Error Code: 002'); } $auth_token = $results[0]; $calc = hash('sha256', hex2bin($validator)); // Validate tokens if ( hash_equals( $calc, $auth_token->token ) ) { $user = $this->user_exists($auth_token->email, 'email'); if ( false === $user ) { return array('status'=>0,'message'=>'There was an error processing your request. Error Code: 003'); } // Update password $update = $this->db->update('users', array( 'password' => password_hash($password, PASSWORD_DEFAULT), ), $user->ID ); // Delete any existing password reset AND remember me tokens for this user $this->db->delete('password_reset', 'email', $user->email); $this->db->delete('auth_tokens', 'username', $user->username); if ( $update == true ) { // New password. New session. session_destroy(); return array('status'=>1,'message'=>'Password updated successfully. <a href="index.php">Login here</a>'); } }
Here we grab the token, validate it and then allow the reset if it’s valid. We also make sure to delete the token from the database, so it can’t be used again.
So, that’s it. If you want to keep going with this tutorial, you can on my free tutorial site here: https://johnsfreetuts.com/logintut/
This Post Has 20 Comments
This line if ( hash_equals( $calc, $auth_token->token ) ) { returns false in my own case. what could be the reason for that?
Well, that’s checking to see if what’s stored in the database matches what was sent from the link in the email for the reset. So, false means they don’t match for some reason. Why? Couldn’t 100% tell you. Depends what’s going on deeper down in your script.
I found your script through Google. You’re using stuff from your own situation so I translated it to my own setup. Just a few questions:
$auth_token = $results[0];
What contains the $result[0], so I can match it with my setup?
if ( hash_equals( $calc, $auth_token->token
What contains the $auth_token->token ?
I hope you can answer those 2 questions. After that I will be able to have the script working on my setup 🙂
$results comes from a database query. You’ll see it at the top of the last code snippet. $auth_token is drived from $results so it all comes from the same place. Basically, you’re querying the database to get the auth_token stored there and checking it against what was submitted by the user.
$user = $this->user_exists($auth_token->email, ’email’);
what is this line checking? is it checking if the email in the database ($auth_token->email) matches the user email (’email’) trying to reset the password? if so how are you getting the user email (’email’) value ?
$auth_token is the token record from the database (see the first chunk of code on this page). That record includes the email address of the user we’re resetting. user_exists() checks to see if that user exists in our user table and if so, grabs that user record. Which is why it’s set equal to $user.
Thank you for this ????
Hi, is there a way to prevent users from directly accessing the reset password page through url and only can be redirected from the email’s url with token? Thanks for this guide!
Just check for the tokens in the URL and if they’re not there, redirect the user somewhere.
i don’t know how to use this
This is the greatest thing ever, keep it up.
Word!
Hello,
My question is related to this line: // Delete any existing tokens for this user
$this->db->delete(‘password_reset’, ’email’, $user->email);
So, in my files I store db connection data in $con, so how I should modify this line to make it work?
I have already tried to replace $this to $con but I got back fatal error with this line, plus I don’t really understand these -> arrays what they actually do.
Thanks for your help 🙂
$this->db is referencing another class that’s extended by the one all this code is in. delete() is method in that class. So, if you’re storing your connection data in $con, you not only need to completely replace $this->db, but you either need to write a delete() function/method and use that in it’s place… something like $con->delete(). OR, you just write your delete code out here.
Very nice tutorial. Thank you!
i may be missed some point. But i dont find where u declare variable $url, where do u set that and what piece on there?
from what i guess, its “your_url?selector=$selector&validator=$validator, isnt it?
where do u set $validator from?
thanks for your help
This was very helpful, thank you.
No problem!
Really useful post from which I now have a well implemented password reset process. Thanks.
First: Thank you very much. It seems to be the best option I’ve seen so far and I implemented it.
I find it very hard to understand for a beginner actually. I think many things could have been simplified a lot. For e.g. you used “sprintf(‘%sreset.php?%s’ …” and ‘string1’.’string1′ would be almost the same in this case if I’m not wrong but way easier to unterstand in my opinion. Or in the last post it is not quite clear where the vars $validator and $selector come from (the post values). And in one half you name it token in the other it’s validator.
And I didn’t unterstand this sentence
// Delete any existing password reset AND remember *me* tokens for this user
and then this:
$this->db->delete(‘auth_tokens’, ‘username’, $user->username);
Where is the table auth_tokens filled?
But these are small things. Again thanks it helped me a lot!