Home    posts    php password reset

Posted on: December 23, 2017

Create a password reset using PHP and MYSQL

So in this tutorial, which will basically be a follow up to how to create a registration form, we will take a look at how to reset a password from already defined MYSQL table called sql_users. For this purpose, another table will be added that will handle password reset tokens which will only be active for 12 hours or until the password was successfuly reset. In the latter case, the token record will be erased from the table after the password is updated. First, you'll need to add the following table to the database. Note that token is set as char 64 because all the tokens will be hashed with PBKDF2 method using 20k rounds, email of the user as salt and randomly generated token as key. It will be saved as 32byte or 64 char HEX output.


CREATE TABLE `password_reset` (
  `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT
  `token` char(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL
  `uid` int(11) NOT NULL UNIQUE KEY
  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin

The following tutorial will be done in 3 parts, in the first part we will create 2 functions that will both return an array of variables which will then be unpacked, one in main file and one in reset file, where they will serve a purpose. The second file will be the main file where a basic IF, ELSEIF, ELSE condition structure will be created to differentiate between logging in, admin part, reset request form, reset token generation and displaying the login form. In the third and final part, a file, that will handle identifying user, token processing and reseting the password, will be created.

PART 1 - Functions to handle conditions (con_functions.php)

Here, we'll make two functions, one for reset request that will make a query based on input to find a match among usernames or passwords and return the email of the user as salt and id as uid. The latter will be used to check whether or not the token already exists for the user that a request was made for and, if so, return tokenerror. Furthermore, it will also check whether the username or email, based on input, exists in DB and if the captcha code match. The second function will check for errors when applying a new password, basically again if username or email exists, then if password input is empty, longer than 8 chars and if new password and confirm password match. Also if captcha code matches.


<?php

// RESET REQUEST FUNCTION
// $mysqli is passed in the function from config file included in the main file.
function resets($mysqli) {

	// Escape the input first, then query among both usernames and emails to potentially find a match of the input.
	// One has to be true to find a row. Both are unique columns in DB
	$usermail = mysqli_real_escape_string($mysqli, $_POST['usermail']);
	$query = mysqli_query($mysqli, "SELECT * FROM sql_users WHERE username = '$usermail' OR email = '$usermail'");
	$rows = mysqli_num_rows($query);
	$fetch = mysqli_fetch_array($query);

	// Get the row's columns into variables
	$salt = $fetch['email'];
	$uid = $fetch['id'];

	// Query to find a row from password_reset table where uid matches id of the user in sql_users table
	// Save it to variable as numerical value
	$tokencheck = mysqli_query($mysqli, "SELECT * FROM password_reset WHERE uid = '$uid' AND timestamp > (now()  interval 12 hour)");
	$tokenrow = mysqli_num_rows($tokencheck);

	// When the reset request button is clicked on, execute bellow...
	if(isset($_POST['resets'])) {

		 If a $tokenrow exists in password_reset table, display already exists error.
		if($tokenrow !== 0) {$tokenerr = '<span style="color:red;">Password reset was already requested! Check your email for details.</span>';}
		else {

			// Check also for empty input and whether the username or email(for which the reset request was made) exists
			if(empty($_POST['usermail'])) {$usermailerr = '<span style="color:red;">You have to provide either username or email!</span>';}			
			elseif($rows === 0) {$usermailerr = '<span style="color:red;">No such username or email exists in the database!</span>';}

			// Check for a Captcha match from the user input and from the one save in a session
			if(hash('sha256', $_POST['resverify']) !== $_SESSION['code']) {$codeserr = '<span style="color:red;">The verification code does not match!</span>';}
		}

	// Return an array of variables to be unpacked in the main file
	return array($salt, $uid, $usermailerr, $codeserr, $tokenerr);
	}
}

// PASSWORD RESET FUNCTION
function newpass($mysqli) {

	// Escape the input first, then query among both usernames and emails to potentially find a match of the input.
	$mailuser = mysqli_real_escape_string($mysqli, $_POST['mailuser']);
	$query = mysqli_query($mysqli, "SELECT * FROM sql_users WHERE username = '$mailuser' OR email = '$mailuser'");
	$rows = mysqli_num_rows($query);

	// If reset button in reset.php file is clicked on, execute below...
	if(isset($_POST['reset'])) {

		// If there are no rows that match the above query, create an error
		if($rows === 0) {$mailusererr = '<span style="color:red;">No such username or email exists in the database!</span>';}

		// Check for empty input, check whether the input was longer than 8 chars and if the password input and confirm password input match
		if(empty($_POST['npassw'])) {$npasserr = '<span style="color:red;">Password field is empty!</span>';}
		elseif(strlen($_POST['npassw']) < 8) {$npasserr = '<span style="color:red;">Password too short! It needs to be at least 8 chars long.</span>';}
		elseif($_POST['npassw'] !== $_POST['copassw']) {$copasserr = '<span style="color:red;">Passwords do not match!</span>';}

		// Check for a Captcha match from the user input and from the one save in a session
		if(hash('sha256', $_POST['resetverify']) !== $_SESSION['code']) {$codererr = '<span style="color:red;">The verification code does not match!</span>';}

	}

	return array($mailuser, $mailusererr, $npasserr, $copasserr, $codererr);
}

PART 2 - Handle login form and password reset request (login.php)

As the title says it, this part will handle the login and password reset request forms, both of which will execute different code. The former will try to login and activate the admin part of the code and the latter will create a reset token and send it via mail to the email address that a request was made for. Note that password reset request form is activated by extra url parameters in the login.php HTTP request as folows...login.php?reset=password...


<?php
session_start();
include_once('db_connect.php');
include_once('con_functions.php');

// Unpack variables from resets function
list($salt, $uid, $usermailerr, $codeserr, $tokenerr) = resets($mysqli);
$reserrors = '';
$reserrors .= $usermailerr.$codeserr.$tokenerr;

// Signing in basic
if(isset($_POST['signin'])) {

	$user = mysqli_real_escape_string($mysqli, $_POST['username']);
	$pass = $_POST['password'];
	$rounds = 50000;
	$passhash = hash_pbkdf2('sha512', $pass, $user, $rounds, 64);

	// Query to find the row where username matches $user and password matches $passhash
	// Get the row in the array so we can get id of the row when needed.
	$query = mysqli_query($mysqli, "SELECT * FROM sql_users WHERE username = '$user' AND password = '$passhash'");
	$rowuser = mysqli_fetch_array($query);
	$finduser = mysqli_num_rows($query);

	// If there are no matches save an error message to a session to be displayed below the login form. redirect afterwards
	if($finduser === 0) {

		$_SESSION['message'] = '<span style="color:red;">Your login combination is incorrect! Please try again...</span>';
		exit(header("location:login.php"));
	}

	// If there is a match and if the status is active, create sessions from row id and $user variable
	// Encode the $user variable because it will be echoed later
	elseif($finduser === 1) {

		if($rowuser['status'] === 'active') {

			session_regenerate_id();
			$_SESSION['userid'] = $rowuser['id'];
			$_SESSION['name'] = htmlentities($user); 
		}
		
		// If status is not active, save the error message into a session to be displayed below login form
		else {$_SESSION['message'] = '<span style="color:red;">Your account has not been activated. Please check your email for activation link.</span>';}
	}
}

// Display admin, if session userid was created(see above)
elseif(isset($_SESSION['userid'])) {

	echo '<p> Login successful for id '.$_SESSION['userid'].'</p>';
	echo '<span style="color:blue;">Welcome '.$_SESSION['name'].'</span>!<br/><br/>';
	echo '<form action="logout.php"><input type="submit" value="logout"/></form>';
}

// Reseting password, when request button is clicked on
// First check for empty errors and if there are none, execute below
elseif(isset($_POST['resets'])) {
	if(empty($reserrors)) {

		// Create a token code, basically a random HEX like output to be included in the link
		$pool = '0123456789abcdef';
		$len = 64;
		$token = '';

		// Pick a random char from $pool variable and append it to empty $token variable above. It repeats 64 times.
		for($i = 0; $i < $len; $i++) {$token .= $pool[random_int(0, strlen($pool) -1)];}

		// Hash the token before adding it to the DB table, $salt is email got from resets function
		// Insert the $tokenhash, $uid(got from resets function) and timestamp into the password_reset table
		$rounds = 20000;
		$tokenhash = hash_pbkdf2('sha512', $token, $salt, $rounds, 64);
		mysqli_query($mysqli, "INSERT INTO password_reset (token, uid, timestamp) VALUES ('$tokenhash', '$uid', CURRENT_TIMESTAMP)");

		>// Assemble the mail and send it to the same email that a reset password request was made for
		$to = $salt;
		$link = "http://yourwebsite.com/reset.php?token=$token";
		$subject = "Password reset token";
		$content = 'Note that this link will only be active for 12 hours. In case you are unable to reset the password in this time frame, you will need to request a password reset again.'."\n\n".'Click the link below to reset your password:'."\n".$link;
		$from = "From: Yourwebsite\r\n";

		if(mail($to, $subject, $content, $from)) {$_SESSION['message'] = '<span style="color:green;">You have successfuly requested a password reset. Check your email for reset link.</span>';}
	}
}

// Reset request, triggered by extra url parameters when Forgot Password link is clicked on
// Errors are from the resets function
elseif($_GET['reset'] === 'password') {

	echo '<form name="reset" action="" method="post">';
	echo '<div id="logins"><strong>*</strong> Username or Email<br/><input name="usermail" type="text" size="17"></div>';
	if(!empty($usermailerr)) {echo $usermailerr."<br/>";} 

	echo '<p><strong>*</strong> Please enter the verification code below</p>';
	echo '<img style="vertical-align:middle;" src="/captcha.php">nbsp;nbsp;<input type="text" name="resverify" maxlength="6" size="6"><br/><br/>';
	if(!empty($codeserr)) {echo $codeserr."<br/>";}

	echo '<input type="submit" name="resets" value="Reset"/>';
	echo '</form>';
	echo $tokenerr;
}

// Else display Login form
else {

	echo '<form name="login" action="" method="post">';
	echo '<div>Username<br/><input style="-webkit- box-sizing:border-box;width:190px;" name="username" type="text" maxlength="70"></div>';
	echo '<div>Password<br/><input style="-webkit- box-sizing:border-box;width:190px;" name="password" type="password" maxlength="70"></div>';
	echo '<a style="font-size:11px;margin:5px 0px 0px 100px;display:block;" href="/login.php?reset=password">Forgot Password?</a>';
	echo '<input type="submit" name="signin" value="login"/>';
	echo '</form>';

	// Display session message once, then reset to null
	echo $_SESSION['message']; 
	$_SESSION['message'] = NULL;
}
?>

PART 3 - Process token and update password (reset.php)

The following code will again check whether the username or email exists in DB based on input. If so, it will fetch the id of the row and then get the token from the password_reset table based on id. It will then compare the token hashed value from DB with the plain token value, sent via email as link(the parameter will be read by the $_GET request), that will be hashed using the same procedure as when it was stored in the DB table. If it matches, then the password will again be hashed and updated for the user in question. The token will also be deleted because it is no longer required. After success, it will redirect to login.php file.


<?php
session_start();
include_once('db_connect.php');
include_once('con_functions.php');

// Unpack variables from password reset function(newpass($mysqli))
// Create $errors variable from each individual error
list($mailuser, $mailusererr, $npasserr, $copasserr, $codererr) = newpass($mysqli);
$errors = '';
$errors .= $mailusererr.$npasserr.$copasserr.$codererr;

// When reset button is clicked on, check for empty errors and if empty, execute below...
if(isset($_POST['reset'])) {
	if(empty($errors)) {

		// Again query to find a match, on username or email
		$query = mysqli_query($mysqli, "SELECT * FROM sql_users WHERE username = '$mailuser' OR email = '$mailuser'");
		$fetch = mysqli_fetch_array($query);

		// Fetch id of the row from query above and save it to variable
		// Then query to find a row from password_reset table where uid matches the id from sql_users table and is not older than 12h
		$id = $fetch['id'];
		$tokenquery = mysqli_query($mysqli, "SELECT * FROM password_reset WHERE uid = '$id' AND timestamp > (now()  interval 12 hour)");
		$tokenfetch = mysqli_fetch_array($tokenquery);

		// Fetch hashed token from the row
		// Get non-hashed token from the GET request
		// Fetch email(to be used as salt) from the row in sql_users table
		// Set rounds
		$tokenhash = $tokenfetch['token'];
		$token = $_GET['token'];
		$salt = $fetch['email'];
		$rounds = 20000;	

		// Check whether the $tokenhash is 64 chars long to avoid false comparisons
		// Now check whether the hashed token from DB and hashed token that is generated by the user for the password reset match
		if(strlen($tokenhash) === 64 && $tokenhash === hash_pbkdf2('sha512', $token, $salt, $rounds, 64)) {

			// If they do, generate a new password to be stored in DB for the requested user
			$user = $fetch['username'];
			$newpass = $_POST['npassw'];
			$rnds = 50000;
			$passhash = hash_pbkdf2('sha512', $newpass, $user, $rnds, 64);

			// Update the password for the requested user and then delete the token from the password_reset table
			mysqli_query($mysqli, "UPDATE sql_users SET password = '$passhash' WHERE id = '$id'");
			mysqli_query($mysqli, "DELETE FROM password_reset WHERE uid = '$id'");

			// Save success message into a session and redirect to login.php page where the session will display the message
			$_SESSION['message'] = '<span style="color:green;">Password has been reset successfully! You can now use it to login to your profile.</span>';
			exit(header('Locationlogin.php'));
			
		}

		Else create a failure message to be displayed below the reset form
		else {$failure = '<span style="color:red;">The token is not valid or has expired! You should make a new reset request.</span>';}
	}
}
?>

The functionality is now ready to be used but the reset form still needs to be created.


<form name="reset" action="" method="post">
<div id="logins"><strong>*</strong> Username or Email<br/><input name="mailuser" type="text" size="17"></div>
<?php if(!empty($mailusererr)) {echo $mailusererr."<br/>";} ?>

<label><strong>*</strong> New Password</label><br/>
<input type="password" name="npassw" size="30"/><br/>
<?php if(!empty($npasserr)) {echo $npasserr."<br/>";} ?>

<label><strong>*</strong> Confirm Password</label><br/>
<input type="password" name="copassw" size="30"/><br/>
<?php if(!empty($copasserr)) {echo $copasserr."<br/>";} ?>

<p><strong>*</strong> Please enter the verification code below</p>
<img style="vertical-align:middle;" src="/captcha.php">nbsp;nbsp;<input type="text" name="resetverify" maxlength="6" size="6"><br/>
<?php if(!empty($codererr)) {echo $codererr."<br/>";} ?><br/>
<input type="submit" name="reset" value="Reset"/>
</form><br/>
<?php echo $failure; ?>

Alternatively, the full registration system with registration form, user login and password reset option can be downloaded here. Please read the text carefully before downloading to avoid misunderstandings but, in case, if uncertainty remains, you can leave a comment below.


Comments:

Be the first to comment.

Add a comment:










I have read and agree with the Privacy terms and conditions.