Skip to content
Share this..

Reset Lost Passwords in CakePHP

2008 October 23
by Eddie

Allowing users to create passwords is critical. But what happens when they want to change their password, or worse yet a user forgets their password. Will they be forever banned, unable to remember that hastily typed string?!

Allowing users to send password reset tickets to their original email is a pretty good way to solve this, and is seen throughout the web.

 


Its a snap in CakePHP as well. Here’s how it works.

  1. User enters their email address.
  2. Cake checks the user table for a match, and sends out a 24 hour ticket.
  3. User receives ticket, follows link, and is then allowed to enter a new password.
  4. Ticket is ‘Punched’ so to speak, and the user can login as usual.

To manage these tasks we’ll rely on the users model and our new tickets model.

 

First off let me be clear. I don’t use Auth component. Just a matter of taste. So in my example I will be interacting directly with my users model, and sessions for any authentication points.

Second I use my own email component as well, but again you can easily change those areas to meet your needs.

 

Users Controller

You should already have this, so we’ll just look at the new actions.

  • Create Ticket
  • Use Ticket
  • Enter New Password

in app/controllers/users_controller.php

	/**
	 * This sweet controller was written by
	 * @author Edward A Webb edwardawebb.com
	 * 
	 */
class UsersController extends AppController {
 
	var $name = 'Users';
	var $uses =array('User','Ticket');
	var $helpers = array('Html', 'Form');
	var $components =array('Email','Ticketmaster');
 
