Lit RESET

Lit RESET will add a password reset feature to the Lit SITE project. Users will be able to request a password reset and receive a reset link in their e-mail.

You can open a completed version of the project in your browser here.

If you want to download completed versions of the PHP scripts for this project, you can find them here.

Copy the project files from Lit SITE and put them in a new project folder. I recommend you name the project folder "n413_litreset"

Familiarize yourself with the the project structure. The password reset feature will focus on the Auth.php controller, the Auth_model.php model, and the login.php view (found at views/auth/login.php).

Here are the main steps for building the Lit RESET project:

This requires 3 new methods in the Auth controller, and 3 new methods in the Auth_model model. 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. Set up a directory in the htdocs folder to hold your project. The recommended name is n413_litreset.

Copy all the files from the Lit SITE project into your project folder. Or you can use the completed set of Lit SITE files from here. Either set should contain a complete set of the CodeIgniter files. Note: The CodeIgniter file structure includes a directory named "application". When we refer to files with path names, they are generally in relation to the "application" folder.

Open config/config.php and update the "base_url" value with the new project path. It should be:

$config['base_url'] = 'http://localhost:8888/n413_litreset/';
//or for XAMPP:
//$config['base_url'] = 'http://localhost/n413_litreset/';
$config['from'] = '<your-account>@<your-server-domain>';

The "base_url" item is a path to the root of the project, and will be changed whenever you move the project.

The "from" item is for the e-mail feature of the project, and will hold your return address for the email message. It must be an account on the server where the project is running. Localhost development setups can't generally send email, so this is only important when you move the project to a webserver. If you know the server's domain name (such as "in-info-web4.informatics.iupui.edu"), go ahead and put your account name and the domain name in the "from" variable.

If you downloaded a new set of files, be sure the config/database.php files have been updated with the correct paths and database connection credentials, port numbers, etc. (See Lit LIST.)

We will use the same database tables ("list", "form_responses", "users_hash", and "litlist") used in The LIST, The FORM, The PASSWORD, and Lit LIST project. If you don't already have those tables set up, see the intructions here. Or you can download the SQL script to create the tables here.

"Forgot" link. Add a "Forgot Password" link below the login form in views/auth/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.

