The RESET. The RESET project builds on the previous projects, The SITE, The ACCOUNT, and The PASSWORD. We will build a feature that allows users to reset their password.
You can download scripts for completed versions of the PASSWORD project here if you need them.
You can open a completed version of this project in your browser here.
If you want to download completed versions of the PHP scripts and image files for this project, you can find them here.
This project has several steps:
This project will use a few new techniques: a Bootstrap modal, create a "token" using encryption, and will send e-mail from a server using PHP. Let's get started!
Project Set-up. As you did in the earlier projects, set up a directory in the htdocs folder to hold your project. I recommend naming it n413_reset. We will use the PASSWORD project as the basis for building this one, so copy all the files from the PASSWORD project into your new project folder. You can find them here if you need them. Be sure to get the full set of files in the zip archive, which include the files from the SITE and ACCOUNT projects.
Be sure your MAMP/XAMPP server is running and check your connection script (n413_connect.php) to be sure it uses the correct database connection credentials if you copied in a new version. You should use the users_hash table (or your version) with the encrypted passwords. Check to be sure you have copies of the Bootstrap and jQuery Javascript files, and the Bootstrap CSS file.
"Forgot" link. Add a "Forgot Password" link below the login form in login.php. The link should be an <a> tag, with a data attribute of data-toggle="modal" and an "href" attribute of href="#forgotModal". This will open a Bootstrap modal, which we will add below the login form.
(login.php) ... <form id="login_form" method="POST" action=""> <div class="row mt-5"> <div class="col-4"></div> <!-- spacer --> <div id="form-container" class="col-4"> User Name: <input type="text" id="username" name="username" class="form-control" value="" placeholder="Enter User Name" required/><br/> Password: <input type="password" id="password" name="password" class="form-control" value="" placeholder="Enter Password" required/><br/> <button type="submit" id="submit" class="btn btn-primary float-right">Submit</button> <a data-toggle="modal" href="#forgotModal">Forgot Password?</a> </div> <!-- /#form-container --> </div> <!-- /.row --> </form> ...
Check your project in a browser to see the "Forgot Password" link below the log-in form.
Bootstrap Modal. A Bootstrap modal is a combination of HTML markup, CSS, and the Bootstrap Javascript code, so be sure you have all the Bootstrap files in place (including jQuery) if you have any trouble.
The Bootstrap modal has a complex structure that must be properly set up for the modal to work. The first time you build the structure it seems very complicated, but once you have the first one, you simply modify it to build all the modals that follow. Here is the structure:
There is a closing </body> tag between the markup for the login form and the </script> tag. Move the </body> tag below the end of the script, as we will add the modal code after the login form's closing </script> tag, but before the closing </body> tag. Here is the structure of the Bootstrap modal, without the <form> tag content that will go in the modal body:
(login.php) ... <!-- -------------------------- AMP JAM RESET Reset Password Modal ------------------------- --> <div class="modal fade" id="forgotModal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h4 class="modal-title">AMP JAM RESET Reset Password</h4> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <!-- /.modal-header --> <div class="modal-body"> <!-- form markup goes here --> </div> <!-- /.modal-body --> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div> <!-- /.modal-footer --> </div> <!-- /.modal-content --> </div> <!-- /.modal-dialog --> </div> <!-- /.modal --> <!-- -------------------------- end Reset Password Modal ------------------------------ -->
If you test the code in a browser, you should see this when you click the "Forgot" link.
E-mail Form Create a form for entering the user's email address. Place it in the modal-body section of the modal markup. Use Bootstrap form classes for styling the form (form-horizontal, form-label, form-control), and be sure to provide placeholder divs for error messages. We will use jQuery and AJAX to submit the form, so leave the form's method and action attributes empty.
(login.php) ... <form id="reset_form" name="reset_form" class="form-horizontal" method="" action="" > <div class="row"> <div class="col-12"> <div class="row" style="padding:2em;"> <div class="form-group"> <label for="email" class="control-label">Enter your E-mail:</label> <input type="text" id="email" name="email" class="form-control" placeholder="E-mail address"> <div id="email_error" style="display:none;color:#990000;"></div> </div> <!-- /.form-group --> </div> <!-- /.row --> <div class="row row-gap"> <div class="col-11"> <button type="submit" class="btn btn-primary float-right">Reset Password</button> <div id="user_message" style="display:none;color:#990000;"></div> </div> <!-- /.col-11 --> </div> <!-- /.row row-gap --> </div> <!-- /col-12 --> </div> <!-- /.row --> </form>
Insert this code into the modal-body section of the modal and test it in your browser. It should look like the screen shot to the right.
Last, add another <script> tag after the modal code to handle the form submission and AJAX. Begin with the submit function and the necessary preventDefault() method to avoid reloading a new page when the submit button is clicked. Then begin the jQuery $.post() function to handle the AJAX call. Remember the $.post() function's four arguments:
The URL will point to a PHP file not yet written: "send_reset_link.php". This file will be developed in the next step. The callback function will display any success or error messages returned from the server, targeting the placeholder divs in the form.
(login.php) ... <script type="text/javascript"> // Attach a submit handler to the form $( "#reset_form" ).submit(function( event ) { event.preventDefault(); $.post("send_reset_link.php", {email:$("#email").val()}, function(data){ //reset the error messages $("#user_message").html(""); $("#user_message").css("display","none"); $("#email_error").html(""); $("#email_error").css("display","none"); if(data.status == "success"){ if(data.user_message != null){ $("#user_message").html(data.user_message); $("#user_message").css("display","block"); } }else{ if(data.email_error != null){ $("#email_error").html(data.email_error); $("#email_error").css("display","block"); } } }, "json" ); //post });//submit </script>
Below is the complete script for login.php. You will not be able to test this until the remaining PHP scripts are in place.
(login.php) <?php include("head.php"); ?> <div class="container-fluid"> <div class="row"> <div class="col-12 text-center mt-5"> <h2>Full Stack Amp Jam Log-in</h2> </div> <!-- /col-12 --> </div> <!-- /row --> <form id="login_form" method="POST" action=""> <div class="row mt-5"> <div class="col-4"></div> <!-- spacer --> <div id="form-container" class="col-4"> User Name: <input type="text" id="username" name="username" class="form-control" value="" placeholder="Enter User Name" required/><br/> Password: <input type="password" id="password" name="password" class="form-control" value="" placeholder="Enter Password" required/><br/> <button type="submit" id="submit" class="btn btn-primary float-right">Submit</button> <a data-toggle="modal" href="#forgotModal">Forgot Password?</a> </div> <!-- /#form-container --> </div> <!-- /.row --> </form> <script> var this_page = "login"; var page_title = 'AMP JAM Site | Login'; $(document).ready(function(){ document.title = page_title; navbar_update(this_page); $("#login_form").submit(function(event){ event.preventDefault(); $.post("n413auth.php", $("#login_form").serialize(), function(data){ //handle messages here if(data.status){ $("#form-container").html(data.success); right_navbar_update(data.role); }else{ $("#form-container").html(data.failed); } }, "json" ); //post }); //submit }); //document.ready function right_navbar_update(role){ var html = ""; if (role > 0) { html = '<li id="messages_item" class="nav-item">'+ '<a id="messages_link" class="nav-link" href="messages.php">Messages</a>'+ '</li>'; } html += '<li id="logout_item" class="nav-item">'+ '<a id="logout_link" class="nav-link" href="logout.php">Log-Out</a>'+ '</li>'; $("#right_navbar").html(html); } </script> <!-- -------------------------- AMP JAM RESET Reset Password Modal ------------------------- --> <div class="modal fade" id="forgotModal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h4 class="modal-title">AMP JAM RESET Reset Password</h4> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <!-- /.modal-header --> <div class="modal-body"> <form id="reset_form" name="reset_form" class="form-horizontal" method="" action="" > <div class="row"> <div class="col-12"> <div class="row" style="padding:2em;"> <div class="form-group"> <label for="email" class="control-label">Enter your E-mail:</label> <input type="text" id="email" name="email" class="form-control" placeholder="E-mail address"> <div id="email_error" style="display:none;color:#990000;"></div> </div> <!-- /.form-group --> </div> <!-- /.row --> <div class="row row-gap"> <div class="col-11"> <button type="submit" class="btn btn-primary float-right">Reset Password</button> <div id="user_message" style="display:none;color:#990000;"></div> </div> <!-- /.col-11 --> </div> <!-- /.row row-gap --> </div> <!-- /col-12 --> </div> <!-- /.row --> </form> </div> <!-- /.modal-body --> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div> <!-- /.modal-footer --> </div> <!-- /.modal-content --> </div> <!-- /.modal-dialog --> </div> <!-- /.modal --> <!-- -------------------------- end Reset Password Modal ------------------------------ --> <script type="text/javascript"> // Attach a submit handler to the form $( "#reset_form" ).submit(function( event ) { event.preventDefault(); $.post("send_reset_link.php", {email:$("#email").val()}, function(data){ //reset the error messages $("#user_message").html(""); $("#user_message").css("display","none"); $("#email_error").html(""); $("#email_error").css("display","none"); if(data.status == "success"){ if(data.user_message != null){ $("#user_message").html(data.user_message); $("#user_message").css("display","block"); } }else{ if(data.email_error != null){ $("#email_error").html(data.email_error); $("#email_error").css("display","block"); } } }, "json" ); //post });//submit </script> </body> </html>
Security. Resetting a user's password creates an opportunity for a security breach. There must be some way to verify the identity of the user before allowing the password to be reset. There are three main methods of doing this:
We will use the e-mail method because it is relatively simple and effective. However, it does require that your PHP scripts are running on a server capable of sending e-mail, and that the e-mail can actually reach the user. E-mail spam is such a large problem that network e-mail servers often filter out mail that is not sent from a recognized source. In some cases, e-mail sending servers must have a "reputation score" high enough to make it through the filter. For that reason, some people who need to have a reliable method of sending e-mail use e-mail relay servers that maintain the reputation scores. Within the IUPUI system, we should be able to send e-mail from the web-4 server and receive it on IU accounts, using the IU mail relay. Whether the message will make it through to other systems can be somewhat questionable. You generally cannot expect e-mail to be sent or received from your localhost server, such as MAMP or XAMPP.
Password Reset Log. To keep records of password reset requests, create a password reset log using a database table. The table should have four columns:
Here is what the table structure should look like:
Here is what the data table looks like:
send_reset_link.php First, create a new file in the project folder called "send_reset_link.php". This file will need to do the following things:
n413_email_config.php The send_reset_link.php script sends an e-mail message to the user. This will not work from your localhost MAMP/XAMPP setup. It will work from web-4 or a web server account. That means you will move the project to one of those environments to get it to work. There are a couple of URLs in the script that will need to be updated when that happens. Let's write a "config" script that contains the URLs so you can easily find and update them instead of needing to search through your code when it's time to move the project.
(n413_email_config.php) <?php $reset_link = 'https://<your-server-domain-name>/<path-to-your-project>/n413_reset/verify_link.php'; //This is the return email address. Use your account name here. $from = '<your-account>@<your-server-domain-name>'; ?>
Begin by including the connection script, including the config script, and sanitizing the user input:
(send_reset_link.php) <?php include("n413connect.php"); include("n413_email_config.php"); $messages = array(); $messages["status"] = 0; $messages["errors"] = 0; $messages["email_error"] = ""; $messages["user_message"] = ""; $user_id = 0; $email = ""; if(isset($_POST["email"])){ $email = html_entity_decode($_POST["email"]); $email = trim($email); //check for an empty username if (strlen($email) < 1){ $messages["errors"] = true; $messages["email_error"] = 'You must enter your email address.'; } // if (strlen($email < 1) //check for valid email if (!filter_var($email, FILTER_VALIDATE_EMAIL)){ $messages["errors"] = true; $messages["email_error"] = 'You must enter a valid email address.'; }else{ // if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $email = stripslashes($email); $email = strip_tags($email); $email = mysqli_real_escape_string( $link, $email ); } // -end else- if (!filter_var($email, FILTER_VALIDATE_EMAIL)) } // if(isset($_POST["email"]))
The filter_var() function that checks for a valid e-mail is one of the PHP sanitizing methods. You can learn more about PHP filters here: https://www.w3schools.com/php/php_form_url_email.asp.
Next, check to see if the e-mail exists in the users_hash table, and if so, create the reset log record and store it in the password_reset_log table:
(send_reset_link.php) ... if(! $messages["errors"] ){ $sql = "SELECT id FROM users_hash WHERE email = '".$email."' "; $result = mysqli_query($link, $sql); if(mysqli_num_rows($result) == 1){ $row = mysqli_fetch_array($result, MYSQLI_BOTH); $user_id = $row["id"]; $token = sha1($email.time()); $sql = "INSERT INTO `password_reset_log` (`id`, `user_id`, `reset_token`, `timestamp`) VALUES (NULL, '".$user_id."', '".$token."', NOW())"; $result = mysqli_query($link, $sql);
To create the token, "hash" (or encrypt) a random string. In this case, we are concatenating the e-mail address and the result of the PHP time() function. The time() function returns the number of seconds since January 1, 1970. Use a hashing algorithm such as the sha1() method to hash the result. This will provide a randomized string that we can use to verify whether the link that comes back from the user is the same link that was sent out.
Notice the mysql function NOW() which is placed in the query for the timestamp. This will provide a current timestamp for the record. When the password reset email link is used, the original reset request time will be compared with the time the email link is used. This will determine whether the link has expired.
The next step is to check the success of the password_reset_log insert operation, then compose an e-mail message and send it.
(send_reset_link.php) ... if(mysqli_affected_rows($link) == 1){ //define the headers $to = $_POST["email"]; //$from is defined in the "config" file. $subject = 'Password Reset Request'; $message_text = ' A password reset request has been made for your AMP JAM account that uses this e-mail address. If you did not initiate this request, please notify the security team at once. If you made the request, please click on the link below to reset your password. This link will expire one hour from the time this e-mail was sent. '.$reset_link.'?token='.$token; //$reset_link is defined in the "config" file. //be sure the /r/n (carriage return) characters are in DOUBLE QUOTES! //PHP treats single quoted escaped characters differently, and things will break $headers = 'From: '.$from . "\r\n" . 'Reply-To: '.$from . "\r\n" . 'X-Mailer: PHP/' . phpversion(); mail($to, $subject, $message_text, $headers); }else{ $messages["errors"] = true; $messages["email_error"] = "There was a problem with the database. Your password cannot be reset."; } }else{ // else - if(mysqli_num_rows($result) == 1) $messages["email_error"] = "The e-mail address you entered was not found in the database.
Check to be sure the e-mail address is correct and try again."; } // end else - if(mysqli_num_rows($result) == 1) } // if (! $messages["errors"] )
The final step is to send user feedback to the browser:
(send_reset_link.php) ... if ((! $messages["errors"])&&($user_id > 0)){ $messages["status"] = 'success'; $messages["user_message"] = "A link to reset your password has been mailed to your e-mail address.<br/>The link is valid for 1 hour."; echo json_encode($messages); }else{ $messages["status"] = 'failed'; echo json_encode($messages); } ?>
The user messages are encoded as a JSON string and echoed back to the browser. The script in the Bootstrap modal is expecting a string formatted this way and will display the message appropriately.
The complete script for send_reset_link.php is shown below:
(send_reset_link.php) <?php include("n413connect.php"); include("n413_email_config.php"); $messages = array(); $messages["status"] = 0; $messages["errors"] = 0; $messages["email_error"] = ""; $messages["user_message"] = ""; $user_id = 0; $email = ""; if(isset($_POST["email"])){ $email = html_entity_decode($_POST["email"]); $email = trim($email); //check for an empty username if (strlen($email) < 1){ $messages["errors"] = true; $messages["email_error"] = 'You must enter your email address.'; } // if (strlen($email < 1) //check for valid email if (!filter_var($email, FILTER_VALIDATE_EMAIL)){ $messages["errors"] = true; $messages["email_error"] = 'You must enter a valid email address.'; }else{ // if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $email = stripslashes($email); $email = strip_tags($email); $email = mysqli_real_escape_string( $link, $email ); } // -end else- if (!filter_var($email, FILTER_VALIDATE_EMAIL)) } // if(isset($_POST["email"])) if(! $messages["errors"] ){ $sql = "SELECT id FROM users_hash WHERE email = '".$email."' "; $result = mysqli_query($link, $sql); if(mysqli_num_rows($result) == 1){ $row = mysqli_fetch_array($result, MYSQLI_BOTH); $user_id = $row["id"]; $token = sha1($email.time()); $sql = "INSERT INTO `password_reset_log` (`id`, `user_id`, `reset_token`, `timestamp`) VALUES (NULL, '".$user_id."', '".$token."', NOW())"; $result = mysqli_query($link, $sql); if(mysqli_affected_rows($link) == 1){ //define the headers $to = $_POST["email"]; //$from is defined in the "config" file. $subject = 'Password Reset Request'; $message_text = ' A password reset request has been made for your AMP JAM account that uses this e-mail address. If you did not initiate this request, please notify the security team at once. If you made the request, please click on the link below to reset your password. This link will expire one hour from the time this e-mail was sent. '.$reset_link.'?token='.$token; //$reset_link is defined in the "config" file. //be sure the /r/n (carriage return) characters are in DOUBLE QUOTES! //PHP treats single quoted escaped characters differently, and things will break $headers = 'From: '.$from . "\r\n" . 'Reply-To: '.$from . "\r\n" . 'X-Mailer: PHP/' . phpversion(); mail($to, $subject, $message_text, $headers); }else{ $messages["errors"] = true; $messages["email_error"] = "There was a problem with the database. Your password cannot be reset."; } }else{ // else - if(mysqli_num_rows($result) == 1) $messages["email_error"] = "The e-mail address you entered was not found in the database.
Check to be sure the e-mail address is correct and try again."; } // end else - if(mysqli_num_rows($result) == 1) } // if (! $messages["errors"] ) if ((! $messages["errors"])&&($user_id > 0)){ $messages["status"] = 'success'; $messages["user_message"] = "A link to reset your password has been mailed to your e-mail address.<br/>The link is valid for 1 hour."; echo json_encode($messages); }else{ $messages["status"] = 'failed'; echo json_encode($messages); } ?>
Move the project to a server account and test it. Register an account with your e-mail address and go through the process to reset the password. You should be able to send yourself a reset message like the one shown here:
verify_link.php This step creates the server-side script that handles the user's click on the e-mail link. This script will:
Create a new file in your project folder named "verify_link.php".
The first step is to retrieve the token from the $_GET array and sanitize it. (The fact that the token was part of the link in the e-mail does not prevent a malicious user from sending you problems). Once the token is processed, query the password_reset_log table for the token.
(verify_link.php) <?php include("n413connect.php"); $user_id = 0; $user_message = ''; $token = ''; if(isset($_GET["token"])){ $token = html_entity_decode($_GET["token"]); $token = trim($token); $token = stripslashes($token); $token = strip_tags($token); $token = mysqli_real_escape_string( $link, $token ); $sql = "SELECT user_id, timestamp FROM password_reset_log WHERE reset_token = '".$token."' "; $result = mysqli_query($link, $sql); ...
Now, get the user_id and timestamp details from the password_reset_log record and check the elapsed time. MySQL has a function for doing this called TIMESTAMPDIFF(), which requires:
It is used as part of a SELECT query, and the keyword "as" allows you to name the resulting "column" with a better name. We will use "time_elapsed".
(verify_link.php) ... if (mysqli_num_rows($result) == 1){ $row = mysqli_fetch_array($result, MYSQLI_BOTH); $user_id = $row["user_id"]; $start_time = $row["timestamp"]; $sql = "SELECT TIMESTAMPDIFF(SECOND, '".$start_time."', NOW()) as time_elapsed"; $result = mysqli_query($link, $sql); $row = mysqli_fetch_array($result, MYSQLI_BOTH); if ($row["time_elapsed"] < 3600){ //1 hour $link_status = "valid"; }else{ //if ($row["time_elapsed"] > 3600) $link_status = "expired"; } // -end else- if ($row["time_elapsed"] > 3600) }else{ // if (mysqli_num_rows() == 1) $link_status = "no_record"; } // -end else- if (mysqli_num_rows() == 1) }else{ // if(isset($_GET["token"])) $link_status = "no_token"; }// -end else- if(isset($_GET["token"])) ...
The logic checks for an elapsed time of less than one hour (3600 seconds). Depending on whether the elaspsed time is within the time window, we will set a status variable to "valid", "expired", or other values that reflect the various states of failure.
Next, compose the user messages, depending on the status of the token. If the token is valid, build a string that draws a new password input form. Use a switch statement for the logic.
(verify_link.php) ... switch ($link_status){ case "valid": $user_message .=' <div id="user_message" class="success"> <p>Please enter a new password to use with your account.<br/>It must have at least 8 characters.</p> <form id="password_form" name="password_form" class="form-horizontal" method="" action="" > <div class="row"> <div class="col-3"></div> <!-- spacer --> <div class="col-6"> <div class="form-group"> <label for="password" class="control-label">Password</label> <input type="password" id="password" name="password" class="form-control" placeholder="Password"> <div id="password_error" style="display:none;color:#990000;"></div> </div> <!-- /.form-group --> <button type="submit" class="btn btn-primary float-right">Submit</button> <div id="user_message" style="display:none;color:#990000;"></div> </div> <!-- /.col-6 --> </div> <!-- /.row --> </form> </div> <!-- /.success -->'; break; case "expired": $user_message .=' <div class="error"> <p>The password reset link has expired.</p> <p>Your password cannot be reset using this link.</p> </div>'; break; case "no_record": case "no_token": default: $user_message .=' <div class="error"> <p>The password reset token is not valid.</p> <p>Your password cannot be reset using this link.</p> </div>'; break; }//switch ...
Add the markup for the browser page and insert the user message in the center of the page:
(verify_link.php) ... include("head.php"); ?> <style> .error { padding:30px;text-align:center;font-size:18px;font-weight:700;background-color:#fff;border:4px solid #900; } .success{ text-align:left;font-size:18px;font-weight:400; } .success > p { text-align:center;margin-bottom:30px; } </style> <div class="container-fluid"> <div class="row"> <div class="col-12 text-center mt-5"> <h2>AMP JAM Password Reset</h2> </div> <!-- /col-12 --> </div> <!-- /row --> <div class="row"> <div class="col-3"></div> <!-- spacer --> <div class="col-6 text-center mt-5"> <?= $user_message ?> </div> <!-- /col-6 --> </div> <!-- /row --> </div> <!-- /container-fluid --> </body> ...
Last, add the Javascript and jQuery to post the form using AJAX. Send the user_id, the new password, and the token to a script named "update_password.php".
(verify_link.php) ... <script type="text/javascript"> // Attach a submit handler to the form $( "#password_form" ).submit(function( event ) { event.preventDefault(); $.post("update_password.php", {id:<?=$user_id?>, password:$("#password").val(), token:'<?=$token?>'}, function(data){ //reset any previous error messages $("#password_error").html(""); $("#password_error").css("display","none"); if(data.status == "success"){ if(data.user_message != null){ $("#user_message").html(data.user_message); $("#user_message").css("display","block"); } }else{ if(data.password_error != null){ $("#password_error").html(data.password_error); $("#password_error").css("display","block"); } } }, "json" ); //post }); //submit </script> </html>
If your project is running on a server, you can test this by sending yourself the e-mail link and clicking on it. If you are developing on a localhost server, you can get the token from your database table and compose the "verify_link" URL in the browser window with the token. The result should look like this:
Here is the complete code for "verify_link.php":
(verify_link.php) <?php include("n413connect.php"); $user_id = 0; $user_message = ""; $token = ""; if(isset($_GET["token"])){ $token = html_entity_decode($_GET["token"]); $token = trim($token); $token = stripslashes($token); $token = strip_tags($token); $token = mysqli_real_escape_string( $link, $token ); $sql = "SELECT user_id, timestamp FROM password_reset_log WHERE reset_token = '".$token."' "; $result = mysqli_query($link, $sql); if (mysqli_num_rows($result) == 1){ $row = mysqli_fetch_array($result, MYSQLI_BOTH); $user_id = $row["user_id"]; $start_time = $row["timestamp"]; $sql = "SELECT TIMESTAMPDIFF(SECOND, '".$start_time."', NOW()) as time_elapsed"; $result = mysqli_query($link, $sql); $row = mysqli_fetch_array($result, MYSQLI_BOTH); if ($row["time_elapsed"] < 3600){ //1 hour $link_status = "valid"; }else{ //if ($row["time_elapsed"] > 3600) $link_status = "expired"; } // -end else- if ($row["time_elapsed"] > 3600) }else{ // if (mysqli_num_rows() == 1) $link_status = "no_record"; } // -end else- if (mysqli_num_rows() == 1) }else{ // if(isset($_GET["token"])) $link_status = "no_token"; }// -end else- if(isset($_GET["token"])) switch ($link_status){ case "valid": $user_message .=' <div id="user_message" class="success"> <p>Please enter a new password to use with your account.<br/>It must have at least 8 characters.</p> <form id="password_form" name="password_form" class="form-horizontal" method="" action="" > <div class="row"> <div class="col-3"></div> <!-- spacer --> <div class="col-6"> <div class="form-group"> <label for="password" class="control-label">Password</label> <input type="password" id="password" name="password" class="form-control" placeholder="Password"> <div id="password_error" style="display:none;color:#990000;"></div> </div> <!-- /.form-group --> <button type="submit" class="btn btn-primary float-right">Submit</button> <div id="user_message" style="display:none;color:#990000;"></div> </div> <!-- /.col-6 --> </div> <!-- /.row --> </form> </div> <!-- /.success -->'; break; case "expired": $user_message .=' <div class="error"> <p>The password reset link has expired.</p> <p>Your password cannot be reset using this link.</p> </div>'; break; case "no_record": case "no_token": default: $user_message .=' <div class="error"> <p>The password reset token is not valid.</p> <p>Your password cannot be reset using this link.</p> </div>'; break; }//switch include("head.php"); ?> <style> .error { padding:30px;text-align:center;font-size:18px;font-weight:700;background-color:#fff;border:4px solid #900; } .success{ text-align:left;font-size:18px;font-weight:400; } .success > p { text-align:center;margin-bottom:30px; } </style> <div class="container-fluid"> <div class="row"> <div class="col-12 text-center mt-5"> <h2>AMP JAM Password Reset</h2> </div> <!-- /col-12 --> </div> <!-- /row --> <div class="row"> <div class="col-3"></div> <!-- spacer --> <div class="col-6 text-center mt-5"> <?= $user_message ?> </div> <!-- /col-6 --> </div> <!-- /row --> </div> <!-- /container-fluid --> </body> <script type="text/javascript"> // Attach a submit handler to the form $( "#password_form" ).submit(function( event ) { event.preventDefault(); $.post("update_password.php", {id:<?=$user_id?>, password:$("#password").val(), token:'<?=$token?>'}, function(data){ //reset any previous error messages $("#password_error").html(""); $("#password_error").css("display","none"); if(data.status == "success"){ if(data.user_message != null){ $("#user_message").html(data.user_message); $("#user_message").css("display","block"); } }else{ if(data.password_error != null){ $("#password_error").html(data.password_error); $("#password_error").css("display","block"); } } }, "json" ); //post }); //submit </script> </html>
update_password.php And now, we are finally ready to do the password update. Create another new file in your project folder named "update_password.php". This script will do the following:
First, set up defaults for the variables, and check to see that there is a good user_id.
(update_password.php) <?php include("n413connect.php"); $messages = array(); $messages["status"] = 0; $messages["errors"] = 0; $messages["password_error"] = ""; $messages["user_message"] = ""; $password = ""; $user_id = 0; $token = ''; $validated = false; if(isset($_POST["id"])){ $user_id = intval($_POST["id"]); }else{ $messages["errors"] = true; $messages["password_error"] = 'Password cannot be reset'; } ...
The next step is to re-validate the token and check the user id. This makes sure that the form input is coming back from the same person we sent it out to.
(update_password.php) ... //revalidate the token and the user id if(isset($_POST["token"])){ $token = $_POST["token"]; } if($token > ''){ $sql="SELECT * from password_reset_log WHERE reset_token = '".$token."'"; $result = mysqli_query($link, $sql); if(mysqli_num_rows($result) == 1){ $row = mysqli_fetch_array($result, MYSQLI_BOTH); if($user_id == $row["user_id"]){ $validated = true; } } } //if($token > '') ...
If the credentials are good, check the new password for length and encrypt it.
(update_password.php) ... if( $validated ){ if(isset($_POST["password"])) { $password = $_POST["password"]; trim($password); //delete leading and trailing spaces if(strlen($password) < 8){ $messages["errors"] = true; $messages["password_error"] = "The password must have at least 8 characters."; }else{ $encrypted_password = password_hash($password, PASSWORD_DEFAULT); if($encrypted_password){ $password = $encrypted_password; }else{ $messages["errors"] = true; $messages["password_error"] = "Password encryption failed. You cannot reset your password at this time."; } //if($encrypted_password) } //if(strlen($password) < 8) } //end - else - if(isset($_POST["password"])) }else{ $messages["errors"] = true; } // if ($validated) ...
If there are no errors, do an UPDATE query to replace the password, then create a session.
(update_password.php) ... if( ! $messages["errors"] ){ $sql = "UPDATE users_hash set `password` = '".$password."' WHERE id = '".$user_id."' "; $result = mysqli_query($link, $sql); if(mysqli_affected_rows($link) == 1){ session_start(); $_SESSION["user_id"] = $user_id; }else{ $messages["errors"] = true; $messages["password_error"] = 'There was a problem with the database.
Your password cannot be reset'; } // if( ! mysqli_affected_rows($link) == 1 } // if ( ! $messages["errors"] ) ...
Finally, check the status of the session, update the user feedback messages, and send them back to the browser. Delete the password_reset_log record as an additional security precaution. The link will no longer be usable.
(update_password.php) ... if ($_SESSION["user_id"] > 0){ $messages["status"] = 'success'; $messages["user_message"] = '<p>Your password has been successfully reset.<br/><br/> <a href="login.php"><button type="button" class="btn btn-primary">Log In</button></a></p>'; echo json_encode($messages); }else{ $messages["status"] = 'failed'; echo json_encode($messages); } // -end else- if ($_SESSION["user_id"] > 0) $sql = "DELETE from password_reset_log WHERE reset_token = '".$token."'"; $result = mysqli_query($link, $sql); session_write_close(); ?>
If the process was sucessful, the user should see this message:
The complete script for update_password.php is here:
(update_password.php) <?php include("n413connect.php"); $messages = array(); $messages["status"] = 0; $messages["errors"] = 0; $messages["password_error"] = ""; $messages["user_message"] = ""; $password = ""; $user_id = 0; $token = ''; $validated = false; if(isset($_POST["id"])){ $user_id = intval($_POST["id"]); }else{ $messages["errors"] = true; $messages["password_error"] = 'Password cannot be reset'; } //revalidate the token and the user id if(isset($_POST["token"])){ $token = $_POST["token"]; } if($token > ''){ $sql="SELECT * from password_reset_log WHERE reset_token = '".$token."'"; $result = mysqli_query($link, $sql); if(mysqli_num_rows($result) == 1){ $row = mysqli_fetch_array($result, MYSQLI_BOTH); if($user_id == $row["user_id"]){ $validated = true; } } } //if($token > '') if( $validated ){ if(isset($_POST["password"])) { $password = $_POST["password"]; trim($password); //delete leading and trailing spaces if(strlen($password) < 8){ $messages["errors"] = true; $messages["password_error"] = "The password must have at least 8 characters."; }else{ $encrypted_password = password_hash($password, PASSWORD_DEFAULT); if($encrypted_password){ $password = $encrypted_password; }else{ $messages["errors"] = true; $messages["password_error"] = "Password encryption failed. You cannot reset your password at this time."; } //if($encrypted_password) } //if(strlen($password) < 8) } //end - else - if(isset($_POST["password"])) }else{ $messages["errors"] = true; } // if ($validated) if( ! $messages["errors"] ){ $sql = "UPDATE users_hash set `password` = '".$password."' WHERE id = '".$user_id."' "; $result = mysqli_query($link, $sql); if(mysqli_affected_rows($link) == 1){ session_start(); $_SESSION["user_id"] = $user_id; }else{ $messages["errors"] = true; $messages["password_error"] = 'There was a problem with the database.
Your password cannot be reset'; } // if( ! mysqli_affected_rows($link) == 1 } // if ( ! $messages["errors"] ) if ($_SESSION["user_id"] > 0){ $messages["status"] = 'success'; $messages["user_message"] = '<p>Your password has been successfully reset.<br/><br/> <a href="login.php"><button type="button" class="btn btn-primary">Log In</button></a></p>'; echo json_encode($messages); }else{ $messages["status"] = 'failed'; echo json_encode($messages); } // -end else- if ($_SESSION["user_id"] > 0) $sql = "DELETE from password_reset_log WHERE reset_token = '".$token."'"; $result = mysqli_query($link, $sql); session_write_close(); ?>
The RESET Project is now complete. You have now added the ability for users to reset a forgotten password. Along the way, you have built a Bootstrap modal UI, sent e-mail from a server, and performed authentication with a token.
These features add resilience and self-management to your site authentication, and will be a valuable part of the websites you develop.
You can open a completed version of the project in your browser here. Register an account with your own working e-mail to try the password reset.
Here are completed versions of the scripts used for this project:
(n413connect.php)<?php $dbhost = 'localhost:8889'; //XAMPP is 'localhost:3306' $dbuser = 'root'; $dbpwd = 'root'; //XAMPP password is '' $dbname = 'ampjam_db'; $link = mysqli_connect($dbhost, $dbuser, $dbpwd, $dbname); if (!$link) { die('Connect Error (' . mysqli_connect_errno() . ') '. mysqli_connect_error()); } ?>
(head.php)<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes"> <title>Full Stack Amp Jam Site Project</title> <link href="css/bootstrap.min.css" rel="stylesheet"> <script src="js/jquery-3.4.1.min.js" type="application/javascript"></script> <script src="js/bootstrap.min.js" type="application/javascript"></script> <script src="js/bootstrap.min.js.map" type="application/javascript"></script> <script> function navbar_update(this_page){ $("#"+this_page+"_item").addClass('active'); $("#"+this_page+"_link").append(' <span class="sr-only">(current)</span>'); } <script> </head> <body> <nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <a class="navbar-brand" href="index.php">AMP JAM Site</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav"> <li id="home_item" class="nav-item"> <a id="home_link" class="nav-link" href="index.php">Home</a> </li> <li id="list_item" class="nav-item"> <a id="list_link" class="nav-link" href="list.php">The List</a> </li> <li id="contact_item" class="nav-item"> <a id="contact_link" class="nav-link" href="form.php">Contact</a> </li> </ul> <ul id="right_navbar" class="navbar-nav ml-auto mr-5"> <?php session_start(); if(isset($_SESSION["user_id"])){ if($_SESSION["role"] > 0){ echo ' <li id="messages_item" class="nav-item"> <a id="messages_link" class="nav-link" href="messages.php">Messages</a> </li>'; } echo ' <li id="logout_item" class="nav-item"> <a id="logout_link" class="nav-link" href="logout.php">Log-Out</a> </li>'; }else{ echo ' <li id="register_item" class="nav-item"> <a id="register_link" class="nav-link" href="register.php">Register</a> </li> <li id="login_item" class="nav-item"> <a id="login_link" class="nav-link" href="login.php">Log-In</a> </li>'; } ?> </ul> </div> </nav>
(register.php)<?php include("head.php"); ?> <style> .error_msg { display:none;color:#C00; } </style> <div class="container-fluid"> <div id="headline" class="row mt-5"> <div class="col-12 text-center"> <h2>Full Stack Amp Jam Registration</h2> </div> <!-- /col-12 --> </div> <!-- /row --> <form id="register_form" method="POST" action=""> <div class="row mt-5"> <div class="col-4"></div> <!-- spacer --> <div id="form-container" class="col-4"> <div>User Name: <input type="text" id="username" name="username" class="form-control" value="" placeholder="Enter User Name" required/></div> <div id="username_length" class="error_msg"></div> <div id="username_exists" class="error_msg"></div> <div class="mt-3">E-mail: <input type="email" id="email" name="email" class="form-control" value="" placeholder="Enter E-mail" required/></div> <div id="email_exists" class="error_msg"></div> <div id="email_validate" class="error_msg"></div> <div class="mt-3">Password: <input type="password" id="password" name="password" class="form-control" value="" placeholder="Enter Password" required/></div> <div id="password_length" class="error_msg"></div> <div class="mt-5"><button type="submit" id="submit" class="btn btn-primary float-right">Submit</button></div> </div> <!-- /#form-container --> </div> <!-- /.row --> </form> </body> <script> var this_page = "register"; var page_title = 'AMP JAM Site | Register'; $(document).ready(function(){ document.title = page_title; navbar_update(this_page); $("#register_form").submit(function(event){ event.preventDefault(); $.post( "n413register.php", $("#register_form").serialize(), function(data){ if(data.status){ $("#form-container").html(data.success); right_navbar_update(); }else{ if(data.errors){ // handle error messages here for (var key in data){ switch(key){ case "status": case "errors": case "success": case "failed": break; default: $("#"+key).html(data[key]); $("#"+key).css("display","block"); break; } //switch } //for-in }else{ $("#form-container").html(data.failed); //registration failed, but without errors } //if data.errors } //if data.status }, //callback function "json" ); //post }); //submit }); //document.ready function right_navbar_update(){ var html = '<li id="logout_item" class="nav-item">'+ '<a id="logout_link" class="nav-link" href="logout.php">Log-Out</a>'+ '</li>'; $("#right_navbar").html(html); } </script> </html>
(users_hash.sql)-- phpMyAdmin SQL Dump -- version 4.8.3 -- https://www.phpmyadmin.net/ -- -- Host: localhost:8889 -- Generation Time: Apr 30, 2020 at 08:20 PM -- Server version: 5.7.23 -- PHP Version: 7.2.10 SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; SET time_zone = "+00:00"; /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8mb4 */; -- -- Database: `ampjam_db` -- -- -------------------------------------------------------- -- -- Table structure for table `users_hash` -- CREATE TABLE `users_hash` ( `id` int(11) NOT NULL, `username` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, `password` text NOT NULL, `role` int(11) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -- Dumping data for table `users_hash` -- INSERT INTO `users_hash` (`id`, `username`, `email`, `password`, `role`) VALUES (1, 'groot', 'groot@guardians.com', '$2y$10$CEc6KF7zaYXIPdQrsiILqu8ro9j2UpqPdKtZ3wz3xEQ5LpD626QCG', 1), (2, 'rocket', 'rocket@guardians.com', '$2y$10$xngf4UYeK7Pbupz5IE21hexBeuLLjXHcyZ3Kj90revx5.6rL8ffeW', 0); -- -- Indexes for dumped tables -- -- -- Indexes for table `users_hash` -- ALTER TABLE `users_hash` ADD PRIMARY KEY (`id`); -- -- AUTO_INCREMENT for dumped tables -- -- -- AUTO_INCREMENT for table `users_hash` -- ALTER TABLE `users_hash` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
(n413register.php)<?php include("n413connect.php"); function sanitize($item){ global $link; $item = html_entity_decode($item); $item = trim($item); $item = stripslashes($item); $item = strip_tags($item); $item = mysqli_real_escape_string( $link, $item ); return $item; } $messages = array(); $messages["status"] = 0; $messages["errors"] = 0; $messages["username_length"] = ""; $messages["username_exists"] = ""; $messages["email_exists"] = ""; $messages["email_validate"] = ""; $messages["password_length"] = ""; $messages["success"] = ""; $messages["failed"] = ""; $username = ""; $email = ""; $password = ""; if(isset($_POST["username"])) { $username = $_POST["username"]; } $username = trim($username); if( strlen($username) < 5 ){ $messages["error"] = 1; $messages["username_length"] = "The Username must be at least 5 characters long."; }else{ $username = sanitize($username); } if(isset($_POST["password"])) { $password = $_POST["password"]; } $password = trim($password); if( strlen($password) < 8 ){ $messages["error"] = 1; $messages["password_length"] = "The Password must be at least 8 characters long."; }else{ $encrypted_password = password_hash($password, PASSWORD_DEFAULT); if($encrypted_password){ $password = $encrypted_password; }else{ $messages["errors"] = 1; $messages["password_length"] = "Password encryption failed. You cannot register at this time"; } } if(isset($_POST["email"])) { $email = $_POST["email"]; } if (filter_var($email, FILTER_VALIDATE_EMAIL)){ $email = sanitize($email); }else{ $messages["errors"] = 1; $messages["email_validate"] = "There are problems with the e-mail address. Please correct them."; } if( ! $messages["errors"]){ $sql = "SELECT * FROM `users_hash` WHERE username = '".$username."'"; $result = mysqli_query($link, $sql); if( mysqli_num_rows($result) > 0){ $messages["errors"] = 1; $messages["username_exists"] = "This Username is already in use. Please provide a different Username"; } $sql = "SELECT * FROM `users_hash` WHERE email = '".$email."'"; $result = mysqli_query($link, $sql); if( mysqli_num_rows($result) > 0){ $messages["errors"] = 1; $messages["email_exists"] = "This E-mail address is already in use. You cannot register another account for this E-mail."; } } //if( ! $messages["errors"]) if( ! $messages["errors"]){ $sql = "INSERT INTO `users_hash` (`id`, `username`, `email`, `password`, `role`) VALUES (NULL, '".$username."', '".$email."', '".$password."', '0')"; $result = mysqli_query($link, $sql); $user_id = mysqli_insert_id($link); if($user_id){ session_start(); $_SESSION["user_id"] = $user_id; $_SESSION["role"] = "0"; } } //if( ! $messages["errors"]) if(isset($_SESSION["user_id"])){ $messages["status"] = "1"; $messages["success"] = '<h3>You are now Registered and Logged In.</h3>'; }else{ $messages["failed"] = '<h3>The Registration was not successful.</h3> <div class="col-12 text-center"><a href="register.php"><button type="button" class="btn btn-primary mt-5">Try Again</button></a></div>'; } echo json_encode($messages); ?>
(n413auth.php)<?php include("n413connect.php"); function sanitize($item){ global $link; $item = html_entity_decode($item); $item = trim($item); $item = stripslashes($item); $item = strip_tags($item); $item = mysqli_real_escape_string( $link, $item ); return $item; } $messages = array(); $messages["status"] = 0; $messages["role"] = 0; $messages["success"] = ""; $messages["failed"] = ""; $username = ""; $password = ""; if(isset($_POST["username"])) { $username = sanitize($_POST["username"]); } if(isset($_POST["password"])) { $password = $_POST["password"]; } $sql= "SELECT * FROM `users_hash` WHERE username = '".$username."' LIMIT 1"; $result = mysqli_query($link, $sql); $row = mysqli_fetch_array($result, MYSQLI_BOTH); if(password_verify($password, $row["password"])){ session_start(); $_SESSION["user_id"] = $row["id"]; $_SESSION["role"] = $row["role"]; } if(isset($_SESSION["user_id"])){ $messages["status"] = "1"; $messages["role"] = $_SESSION["role"]; $messages["success"] = '<h3 class="text-center">You are now Logged In.</h3>'; }else{ $messages["failed"] = '<h3 class="text-center">The Log-in was not successful.</h3> <div class="col-12 text-center"><a href="login.php"><button type="button" class="btn btn-primary mt-5">Try Again</button></a></div>'; } echo json_encode($messages); ?>
(login.php)<?php include("head.php"); ?> <div class="container-fluid"> <div class="row"> <div class="col-12 text-center mt-5"> <h2>Full Stack Amp Jam Log-in</h2> </div> <!-- /col-12 --> </div> <!-- /row --> <form id="login_form" method="POST" action=""> <div class="row mt-5"> <div class="col-4"></div> <!-- spacer --> <div id="form-container" class="col-4"> User Name: <input type="text" id="username" name="username" class="form-control" value="" placeholder="Enter User Name" required/><br/> Password: <input type="password" id="password" name="password" class="form-control" value="" placeholder="Enter Password" required/><br/> <button type="submit" id="submit" class="btn btn-primary float-right">Submit</button> <a data-toggle="modal" href="#forgotModal">Forgot Password?</a> </div> <!-- /#form-container --> </div> <!-- /.row --> </form> <script> var this_page = "login"; var page_title = 'AMP JAM Site | Login'; $(document).ready(function(){ document.title = page_title; navbar_update(this_page); $("#login_form").submit(function(event){ event.preventDefault(); $.post("n413auth.php", $("#login_form").serialize(), function(data){ //handle messages here if(data.status){ $("#form-container").html(data.success); right_navbar_update(data.role); }else{ $("#form-container").html(data.failed); } }, "json" ); //post }); //submit }); //document.ready function right_navbar_update(role){ var html = ""; if (role > 0) { html = '<li id="messages_item" class="nav-item">'+ '<a id="messages_link" class="nav-link" href="messages.php">Messages</a>'+ '</li>'; } html += '<li id="logout_item" class="nav-item">'+ '<a id="logout_link" class="nav-link" href="logout.php">Log-Out</a>'+ '</li>'; $("#right_navbar").html(html); } </script> <!-- -------------------------- AMP JAM RESET Reset Password Modal ------------------------- --> <div class="modal fade" id="forgotModal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h4 class="modal-title">AMP JAM RESET Reset Password</h4> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <!-- /.modal-header --> <div class="modal-body"> <form id="reset_form" name="reset_form" class="form-horizontal" method="" action="" > <div class="row"> <div class="col-12"> <div class="row" style="padding:2em;"> <div class="form-group"> <label for="email" class="control-label">Enter your E-mail:</label> <input type="text" id="email" name="email" class="form-control" placeholder="E-mail address"> <div id="email_error" style="display:none;color:#990000;"></div> </div> <!-- /.form-group --> </div> <!-- /.row --> <div class="row row-gap"> <div class="col-11"> <button type="submit" class="btn btn-primary float-right">Reset Password</button> <div id="user_message" style="display:none;color:#990000;"></div> </div> <!-- /.col-11 --> </div> <!-- /.row row-gap --> </div> <!-- /col-12 --> </div> <!-- /.row --> </form> </div> <!-- /.modal-body --> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div> <!-- /.modal-footer --> </div> <!-- /.modal-content --> </div> <!-- /.modal-dialog --> </div> <!-- /.modal --> <!-- -------------------------- end Reset Password Modal ------------------------------ --> <script type="text/javascript"> // Attach a submit handler to the form $( "#reset_form" ).submit(function( event ) { event.preventDefault(); $.post("send_reset_link.php", {email:$("#email").val()}, function(data){ //reset the error messages $("#user_message").html(""); $("#user_message").css("display","none"); $("#email_error").html(""); $("#email_error").css("display","none"); if(data.status == "success"){ if(data.user_message != null){ $("#user_message").html(data.user_message); $("#user_message").css("display","block"); } }else{ if(data.email_error != null){ $("#email_error").html(data.email_error); $("#email_error").css("display","block"); } } }, "json" ); //post });//submit </script> </body> </html>
(n413_email_config.php)<?php $reset_link = 'https://<your-server-domain-name>/<path-to-your-project>/n413_reset/verify_link.php'; //This is the return email address. Use your account name here. $from = '<your-account>@<your-server-domain-name>'; ?>
(password_reset_log.sql)-- phpMyAdmin SQL Dump -- version 4.9.3 -- https://www.phpmyadmin.net/ -- -- Host: localhost:8889 -- Generation Time: Oct 20, 2020 at 03:56 PM -- Server version: 5.7.26 -- PHP Version: 7.4.2 SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; SET time_zone = "+00:00"; -- -- Database: `ampjam_db` -- -- -------------------------------------------------------- -- -- Table structure for table `password_reset_log` -- CREATE TABLE `password_reset_log` ( `id` int(11) NOT NULL, `user_id` int(11) NOT NULL, `reset_token` varchar(255) NOT NULL, `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=MyISAM DEFAULT CHARSET=latin1; -- -- Indexes for dumped tables -- -- -- Indexes for table `password_reset_log` -- ALTER TABLE `password_reset_log` ADD PRIMARY KEY (`id`); -- -- AUTO_INCREMENT for dumped tables -- -- -- AUTO_INCREMENT for table `password_reset_log` -- ALTER TABLE `password_reset_log` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
(send_reset_link.php)<?php include("n413connect.php"); include("n413_email_config.php"); $messages = array(); $messages["status"] = 0; $messages["errors"] = 0; $messages["email_error"] = ""; $messages["user_message"] = ""; $user_id = 0; $email = ""; if(isset($_POST["email"])){ $email = html_entity_decode($_POST["email"]); $email = trim($email); //check for an empty username if (strlen($email) < 1){ $messages["errors"] = true; $messages["email_error"] = 'You must enter your email address.'; } // if (strlen($email < 1) //check for valid email if (!filter_var($email, FILTER_VALIDATE_EMAIL)){ $messages["errors"] = true; $messages["email_error"] = 'You must enter a valid email address.'; }else{ // if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $email = stripslashes($email); $email = strip_tags($email); $email = mysqli_real_escape_string( $link, $email ); } // -end else- if (!filter_var($email, FILTER_VALIDATE_EMAIL)) } // if(isset($_POST["email"])) if(! $messages["errors"] ){ $sql = "SELECT id FROM users_hash WHERE email = '".$email."' "; $result = mysqli_query($link, $sql); if(mysqli_num_rows($result) == 1){ $row = mysqli_fetch_array($result, MYSQLI_BOTH); $user_id = $row["id"]; $token = sha1($email.time()); $sql = "INSERT INTO `password_reset_log` (`id`, `user_id`, `reset_token`, `timestamp`) VALUES (NULL, '".$user_id."', '".$token."', NOW())"; $result = mysqli_query($link, $sql); if(mysqli_affected_rows($link) == 1){ //define the headers $to = $_POST["email"]; //$from is defined in the "config" file. $subject = 'Password Reset Request'; $message_text = ' A password reset request has been made for your AMP JAM account that uses this e-mail address. If you did not initiate this request, please notify the security team at once. If you made the request, please click on the link below to reset your password. This link will expire one hour from the time this e-mail was sent. '.$reset_link.'?token='.$token; //$reset_link is defined in the "config" file. //be sure the /r/n (carriage return) characters are in DOUBLE QUOTES! //PHP treats single quoted escaped characters differently, and things will break $headers = 'From: '.$from . "\r\n" . 'Reply-To: '.$from . "\r\n" . 'X-Mailer: PHP/' . phpversion(); mail($to, $subject, $message_text, $headers); }else{ $messages["errors"] = true; $messages["email_error"] = "There was a problem with the database. Your password cannot be reset."; } }else{ // else - if(mysqli_num_rows($result) == 1) $messages["email_error"] = "The e-mail address you entered was not found in the database.
Check to be sure the e-mail address is correct and try again."; } // end else - if(mysqli_num_rows($result) == 1) } // if (! $messages["errors"] ) if ((! $messages["errors"])&&($user_id > 0)){ $messages["status"] = 'success'; $messages["user_message"] = "A link to reset your password has been mailed to your e-mail address.<br/>The link is valid for 1 hour."; echo json_encode($messages); }else{ $messages["status"] = 'failed'; echo json_encode($messages); } ?>
(verify_link.php)<?php include("n413connect.php"); $user_id = 0; $user_message = ""; $token = ""; if(isset($_GET["token"])){ $token = html_entity_decode($_GET["token"]); $token = trim($token); $token = stripslashes($token); $token = strip_tags($token); $token = mysqli_real_escape_string( $link, $token ); $sql = "SELECT user_id, timestamp FROM password_reset_log WHERE reset_token = '".$token."' "; $result = mysqli_query($link, $sql); if (mysqli_num_rows($result) == 1){ $row = mysqli_fetch_array($result, MYSQLI_BOTH); $user_id = $row["user_id"]; $start_time = $row["timestamp"]; $sql = "SELECT TIMESTAMPDIFF(SECOND, '".$start_time."', NOW()) as time_elapsed"; $result = mysqli_query($link, $sql); $row = mysqli_fetch_array($result, MYSQLI_BOTH); if ($row["time_elapsed"] < 3600){ //1 hour $link_status = "valid"; }else{ //if ($row["time_elapsed"] > 3600) $link_status = "expired"; } // -end else- if ($row["time_elapsed"] > 3600) }else{ // if (mysqli_num_rows() == 1) $link_status = "no_record"; } // -end else- if (mysqli_num_rows() == 1) }else{ // if(isset($_GET["token"])) $link_status = "no_token"; }// -end else- if(isset($_GET["token"])) switch ($link_status){ case "valid": $user_message .=' <div id="user_message" class="success"> <p>Please enter a new password to use with your account.<br/>It must have at least 8 characters.</p> <form id="password_form" name="password_form" class="form-horizontal" method="" action="" > <div class="row"> <div class="col-3"></div> <!-- spacer --> <div class="col-6"> <div class="form-group"> <label for="password" class="control-label">Password</label> <input type="password" id="password" name="password" class="form-control" placeholder="Password"> <div id="password_error" style="display:none;color:#990000;"></div> </div> <!-- /.form-group --> <button type="submit" class="btn btn-primary float-right">Submit</button> <div id="user_message" style="display:none;color:#990000;"></div> </div> <!-- /.col-6 --> </div> <!-- /.row --> </form> </div> <!-- /.success -->'; break; case "expired": $user_message .=' <div class="error"> <p>The password reset link has expired.</p> <p>Your password cannot be reset using this link.</p> </div>'; break; case "no_record": case "no_token": default: $user_message .=' <div class="error"> <p>The password reset token is not valid.</p> <p>Your password cannot be reset using this link.</p> </div>'; break; }//switch include("head.php"); ?> <style> .error { padding:30px;text-align:center;font-size:18px;font-weight:700;background-color:#fff;border:4px solid #900; } .success{ text-align:left;font-size:18px;font-weight:400; } .success > p { text-align:center;margin-bottom:30px; } </style> <div class="container-fluid"> <div class="row"> <div class="col-12 text-center mt-5"> <h2>AMP JAM Password Reset</h2> </div> <!-- /col-12 --> </div> <!-- /row --> <div class="row"> <div class="col-3"></div> <!-- spacer --> <div class="col-6 text-center mt-5"> <?= $user_message ?> </div> <!-- /col-6 --> </div> <!-- /row --> </div> <!-- /container-fluid --> </body> <script type="text/javascript"> // Attach a submit handler to the form $( "#password_form" ).submit(function( event ) { event.preventDefault(); $.post("update_password.php", {id:<?=$user_id?>, password:$("#password").val(), token:'<?=$token?>'}, function(data){ //reset any previous error messages $("#password_error").html(""); $("#password_error").css("display","none"); if(data.status == "success"){ if(data.user_message != null){ $("#user_message").html(data.user_message); $("#user_message").css("display","block"); } }else{ if(data.password_error != null){ $("#password_error").html(data.password_error); $("#password_error").css("display","block"); } } }, "json" ); //post }); //submit </script> </html>
(update_password.php)<?php include("n413connect.php"); $messages = array(); $messages["status"] = 0; $messages["errors"] = 0; $messages["password_error"] = ""; $messages["user_message"] = ""; $password = ""; $user_id = 0; $token = ''; $validated = false; if(isset($_POST["id"])){ $user_id = intval($_POST["id"]); }else{ $messages["errors"] = true; $messages["password_error"] = 'Password cannot be reset'; } //revalidate the token and the user id if(isset($_POST["token"])){ $token = $_POST["token"]; } if($token > ''){ $sql="SELECT * from password_reset_log WHERE reset_token = '".$token."'"; $result = mysqli_query($link, $sql); if(mysqli_num_rows($result) == 1){ $row = mysqli_fetch_array($result, MYSQLI_BOTH); if($user_id == $row["user_id"]){ $validated = true; } } } //if($token > '') if( $validated ){ if(isset($_POST["password"])) { $password = $_POST["password"]; trim($password); //delete leading and trailing spaces if(strlen($password) < 8){ $messages["errors"] = true; $messages["password_error"] = "The password must have at least 8 characters."; }else{ $encrypted_password = password_hash($password, PASSWORD_DEFAULT); if($encrypted_password){ $password = $encrypted_password; }else{ $messages["errors"] = true; $messages["password_error"] = "Password encryption failed. You cannot reset your password at this time."; } //if($encrypted_password) } //if(strlen($password) < 8) } //end - else - if(isset($_POST["password"])) }else{ $messages["errors"] = true; } // if ($validated) if( ! $messages["errors"] ){ $sql = "UPDATE users_hash set `password` = '".$password."' WHERE id = '".$user_id."' "; $result = mysqli_query($link, $sql); if(mysqli_affected_rows($link) == 1){ session_start(); $_SESSION["user_id"] = $user_id; }else{ $messages["errors"] = true; $messages["password_error"] = 'There was a problem with the database.
Your password cannot be reset'; } // if( ! mysqli_affected_rows($link) == 1 } // if ( ! $messages["errors"] ) if ($_SESSION["user_id"] > 0){ $messages["status"] = 'success'; $messages["user_message"] = '<p>Your password has been successfully reset.<br/><br/> <a href="login.php"><button type="button" class="btn btn-primary">Log In</button></a></p>'; echo json_encode($messages); }else{ $messages["status"] = 'failed'; echo json_encode($messages); } // -end else- if ($_SESSION["user_id"] > 0) $sql = "DELETE from password_reset_log WHERE reset_token = '".$token."'"; $result = mysqli_query($link, $sql); session_write_close(); ?>
If you want to download completed versions of the PHP scripts and image files, you can find them here.