	function resetpassword($email=null){
		//grab a fresh botcheck question from the db
// for this example youll need to static code these, my botcheck article is coming soon though
		// $bc=$this->Botcheck->getFreshBotcheck();
		$this->whatWeAsk="Is water a liquid at room temperature?";
		$this->humanWouldType=array('Yes', 'of course');
		$this->set('botQuestion',$this->whatWeAsk);
		if(empty($this->data)){
			$this->data['User']['email']=$email;
			//show form
		}else{
			//already entered email
			$botcheck = $this->data['User']['check'];
				//set email to passed variable if present
				if(!$email) $email=$this->data['User']['email'];
				// make sure whave email and a check
				if(!$email){
					$this->User->invalidate('email');
				}elseif(!in_array(strtolower($botcheck),$this->humanWouldType)){
				 	$this->User->invalidate('check');
				}else{
					//email entered, check for it
					$account=$this->User->findByEmail($email);
					if($account['User']['isBanned']){
						//banned user, tell em where to go
						$this->Session->setFlash('

This account is locked due to violation of terms

');
						$this->redirect('/');
					}
					if(!isset($account['User']['email'])){
						$this->Session->setFlash('

We Don\’t have such and email on record.

');
						$this->redirect('/');
 
					}
					$hashyToken=md5(date('mdY').rand(4000000,4999999));
					$message = $this->Ticketmaster->createMessage($hashyToken);
					$this->Email->useremail($email,$account['User']['username'],$message);
					$data['Ticket']['hash']=$hashyToken;
					$data['Ticket']['data']=$email;
					$data['Ticket']['expires']=$this->Ticketmaster->getExpirationDate();
 
					if ($this->Ticket->save($data)){
						$this->Session->setFlash('An email has been sent with instructions to reset your password');
						$this->redirect('/');
					}else{
						$this->Session->setFlash('Ticket could not be issued');
						$this->redirect('/');
 
					}
				}
 
		}
	}
 
	function useticket($hash){
		//purge all expired tickets
		//built into check
		$results=$this->Ticketmaster->checkTicket($hash);
 
		if($results){
			//now pull up mine IF still present
			$passTicket=$this->User->findByEmail($results['Ticket']['data']);
 
			$this->Ticketmaster->voidTicket($hash);
			$this->Session->write('tokenreset',$passTicket['User']['id']);
			$this->Session->setFlash('Enter your new password below');
			$this->redirect('/users/newpassword/'.$passTicket['User']['id']);
		}else{
			$this->Session->setFlash('Your ticket is lost or expired.');
			$this->redirect('/');
		}
 
	}
 
	function newpassword($id = null) {
 
		if($this->Session->check('tokenreset')){
			//user is not logged in, BUT has TOKEN in hand
		}else{
			// But you only want authenticated users to access this action.
//lines like the one below 'checkSession are  authentication code, so you can ignore these or use Auth
			$this->checkSession(1,'/users/edit/'.$id);
 
			//But youll need to read the user info somehow, and only the user who owns the profile 
			$attempter=$this->Session->read('User');
 
			//make sure its the admin or the rigth user
			if($attempter['User']['id']!=$id && $attempter['Role']['rights']<4)
			{
				//not  the user, not the admin and not a reset request via toekns
				/*
				 * SHAME
				 */
				$this->Userban->banuser('Edit Anothers Password');
				$this->Session->setFlash('Your account has been banned');
				$this->redirect('/');
			}
 
		}	
 
		if (empty($this->data)) {
			if($this->Session->check('tokenreset')) $id=$this->Session->read('tokenreset');
			if (!$id) {
				$this->Session->setFlash('Invalid id for User');
				$this->redirect('/users/index');
			}
			$this->data = $this->User->read(null, $id);
		} else {				
 
			$this->data['User']['password']=md5($this->data['User']['password']);
			if ($this->User->save($this->data,true,array('password'))) {
				//delkete session token and dlete used ticket from table
				$this->Session->delete('tokenreset');
				$this->Session->setFlash('The User\'s Password has been updated');
				$this->redirect('/');
			} else {
				$this->Session->setFlash('Please correct errors below.');
			}
		}
	}
}

 

The rest is new

Tickets Model

in app/models/ticket.php

<!--?php 
class Ticket extends AppModel {
 
	var $name = 'Ticket';
	var $recursive = -1;
 
}
?-->

 

MySQL query to build the ticket table

CREATE TABLE IF NOT EXISTS `prefix_tickets` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `hash` VARCHAR(255) DEFAULT NULL,
  `data` VARCHAR(255) DEFAULT NULL,
  `created` datetime DEFAULT NULL,
  `expires` datetime DEFAULT NULL,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `hash` (`hash`)
) ;

 

Ticketmaster Component

This manages tickets, creation, validation and destruction.

in app/controllers/components/ticketmaster.php

<!--?php 
class TicketmasterComponent extends object{
	var $sitename='Site to use in Email message';
	var $linkdomain='example.com';
	//how many hours to honor token
	var $hours=24;
/*
 *  Startup - Link the component to the controller.
 */ 
    function startup(&#038;$controller)
    {
		$this--->controller =&amp; $controller;    	
    }	
	function getExpirationDate(){
		$date=strftime('%c');
		$date=strtotime($date);
		$date+=($this-&gt;hours*60*60);
		$expired=date('Y-m-d H:i:s',$date);
		return $expired;
 
	}
 
	function createMessage($token){
 
		$ms='';
		$ms='Your email has been used in a password reset request at '.$this-&gt;sitename.'&lt;br?&gt;';
		$ms.='If you did not initiate this request, then ignore this message.
';
		$ms.='  Copy the link below into your browser to reset your password.
';
		$ms.='<a href="http://'.$this-&gt;linkdomain.'/users/useticket/'.$token.'">Reset Password</a>';
		$ms.='';
 
		$ms=wordwrap($ms,70);
 
		return $ms;
 
	}
 
	function purgeTickets(){
		$this-&gt;controller-&gt;Ticket-&gt;deleteAll('Ticket.expires &lt;= now() LIMIT 1');
 
	}	
 
	/*
	 * actually for logical reason well be indiscrimnate and clean ALL tockets for this email
	 */
	function voidTicket($hash){
		$this-&gt;controller-&gt;Ticket-&gt;deleteAll(array('hash' =&gt; $hash));
	}
 
	function checkTicket($hash){
		$this-&gt;purgeTickets();
		$ret=false;
		$tick=$this-&gt;controller-&gt;Ticket-&gt;findByHash($hash);
 
		if(empty($tick)){
			//no more ticket			
		}else{
			$ret=$tick;
		}
		return $ret;
	}
}
?&gt;

 

User Views

Its always nice to let users interact with actions, so;

First this form allows users to create a ticket that will get them into the form above without their password.

in app/views/users/resetpassword.ctp

Reset Lost Password

 

Next the useticket action of the controller will allow users to link from the email they received with a unique ‘token’, like a temporary pass.

They will be redirected here, and allowed to enter a new password.

in app/views/users/newpassword.ctp

New Password

Username: data[‘User’][‘username’] ;?> hidden(‘User/username’, array(‘size’ => ’60’));?>

hidden(‘User/id’)?>

 

33 Responses leave one →
  1. November 19, 2008

    Hello there, this looks exactly like what I need!

    One question though, is your “newpassword.ctp” correct? It looks just like the “resetpassword.ctp”…

    Thanks

    Mark

  2. Eddie permalink*
    November 19, 2008

    @Mark
    Sorry about that. I updated the newpassword.ctp code above.

    The same form is used for existing users who are already logged in and want to update their passwords as well.

  3. James permalink
    January 5, 2009

    Thanks for this wonderful tutorial! keep it coming!

  4. Hendrik permalink
    February 3, 2009

    Thank you veryvery much, these structures helped a lot!

  5. Rexford permalink
    February 10, 2009

