Feeling Forgetful?

Let’s process a password reset request for an existing user.

1) One last time, we’ll revisit the Sign in Page form (“/opt/lampp/htdocs/hector/application/views/Sign_In.php”), this time focusing in on the “Forgot password?” link toward the bottom of the page:

<?php
	echo Form::open('User/sign_in_prcs', array('name' => 'frmSignIn', 'id' => 'id_frmSignIn'));
?>
				<fieldset>
<?php								
	echo '<legend>'.$page_description.'</legend>';
	echo Form::label('txtUsernameOrEmailAddr', 'Username or Email Address:');
	echo Form::input('txtUsernameOrEmailAddr', $post ? $post['txtUsernameOrEmailAddr']: '', array('id' => 'id_txtUsernameOrEmailAddr', 'size' => '35', 'maxlength' => '100', 'required' => true));
	if (array_key_exists('txtUsernameOrEmailAddr', $errors))
	{
		echo '<small class="error">'.$errors['txtUsernameOrEmailAddr'].'</small>';
	}
	echo Form::label('pwdPswd', 'Password:');
	echo Form::password('pwdPswd', NULL, array('id' => 'id_pwdPswd', 'size' => '35', 'maxlength' => '200', 'required' => true));
	if (array_key_exists('pwdPswd', $errors))
	{
		echo '<small class="error">'.$errors['pwdPswd'].'</small>';
	}
	echo '<br />';
	echo Form::button('btnSignIn', 'Sign In', array('class' => 'small button radius submit'));
	echo '<br />'.HTML::anchor('User/forgot_pswd', 'Forgot password?');
?>
				</fieldset>
<?php
	echo '<br />'.HTML::anchor('User/create_acct', 'Create an account');
	echo Form::close();
?>

As will surprise no one, at this point in our adventure, clicking this link will route the user to the User controller’s forgot_pswd action.

2) So, let’s pop over to the User controller (“/opt/lampp/htdocs/hector/application/classes/Controller/User.php”) and add the forgot_pswd action now:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_User extends Controller_Template_Sitepage {
.
.
.

	public function action_forgot_pswd()
	{		
		
		/*
		/	Purpose: Action to handle "forgot password" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/
		
		// Define arrays
		$errors = array();
				
		// Render Forgot Password page
		$this->template->content = ViewBuilder::factory('Page')->forgot_pswd($this->template, NULL, $errors = array());
		
	}	

.
.
.
} // End User

Since this follows our standard procedure for (thin) controllers, it merely calls the forgot_pswd() function in the ViewBuilder_Page class.

3) We’ll add this function to the ViewBuilder_Page class (“/opt/lampp/htdocs/hector/application/classes/ViewBuilder/Page.php”) here:

<?php defined('SYSPATH') or die('No direct script access.');

class ViewBuilder_Page {
.
.
.

	public function forgot_pswd($template, $post_or_null, $errors)
	{
			  
		/*
		/	Purpose: Build Forgot Password page
		/
		/	Parms:
		/		'template' >> Array containing page header attributes
		/		'post_or_null' >> Submitted form data or NULL
		/		'errors' >> Form validation errors or NULL
		/
		/	Returns:
		/		Page to render
		*/		

		// Render Forgot Password page
		$template->page_description = 'Reset Password';
		$template->title .= $template->page_description;
		$template->set_focus = 'frmForgotPswd.txtUsernameOrEmailAddr';
		$content = ViewBuilder::factory('AppUserView')->forgot_pswd($post_or_null, $errors);
		
		return $content;
		
	}	

.
.
.
} // End ViewBuilder_Page

Again, as we’ve done repeatedly before, this relies on a new function in the ViewBuilder_AppUserView class to move us a step closer to actually rendering the view.