(views/auth/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.

(views/auth/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 method in the Auth.php controller, which is not yet in place. It will be called "send_reset_link". This method will be developed in a later step. The callback function will display any success or error messages returned from the server, targeting the placeholder divs in the form.

(views/auth/login.php)
...
<script type="text/javascript"> 
    // Attach a submit handler to the form
    $( "#reset_form" ).submit(function( event ) {
        event.preventDefault();
        $.post("<?=site_url()?>/auth/send_reset_link",
            {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 views/auth/login.php. You will not be able to test the form submission until the remaining steps scripts are in place.

(views/auth/login.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("<?=site_url()?>/auth/send_reset_link",
            {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. There is one additional database table you will need for this project. 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 This feature will accept the user's email address input, check to see if it is in the database, then send a reset link (and a security token) to the user's email. This functionality is spread across the model-view-controller design pattern in the following way:

  • View: The user enters the e-mail address in a form, the form data is sent to the controller using AJAX.
  • Controller: The controller method gets the e-mail data from the $_POST array (using the CodeIgniter Input class), and passes it to the model.
  • Model: The model checks the database for the e-mail address, and, if successful, creates a "token" and sends an e-mail message to the user with a reset link and the token. The model sends any user messages back to the controller.
  • Controller: The controller method receives the user messages from the model and sends them on to the borwser.
  • View: The callback function in the view uses the logic in the user message to display the user messages.

First, open controllers/Auth.php and add a method (function) named "send_reset_link()". Be sure you add the method inside the Auth class. This method will need to do the following things:

  • Recieve the email address input from the form we just built.
  • Forward the email address to a method of the same name in the model(models/Auth_model.php).
  • pass the returned messages from the model back to the view.

The controller method looks like this:

(views/auth/login.php)
...
    public function send_reset_link(){
        $email = $this->input->post('email');
        $messages = $this->auth_model->send_reset_link($email);
        echo json_encode($messages);
    }  

One thing to note: The CodeIgniter Input class will do much of the data sanitizing automatically, so it is not necessary to do so many sanitizing steps.

Next, open models/Auth_model.php and create a new method inside the Auth_model class. This method will be more complex. It will do the following things:

  • 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"), 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.

Begin by setting up the $messages variable, and verifying the email address:

(models/Auth_model/send_reset_link)
...
public function send_reset_link($email){

    $messages = array();
    $messages["status"] = 0;
    $messages["errors"] = 0;
    $messages["email_error"] = "";
    $messages["user_message"] = ""; 

    $user_id = 0;
    $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 = $this->db->escape_str($email);
    }  // -end else-  if (!filter_var($email, FILTER_VALIDATE_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.

Note that this section of code is very similar to the classic PHP version (The RESET), but is somewhat simpler, without the need to check whether variables exist and some decoding steps. One important difference is with the "escape" step. The classic PHP version directly calls the mysqli library and references the $link variable for the escape step. With CodeIgniter, the database processes are abstracted and the db class is called instead. The db class has two methods for escaping strings: escape() and escape_str(). The escape() method places single quotes around the output, and the escape_str() method does not. When it is time to compose the SQL query, it is important to know whether the string is wrapped in single quotes or not.

Now check the users_hash table to see if the email is found in any of the records. If it is, create the token and insert a record into the password_reset_log.

(models/Auth_model/send_reset_link)            
...
    if(! $messages["errors"] ){
        $sql = "SELECT id FROM users_hash 
                WHERE email = '".$email."' ";
        $query = $this->db->query($sql);	

        if($query->num_rows() == 1){
            $row = $query->row_array();
            $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())";
            $query = $this->db->query($sql);

CodeIgniter abstracts the query method as well, using the query class. The num_rows() method and the row_array() method are part of the query class. The row_array() method outputs the first row of the query result as an associative array.

The "token" is generated by concatenating the user's email with the output of the PHP time() function (a UNIX time number representing the number of seconds since Jan. 1, 1970). This produces a constantly changing unique string, which will be "hashed" by the sha1() encryption function. The result is a unique and complex string that can be used to verify the password reset link in the coming steps.

The NOW() function in the INSERT query is a MySQL function that produces a current timestamp.

The next step sends the e-mail message to the user.

(models/Auth_model/send_reset_link)    
...
            if($this->db->affected_rows() == 1){
                //define the headers
                $to = $email;
                $from = config_item('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.
				
	'.site_url().'/auth/verify_link/'.$token;  //this should be a path to the Auth controller's verify_link method.  The token is the 3rd URL segment
				
                //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($this->db->affected_rows() == 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 "from" part of the headers section is the sender's e-mail address, which has been stored in the config file, since it will be updated whenever the project is moved to a new server. The config_item() method retrieves it. The sender's e-mail address must be an account on the project web server, as the domain name of the sender e-mail must match the sending server's domain. Otherwise, network mail servers will reject the message as spam.

The special password reset link uses the URL for the project (site_url()), and the controller/method path for the "verify_link" method. The token is added as an additional URL segment, which will be passed to the verify_link method as an argument.

The headers section requires carriage returns to be inserted, and PHP does this with special /r/n characters (return-new line). They must be formatted in double quotes, or PHP renders them as literal characters.

Sending the e-mail is very simple. Just use the mail() function, using the arguments you have built upstream.

The last task is to send back the user messages:

(models/Auth_model/send_reset_link)    
...
    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.
The link is valid for 1 hour."; }else{ $messages["status"] = 'failed'; } return $messages; } // -end- send_reset_link

The complete script for the models/Auth_model/send_reset_link is here:

(models/Auth_model/send_reset_link)
    public function send_reset_link($email){

        $messages = array();
        $messages["status"] = 0;
        $messages["errors"] = 0;
        $messages["email_error"] = "";
        $messages["user_message"] = ""; 

        $user_id = 0;
        $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 = $this->db->escape_str($email);
        }  // -end else-  if (!filter_var($email, FILTER_VALIDATE_EMAIL)) 
                   
        if(! $messages["errors"] ){
            $sql = "SELECT id FROM users_hash 
                    WHERE email = '".$email."' ";
            $query = $this->db->query($sql);	

            if($query->num_rows() == 1){
                $row = $query->row_array();
                $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())";
                $query = $this->db->query($sql);    
                if($this->db->affected_rows() == 1){
                    //define the headers
                    $to = $email;
                    $from = config_item('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.
				
	'.site_url().'/auth/verify_link/'.$token;  //this should be a path to the Auth controller's verify_link method.  The token is the 3rd URL segment
				
                    //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($this->db->affected_rows() == 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.
The link is valid for 1 hour."; }else{ $messages["status"] = 'failed'; } return $messages; } // -end- send_reset_link

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: