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/