4) That function is the new forgot_pswd() function, which we’ll now add to the ViewBuilder_AppUserView class (“/opt/lampp/htdocs/hector/application/classes/ViewBuilder/AppUserView.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class ViewBuilder_AppUserView {
.
.
.

	public function forgot_pswd($post_or_null, $errors)
	{
			  
		/*
		/	Purpose: Build Forgot Password view
		/
		/	Parms:
		/		'post_or_null' >> Submitted form data or NULL
		/		'errors' >> Form validation errors or NULL
		/
		/	Returns:
		/		View to render
		*/			
		
		$content = 
		
			// Render Forgot Pswd view		
			View::factory('Forgot_Pswd')
				->set('page_description', 'Reset Password')
				->set('post', $post_or_null)
				->set('errors', $errors)
		;
      
		return $content;
		
	}

.
.
.
} // End ViewBuilder_AppUserView

Here we call the view code responsible for actually rendering the Password Reset Request page.

5) Naturally, we need the View code too. Create the “Forgot_Pswd” view (name it “Forgot_Pswd.php” and save it in the “/opt/lampp/htdocs/hector/application/views” directory):

Forgot_Pswd_Views_8_1

Add the following code and markup:

<?php
	echo Form::open('User/forgot_pswd_prcs', array('name' => 'frmForgotPswd', 'id' => 'id_frmForgotPswd', 'class' => 'custom'));
?>
				<fieldset>
<?php								
	echo '<legend>'.$page_description.'</legend>';
	echo '<h5>Please enter your Username or Email Address</h5>';
	echo Form::label('txtUsernameOrEmailAddr', 'Username or Email Address:');
	echo Form::input('txtUsernameOrEmailAddr', $post ? $post['txtUsernameOrEmailAddr']: '', array('id' => 'id_txtUsernameOrEmailAddr', 'size' => '35', 'maxlength' => '100', 'required' => true));
	if (array_key_exists('txtUsernameOrEmailAddr', $errors))
	{
		echo '<small class="error">'.$errors['txtUsernameOrEmailAddr'].'</small>';
	}
	echo '<br />';
	echo Form::button('btnResetPswd', 'Reset Password', array('class' => 'small button radius submit'));
?>
				</fieldset>
<?php
	echo Form::close();
?>

6) Open your browser, navigate to “http://localhost/hector”, and hit the “Forgot password?” link. You should see this:

Hector_Reset_Password_8_2

The Password Reset Request page has no surprises. It simply asks the user for their username or email address. Upon submission of the form, program execution will be passed to the User controller’s forgot_pswd_prcs action.