    didnt work for me. for some reason the resetpassword view does not display.

  6. February 11, 2009

    @Rexford
    Do you mean the link in the user email does not work, or are you seeing an empty page. Make sure the domain and path are set properly in the ticketmaster component.

  7. February 13, 2009

    @Rexford

    I’m getting the same error when the data is submitted from newpassword.ctp. I’ve modified this script to use the built-in E-mail component, and I’ve had it all working on a previous site, but the second implement isn’t working.

    I just get a WSOD when I click the submit button. I can debug “this data” in the beforeFilter(), but white screen after that.

    Any ideas?

  8. Eddie permalink*
    February 13, 2009

    @Joe, Rexford
    I am curious if you guys are using Session to store user data. ( the checksession method called on line 7 of the newpassword method in controller)

    I use a custom method to manage user permissions that stores the information on a user and checks their credentials.

    Also the use of the UserBan behavior. If you guys continue having trouble I can just upload all the components as an archive perhaps

  9. February 20, 2009

    @Joe, Rexford and anyone having issues with the resetpassword view not displaying after hitting submit.

    You have make sure the Users controller is aware of the Ticket model and a Ticket instance exists in each controller action. Make sure to add the following lines:

    App::import(‘Model’,’Ticket’);
    $this->Ticket = new Ticket();

    before the:
    if ($this->Ticket->save($data) )

    call in the resetpassword action. You should repeat this in each method where a Ticket action is issued; specifically in the voidTicket($hash) and checkTicket($hash) methods in the TicketMaster Component.

    App::import(‘Model’,’Ticket’);
    $this->component->Ticket = new Ticket();

    hope that helps.

  10. Eddie permalink*
    February 20, 2009

    @Kofi

    That is not necessary unless you omit the $components directive.

    Furthermore that would add a good deal of overhead to re-instantiate the class every time it is used rather than once while loading the controller.

  11. Joel Pearson permalink
    February 22, 2009

    Looking at your voidTicket function the comment seems weird.

    Because you say you will delete “ALL” tickets for “this email” but you pass the hash, which can only ever delete 1 ticket, because it is a unique column.
    Did you mean for the voidTicket to delete all tickets that have the same data (ie email address)? Because the hash doesn’t have anything directly to do with the email address. If there are multiple tickets with the same data (email) only one of them is going to be deleted.

    /*
    * actually for logical reason well be indiscrimnate and clean ALL tockets for this email
    */
    function voidTicket($hash){
    $this->controller->Ticket->deleteAll(array(‘hash’ => $hash));
    }

    Also why does purgeTickets() have a “LIMIT 1”? So only 1 ticket is purged every time you run that function? If so maybe it should be called purgeTicket (note the s has gone). If it does only delete one at a time, is that just for performance reasons?

    function purgeTickets(){
    $this->controller->Ticket->deleteAll(‘Ticket.expires <= now() LIMIT 1’);

    }

    Otherwise this looks pretty handy.

  12. Eddie permalink*
    February 26, 2009

    @Joel Pearson
    Thanks for your feedback.
    The first comment refers to all tickets created for that action, which is saved as a hashed value. THis is in case a user requests multiple resets, the first use kills all the tickets.
    THe second area was overlooked, and limit should explicitly be set for a higher value, or no limit at all.
    I will update these areas when i have a chance

  13. April 13, 2009

    hello…

    Thats a very interesting tutorial you have there… I am trying to use it for my user registration forms.. Where in I want to validate the user information using a token/ticket.. however I have run into small problems (the below material may be slightly off – topic) .. it will be very nice if someone helps me out here

    What I have done is kind of integrated the newpassword and useticket function… In new password you make sure the user is logged in and then capture the data to reset the password… however… in my case the user is not logged in .. he can only log in if the token is verified… then how do i go abt it… capturing the earlier entered data…

    well I am stuck there… thanks in advance for any help rendered…

  14. Eddie permalink*
    April 13, 2009

    @Nikhil
    That is no problem, the basic ticket method can be used for any action. I use it on another site as a 1-time invitation between users. It all depends on where and how you call the Ticket component’s methods.
    Seems there are a few options. You can add a ‘Activated’ field to your user’s table, and only allow activated users to sign in. Or you can add the password as a hashed value to the the tickets table. this value can then be dropped directly into the user’s table once they use the ticket.

  15. April 14, 2009

    Thanks!!! I was thinking having a temp user database integrated with the token db. But that didn’t fizzle out too well however. The solution of activated/non activated users seems to be great. I will try it out. Thanks again for the enlightenment ;). Will keep you updated on the same.

  16. lyn permalink
    May 3, 2009

    hello, can you help me how to make a reset password using the cakephp 1.1? I am trying to modify the code using the 1.1 but it doesn’t work.

