The RESET

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.

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:

  • URL
  • data
  • callback function
  • data type

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:

  • Security questions. When the user registers for the account, the registration process asks for some number of security questions to be answered, which are simple for the user to remember, but difficult for others to guess. These systems can get very complex, as there should be several questions to choose from, and finding the right balance of memorable questions and hard-to-guess answers is difficult. The system must store the question selected and the answer provided by the user. When the password needs to be reset, the system must present the question(s), verify the correct answer, then allow the password to be reset (if the right answers are provided). The weakness of the system is apparent when users choose questions and answers that can be guessed by others, and that is the case with many of the high profile secrity breaches involving celebrities and embarrassing photos, etc. It's not so difficult to find out where someone went to high school, etc., etc.
  • Google or Facebook API sign-ins. The larger social media platforms provide API's that allow applications to query their systems with credentials for authentication. If the password is forgotten, the problem is passed off to Google, etc.
  • E-mail authentication. This method assumes the user has a secure e-mail account, and relies on e-mail authentication to verify the user. A link to reset the password is sent by e-mail, and the e-mail response is considered sufficient authentication. Additional precautions include limited time windows for expiration and the use of "tokens" to assure that the response is coming from a genuine password reset request.

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:

  • id The id column should be set for Auto-Increment.
  • user_id This column should be set to integer data type.
  • reset_token Set this column to VARCHAR 255.
  • timestamp Set this column to timestamp data type, and set the "Default" column to "CURRENT TIMESTAMP", and the "attribute" column to "on update CURRENT TIMESTAMP".

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:

  • Recieve the email address input from the form we just built and sanitize the data.
  • Use the email address to check for a match in the user_hash table.
  • Create a password reset log record which contains:
    • the user id
    • a "token" (a unique string of characters, created by hashing a unique and changing data source, such as the time)
    • a timestamp
  • Insert the record into the password reset log table.
  • Send an e-mail to the user's email address, with a link to a password reset script ("verify_link.php"), using the token created earlier. The message should provide instructions for what the user should do.
  • Notify the user whether or not the password reset request was successful, and to expect an e-mail message with a link.

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 reset link should be a URL that points to the script on your site we will create in the next step. Set it up in the n413_email_config.php file. Update this link when you have the next script (verify_link) in place, and whenever you move the project to a new server.
  • The token is appended to the reset URL as a "GET" URL query.
  • The $to value contains the user's e-mail address, and is set to the original address submitted by the user in case any of the sanitizing steps caused changes in the address.
  • The $from value should be your account name on the server where the script is running. Set this up in the n413_email_config.php file. It will be updated when you move the project to a different server. One of the issues that causes e-mail to be rejected by network mail servers is a disagreement between the "From" address domain name, and the actual originating server domain name, so be sure they match.
  • When composing the e-mail headers, you will need to insert carriage returns into the string. PHP does this by using the \r\n character sequence (return-new line), but you must be careful to enclose those characters in double quotes. PHP will handle the same single-quoted characters in a different way.
  • Edit the text of the message to read correctly for your project. Test on a server and send a message to yourself to be sure everything (especially the reset link) is displayed properly.
  • The mail() function does the magic and sends the mail. Actually sending the message is very simple. All the work is upstream.

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:

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:

  • Sanitize the input
  • Revalidate the token.
  • Encrypt the password
  • Update the password in the users_hash table
  • Return a user message with the status of the password reset.

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

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.