7) Now it’s time to add the forgot_pswd_prcs action to the User controller (“/opt/lampp/htdocs/hector/application/classes/Controller/User.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_User extends Controller_Template_Sitepage {
.
.
.

	public function action_forgot_pswd_prcs()
	{		
		
		/*
		/	Purpose: Action to handle processing of "forgot password" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/
		
		// Grab $_POST array from submitted form
		$post = $this->request->post();
  		
		if (isset($post['btnResetPswd']))
		{

			// Reset Password button was pressed
			
			$password_reset_arr = Model::factory('AppUser')->reset_user_password($post);
			if ($password_reset_arr['Success'])
			{

				// Successful password reset
		
				// Redirect to forgot pswd done action for cleaner resultant URL
				$this->redirect('User/forgot_pswd_done');					  
			}
			else
			{
			
				// Validation failed, render Forgot Password page with custom error text displayed on appropriate form field(s)
				$this->template->content = ViewBuilder::factory('Page')->forgot_pswd($this->template, $post, $password_reset_arr['Errors']);					  
			}				  
		}
		else	// This "else" is required so that failed validation (above) won't auto redirect!
		{
		
			// Bad URL, redirect to create_acct action
			$this->redirect('User/create_acct');
		}
		
	}

.
.
.
} // End User

If this function is being called and the “Reset Password” button was NOT pressed, this is considered a bad URL and the visitor is routed to the Create Account page. Otherwise, the $_POST array (contained in the $post variable assigned here), is passed to the AppUser model’s reset_user_password() function. If this returns a successful value, process flow is redirected to the forgot_pswd_done action. Otherwise, validation has failed and we’ll render the Password Reset Request page, with the user’s inputted values retained and relevant error messages displayed.

8) Open up the AppUser model (“/opt/lampp/htdocs/hector/application/classes/Model/AppUser.php”) and add the reset_user_password() function in the “Business logic functions” section:

class Model_AppUser extends Model_Database {		  
.
.
.

	// Business logic functions
          
	public function reset_user_password($post)
	{

		/*
		/	Purpose: Reset user's password
		/
		/	Parms:
		/		'post' >> Submitted form data from Forgot Password view
		/
		/	Returns:
		/		Array containing:
		/			'Clean_Post_Data' >> Array containing cleaned form data
		/			'Success' >> Boolean indicating success/failure of password reset request
		/		[AND]
		/			'Errors' >> Array containing form validation errors
		*/
			  
		$return_arr = array(
			'Success' => FALSE		  
		);

		// Validate Forgot Password form entry
		$validation_results_arr = Validation_AppUser::forgot_password($post);
		if ($validation_results_arr['Success'])
		{
				  			
			// Validation clean, reset user's password
			
			$app_user_data = array(
				'username_or_email_addr' => $validation_results_arr['Clean_Post_Data']['txtUsernameOrEmailAddr']		  					  
			);
			$app_user_rowset = $this->read_user_data_flexible($app_user_data);
			if ($app_user_rowset['Rows_Affected'] == 1)
			{
					  
				// This is a valid username  
				
				// Isolate row of data
				$app_user_arr = $app_user_rowset['Rows'][0];
				
				// Generate 32-digit random, alphanumeric authcode         
				$authcode = Text::random('alnum', 32);
				
				// Update DB with new authcode, new authcode_gen_dttm
				$app_user_data['user_id'] = $app_user_arr['user_id'];
				$app_user_data['authcode'] = Bonafide::instance()->hash($authcode);
				
				$authcode_updated_arr = $this->update_user_authcode($app_user_data);
				if ($authcode_updated_arr['Rows_Affected'] == 1)
				{

					// Send email to user with account reset validation URL
					$tran_email_data = array(
						'recipient' => $app_user_arr['email_addr']
						,'sender' => 'pswd_reset.sender'
						,'sender_descr' => 'pswd_reset.sender_descr'
						,'subject' => 'pswd_reset.subject'
						,'body' => 'pswd_reset.body'
						,'placeholder_arr' => array(
							'USERNAME' => $app_user_arr['username']
							,'EMAIL_ADDR' => $app_user_arr['email_addr']
							,'AUTHCODE' => $authcode
						)
					);
					TransactionEmail::send_email($tran_email_data);
					
					// All is well!
					$return_arr['Success'] = TRUE;
				}
				else
				{
						 
					// Failed user update - pretend all is well to discourage hackers
					$return_arr['Success'] = TRUE;
				}
			}
			else
			{
					  
				// User reset attempt for nonexistent user - pretend all is well to discourage hackers
				$return_arr['Success'] = TRUE;
			}
		}
		else
		{
			$return_arr['Errors'] = $validation_results_arr['Errors'];
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_AppUser

Validation of the user-entered form data is performed. If the data is NOT clean, the specific errors generated during validation are returned. Otherwise, the user’s app_user row is read from the database, a random authcode is generated (and hashed) and the user’s row is then updated with both this new authcode and a new authcode generated datetime. If the update is successful, a password reset email (containing the individual URL the user must click to reset their password) is generated and then sent.

9) Let’s define the new validation function, adding the forgot_password() static function to the Validation_AppUser class (“/opt/lampp/htdocs/hector/application/classes/Validation/AppUser.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Validation_AppUser extends Validation {
.
.
.

	public static function forgot_password($post)
	{
			  
		/*
		/	Purpose: Validate Forgot Password form submitted data
		/
		/	Parms:
		/		Array containing:
		/			'txtUsernameOrEmailAddr' >> User's username or email address 
		/
		/	Returns:
		/		Array containing:
		/			'Clean_Post_Data' >> Array containing cleaned form data
		/			'Success' >> Boolean indicating success/failure of validation
		/		[AND]
		/			'Errors' >> Array containing form validation errors
		*/

		$return_arr = array(
			'Success' => FALSE		  
		);
			  
		$clean_post_data = Validation::factory($post)			
			
			// Username or email address must be non-empty
			->rule('txtUsernameOrEmailAddr', 'not_empty')			

			// Username or email address must not be entirely whitespace chars
			->rule('txtUsernameOrEmailAddr', 'Model_AppUser::non_blank')
		;
		
		if ($clean_post_data->check())
		{
			$return_arr['Clean_Post_Data'] = $clean_post_data;
			
			$return_arr['Success'] = TRUE;
		}
		else
		{	
			$return_arr['Errors'] = $clean_post_data->errors('form_errors');
		}
		
		return $return_arr;
		
	}	

.
.
.
} // End Validation_AppUser

This function relies both on a Kohana-provided validation function (to ensure the username/email address field is not blank) and on the existing custom validation function, non_blank(), we added earlier to the AppUser model. If there are problems, the specific error(s) are passed back to the caller.

10) Now, let’s add the update_user_authcode() function, called by the code we added in step #8 (above), to the “SQL functions” section in the AppUser model (“/opt/lampp/htdocs/hector/application/classes/Model/AppUser.php”):

<?php defined('SYSPATH') or die('No direct script access.');

class Model_AppUser extends Model_Database {
.
.
.

	// SQL functions

	public function update_user_authcode($data)
	{
			  
		/*
		/	Purpose: Set specific user's authcode and authcode gen timestamp
		/
		/	Parms:
		/		Array containing:
		/			'authcode' >> User's (hashed) authcode
		/			'user_id' >> User's ID
		/		
		/	Returns:
		/		Array containing:
		/			'Rows_Affected' >> Number of user rows affected by UPDATE
		/		OR
		/			Generates fatal error, sending error notif to admin
		*/
			  
		$return_arr = array();
		
		try
		{
			$return_arr['Rows_Affected'] = 
				DB::update($this->table_name)
					->set(array(
						'authcode' => $data['authcode']
						,'authcode_gen_dttm' => date('Y-m-d H:i:s')
					))
					->where('user_id', '=', $data['user_id'])
					->execute()
			;
		}
		catch (Database_Exception $e)
		{
		
			// Generate system email with appropriate data to track down/recreate error
			$error_data = array(
				'problem_descr' => 'AppUser->update_user_authcode()'
					.'<br />'.Database_Exception::text($e)
			);							
			AdminErrorNotif::fatal($error_data);		
		}
		
		return $return_arr;
		
	}

.
.
.
} // End Model_AppUser

Nothing magical to explain here! 🙂

11) Next, we need to add a new array key, “pswd_reset”, and its array of keys and values, to the “email_text.php” messages file (“/opt/lampp/htdocs/hector/application/messages/email_text.php”):

<?php defined('SYSPATH') or die('No direct script access.');

return array
(
	'acct_validation' => array(
		'sender' => '[system_email_address]'
		,'sender_descr' => 'Hector'
		,'subject' => 'Hector - Please complete your account creation request within 60 minutes.'
		,'body' => "Hi [USERNAME], <br /><br />Thank you for signing up for a Hector account! <br /><br />In order to complete your account activation, please click on the following link within the next 60 minutes (after which this URL will expire).&nbsp;&nbsp;Doing so will prompt you to choose a password for your account: <br /><br />https://[site]/Validate/new_acct/[EMAIL_ADDR]/[AUTHCODE] <br /><br />Thanks, <br />&nbsp;&nbsp;&nbsp;The Organizer"
	), 
	'pswd_reset' => array(
		'sender' => '[system_email_address]'
		,'sender_descr' => 'Hector'
		,'subject' => 'Hector - Please complete your password reset request within 60 minutes.'
		,'body' => "Hi [USERNAME], <br /><br />Sorry to hear you're unable to sign in to Hector! <br /><br />In order to confirm that you control this email address, please click on the following link within the next 60 minutes (after which this URL will expire).&nbsp;&nbsp;Doing so will prompt you to choose a new password for your account: <br /><br />http://[site]/Validate/reset_acct/[EMAIL_ADDR]/[AUTHCODE] <br /><br />If you did NOT request a password reset, please ignore this email and continue to sign in with your current password. <br /><br />Thanks, <br />&nbsp;&nbsp;&nbsp;The Organizer"
	),
	'system_error_report' => array(
		'sender' => '[system_email_address]'
		,'recipient' => '[admin_email_address]'
		,'sender_descr' => 'Hector'
		,'subject' => 'Hector - ALERT - system problem!'
		,'body' => "Hey there, <br /><br />Sorry to be the bearer of bad news, but the following error occurred:<br /><br />User: [USER_ID]<br />At: [DTTM]<br /><br />Controller: [CONTROLLER]<br />Action: [ACTION]<br /><br />[PROBLEM_DESCR]<br /><br />Please look into this!<br /><br />Thanks,<br />&nbsp;&nbsp;&nbsp;The Innards"
	)
);

12) Back in step #7 (above), within the User controller’s forgot_pswd_prcs action, the user, upon successfully completing their password reset request, is redirected to the forgot_pswd_done action. Let’s add that to the User controller (“/opt/lampp/htdocs/hector/application/classes/Controller/User.php”) now:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_User extends Controller_Template_Sitepage {
.
.
.

	public function action_forgot_pswd_done()
	{		
		
		/*
		/	Purpose: Action to handle post-processing of "forgot password" requests
		/
		/	Parms:
		/		[NONE]
		/		
		/	Returns:
		/		[NONE]
		*/
		
		// Render Informational Message page instructing user to check email for URL to click to complete acct reset process
		$this->template->content = ViewBuilder::factory('Page')->informational_message($this->template, 'Account Confirmation Required', Kohana::message('misc_text', 'pswd_reset_requested'));
		
	}
 
.
.
.
} // End User