    Thanks

  17. January 11, 2010

    Nice tutorial. I am a newbie and want to learn the basics of cakephp. I am currently working on a Cakephp project. your tutorial help me alot to create the forgot password page.

  18. January 13, 2011

    i am trying to use this ticket method but this is not working, i am just getting a blank page after installing all those here. please give me solution.

  19. Rob M permalink
    January 30, 2011

    Wow. This is excellent. One tiny detail:
    $this->humanWouldType=array(‘Yes’, ‘of course’);
    the ‘Y’ in yes wants to be lowercase, as strtolower() is used to compare with input

  20. Eddie permalink*
    January 31, 2011

    @Rob M

    You’re absolutely correct in_array() is case-sensitive. My actual installation uses a database table full of random question and answers.

    And to be compeltly sure, we’d be best of to convert the array’s content to lower case as we load them from the DB.

    Thanks for looking out!

  21. Eddie permalink*
    January 31, 2011

    @Sukata

    You will need to troubleshoot your issue and provide more detail. I think I have given quite enough for the compensation I am being paid…

  22. April 27, 2011

    Hi there,

    Thanks for the tutorial.
    I am a newbie to cake php.
    I impemented all of the above but I cannot find a way to add a link to the reset password form in my login.ctp template (I am actually taking over form a previous developper)…
    Could you guys help me?

    Cheers,
    Fab

  23. Eddie permalink*
    April 27, 2011

    @Fab

    Thanks for the comment.

    The link to get to the password reset page is a simple static link to http://domain.com/users/resetpassword

    Perhaps your system is using Auth, and relying on the login page to be created magically. Which you can discern if your login function in the User controller looks like this function login() {}

    BUT, you may optionally create a .ctp to render, as long as it provides the needed fields, and posts to the right URL.

    Here is my app/views/users/login.ctp output of my login form:

     
     
    <div id="login_page">
    	<div id="login_box">
    	<h3 class="flash_heading">Please Login</h3>
    	<?php if ($error): ?>
    	<p><?php echo $error_msg; ?></p>
    	<?php endif; ?>
     
     
    	<form action="<?php echo $html->url('/users/login'); ?>" method="post">
    	<div class="logininput">
    	    <?php echo $form->label('User.username','Username:') ?>
    	    <?php echo $form->text('User.username', array('size' => 20)); ?>
    	</div>
    	<div class="logininput">
    	    <label for="password">Password:</label>
    	    <?php echo $form->password('User.password', array('size' => 20)); ?>
     
    		<?php echo '<span class="forgotpass">Did you '.$html->link('Forget your password?', '/users/resetpassword').'</span>'; ?>
     
    	</div>
    	<div class="optional">
    	    <?php echo $form->submit('Login'); ?>
    	</div>
    	</form>
     
    	<div>
    	<?php echo "Don't have an account?<br /> ".$html->link('Register', '/users/register')." now, it's free and easy."; ?>
    	</div>
    	</div>
    </div>
  24. July 21, 2011

    for the validation you could consider using a behavior like the one I wrote today:
    https://github.com/dereuromark/tools/blob/master/models/behaviors/change_password.php

    example usage:
    http://www.cakephp-forum.com/views/klartext-passwort-mit-auth-t1041.html#p4373

    for your botcheck (i call it passive captcha^^) you could use a second behavior

  25. Brandon permalink
    March 29, 2012

    Will you be updating this code for CakePHP 2.x?

  26. Eddie permalink*
    April 23, 2012

    I will be soon as I am just now getting back to using CakePHP for a little side project. Stay tuned! I’m sorry I can’t provide a better timeline….

  27. Maddy permalink
    March 14, 2013

    can u please write same code for cakephp 2.x using CakeEmail in detail ASAP
    Thank you…

  28. Ian permalink
    March 22, 2013

    Hi Eddie… Just checking if you have done the update to CakePHP 2.0 as yet….

  29. Frank permalink
    May 7, 2013

    I am newbie in Cakephp and currently working on it, I found your post is very helpful.

  30. Eddie permalink*
    October 10, 2013

    Hey all, no update for CakePHP 2, cause I just have not been playing in the framework for sometime.

    But I would be happy (thrilled) to post the updates with full credit to anyone who has bothered to make updates.

    Cheers!

Trackbacks and Pingbacks

  1. Mark-a-Spot » Blog Archive » Tools im Einsatz
  2. Reset Lost Passwords in CakePHP | Edward A. Webb (.com) | Source code bank
  3. Recuperar contraseƱa perdida | TimesApp

Leave a Reply

Note: You can use basic XHTML in your comments. Your email address will never be published.

Subscribe to this comment feed via RSS