Reset Lost Passwords in CakePHP
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.
- User enters their email address.
- Cake checks the user table for a match, and sends out a 24 hour ticket.
- User receives ticket, follows link, and is then allowed to enter a new password.
- 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('<h3>This account is locked due to violation of terms</h3>'); $this->redirect('/'); } if(!isset($account['User']['email'])){ $this->Session->setFlash('<h3>We Don\'t have such and email on record.</h3>'); $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(&$controller) { $this->controller =& $controller; } function getExpirationDate(){ $date=strftime('%c'); $date=strtotime($date); $date+=($this->hours*60*60); $expired=date('Y-m-d H:i:s',$date); return $expired; } function createMessage($token){ $ms='<html><head><title>Password Reset Request</title></head>'; $ms='<body>Your email has been used in a password reset request at '.$this->sitename.'<br/>'; $ms.='If you did not initiate this request, then ignore this message.<br/>'; $ms.=' Copy the link below into your browser to reset your password.<br/>'; $ms.='<a href="http://'.$this->linkdomain.'/users/useticket/'.$token.'">Reset Password</a>'; $ms.='</body></html>'; $ms=wordwrap($ms,70); return $ms; } function purgeTickets(){ $this->controller->Ticket->deleteAll('Ticket.expires <= now() LIMIT 1'); } /* * actually for logical reason well be indiscrimnate and clean ALL tockets for this email */ function voidTicket($hash){ $this->controller->Ticket->deleteAll(array('hash' => $hash)); } function checkTicket($hash){ $this->purgeTickets(); $ret=false; $tick=$this->controller->Ticket->findByHash($hash); if(empty($tick)){ //no more ticket }else{ $ret=$tick; } return $ret; } } ?>
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
<div id="view_page"> <h2>Reset Lost Password</h2> <form action="<?php echo $html->url('/users/resetpassword/'); ?>" method="post"> <div class="required"> <?php echo $form->label('User/email', 'E-mail');?> <?php echo $form->text('User/email', array('size' => '40'));?> <?php echo $form->error('User/email', 'Please enter email');?> </div> <div> <p> <?php echo $form->input('User.check', array('size'=>'10','label'=>$botQuestion,'error'=>'Are you human?'));?> </p> <?php echo $form->error('User/check', 'Are you human!');?> </div> <div class="submit"> <?php echo $form->submit('Reset Password');?> </div> </form> </div>
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
<div id="view_page"> <h2>New Password</h2> <form action="<?php echo $html->url('/users/newpassword/'.$form->value('User/id')); ?>" method="post"> <div class="required"> <?php echo $this->data['User']['first_name']." ".$this->data['User']['last_name'];?> <?php echo $form->hidden('User/first_name', array('size' => '60','enabled'=>'false'));?> </div> <div class="required"> <?php echo $form->hidden('User/last_name', array('size' => '60'));?> </div> <div class="required"> <b>Username: </b><?php echo $this->data['User']['username'] ;?> <?php echo $form->hidden('User/username', array('size' => '60'));?> </div> <div class="required"> <?php echo $form->label('User/password', 'Password');?> <?php echo $form->password('User/password', array('size' => '30','value'=>''));?> <?php echo $form->error('User/password', 'Please enter the Password.');?> </div> <div class="required"> <?php echo $form->label('User/confirmpassword', 'Confirm Password');?> <?php echo $form->password('User/confirmpassword', array('size' => '30','value'=>''));?> <?php echo $form->error('User/confirmpassword', 'Please enter the Password Again.');?> <?php echo $form->error('User/checkpassword', 'Please Be Sure Passwords Match.');?> </div> <?php echo $form->hidden('User/id')?> <div class="submit"> <?php echo $form->submit('Save');?> </div> </form> </div> <ul class="actions"> <li><?php echo $html->link('Return Home', '/')?></li> </ul>

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
@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.
Thanks for this wonderful tutorial! keep it coming!
Thank you veryvery much, these structures helped a lot!
didnt work for me. for some reason the resetpassword view does not display.
@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.
@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?
@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
@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.
@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.
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.
@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
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…
@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.
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.
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
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.