13) It looks like we also have a new message to add to “misc_text”, so let’s open it (“/opt/lampp/htdocs/hector/application/messages/misc_text.php”) and add the “pswd_reset_requested” key and value as shown:

<?php defined('SYSPATH') or die('No direct script access.');

return array
(
	'acct_creation_requested' => 'Your account creation request has been submitted.  <br /><br />Please check your email for a note from [system_email_address] and be sure to click the enclosed single-use, expiring link within 60 minutes from submitting your account creation request.'
	,'bad_validation_url' => 'Sorry - it appears that the URL you are using is invalid.  <br /><br />Please double-check it and try again.'
	,'pswd_reset_requested' => 'Your password reset request has been submitted.  <br /><br />Please check your email for a note from [system_email_address] and be sure to click the enclosed single-use, expiring link within 60 minutes from submitting your password reset request.'
);            

14) You should now be able to open your browser, navigate to “http://localhost/hector”, hit the “Forgot password?” link, enter your username or email address, press the “Reset Password” button, and see something similar1:

Hector_Account_Confirmation_Required_8_3

Alrighty then, we have a working password reset request process – sweet!

Of course, naysayers will point out that clicking on the URL sent in the password reset email will presently fail, but that will be taken care of in the next post. I promise!

Notes:

  1. Or just return to your browser, if you still have the page up from step #6 (above), populate the text field with your username or email address, and press the “Reset Password” button.
00

Leave a Reply

Your email address will not be published. Required fields are marked *