CakePHP - Activating User Account via Email

3rd June, 2008 – 10:11 pm

Continuing on from my User Registration with the AuthComponent post I’m going to cover how to activate user account’s via email. Before we get down to the code lets look at a simple use case first.

Activating User Accounts Via Email Use Case
Goal: To confirm that users are registering with a valid email address, force them to activate their account before they can log in.

  1. User registers for an account, all validations passes and $User->save() has been called
  2. At this point we flag that the user’s account is pending activation. An email gets sent to the email address the user registered with. The email contains a unique activation link
  3. The user recieves the activation email and clicks the activation link
  4. The system (your website) handles the incoming link, checks that the activation link is correct (the hash matches) and marks the user’s account as “active” - the user can now log in!
    • Alternative Path: The activation link is rejected by the system (it’s invalid / wasn’t copied correctly) - we present some helpful information to the user.

Time for Some Code!
Okay, let’s start simple - the basic User Table we created in the previous article needs to be expanded to include an “active” flag (boolean) to indicate if the user’s account has been activated yet:

– Table structure for table `users`
CREATE TABLE IF NOT EXISTS `users` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(20) NOT NULL,
  `password` VARCHAR(50) NOT NULL,
  `email` VARCHAR(255) NOT NULL,
  `active` TINYINT(1) NOT NULL DEFAULT ‘0′,
  `created` DATETIME NOT NULL,
  `modified` DATETIME NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=INNODB  DEFAULT CHARSET=latin1;
 

Okay, let’s go ahead and hook this into our Users Controller’s login() action to stop “un-activated” users from loging in (after all, that is the primary goal of performing this work).

<?php
// Note: not all logic is show!
uses(’sanitize’);
class UsersController extends AppController
{
        var $name = ‘Users’;
        var $components = array(‘Auth’);
        function login() {
                // Check for incoming login request.
                if ($this->data) {
                        // Use the AuthComponent’s login action
                        if ($this->Auth->login($this->data)) {
                                // Retrieve user data
                                $results = $this->User->find(array(‘User.username’ => $this->data[‘User’][‘username’]), array(‘User.active’), null, false);
                                // Check to see if the User’s account isn’t active
                                if ($results[‘User’][‘active’] == 0) {
                                        // Uh Oh!
                                        $this->Session->setFlash(‘Your account has not been activated yet!’);
                                        $this->Auth->logout();
                                        $this->redirect(‘/users/login’);
                                }
                                // Cool, user is active, redirect post login
                                else {
                                        $this->redirect(‘/’);
                                }
                        }
                }
        }
}
?>

With this login check in place, we now need to sort out sending out the email which will actually “activate” the user’s account for them. Before we start with the controller actions, let’s defined some custom logic in the Model. As a quick side note, I work to the principle of skinny controllers, fat models (and so should you). What this means, in a nutshell - is that any logic which relates to a Model (in our case, generating the Confirmation Link) should be done in the Model - so let’s do that now.

<?php
# /app/models/user.php
# please note that validation logic is not shown
Class User extends AppModel
{
        var $name = ‘User’;

        /**
         * Creates an activation hash for the current user.
         *
         *      @param Void
         *      @return String activation hash.
        */

        function getActivationHash()
        {
                if (!isset($this->id)) {
                        return false;
                }
                return substr(Security::hash(Configure::read(‘Security.salt’) . $this->field(‘created’) . date(‘Ymd’)), 0, 8);
        }
}
?>

So, incase you didn’t gather, we can grab a unique Activation Hash for any given user by calling $User->getActivationHash() from inside the controller. Let’s just break down what we are doing in the getActivationHash funciton and the reason why we’re doing it.

When we send the email to the user, we are going to send them a link which they can click on to activate their account. If we don’t create unique activation links then users would be able to “guess” or craft activation links for other users, for example, if we didn’t use an activation hash our links may look like this: http://mysite.com/user/activate/jreeves/ - Hmm, well I know that my username is jreeves, so I could guess pretty easily that /users/activate/dchang is going to active someone elses’ account… not great.

So, what is getActivationHash doing? Basically, it’s taking the datetime of when the user created their account (this will be unique for each user), adding in the Day-Month-Year value (so that activation links only last for 24 hours) and combining the whole shebang with the Security.salt value from CakePHP’s core.ini and Hashing it (with either MD5 or SHA-1 depending on your Cake’s settings). In case you are wondering, this process is called salting and it makes any unique value (such as a password, or MD5 hash), almost impossible to guess.

Okay, enough talk, let’s hook this into the register action so that this email gets sent out.

<?php
# /controllers/users_controller.php
# please note that not all code is shown…
uses(’sanitize’);
class UsersController extends AppController {
        var $name = ‘Users’;
        // Include the Email Component so we can send some out :)
        var $components = array(‘Email’, ‘Auth’);
       
        // Allow users to access the following action when not logged in       
        function beforeFilter() {
                $this->Auth->allow(‘register’, ‘thanks’, ‘confirm’, ‘logout’);
                $this->Auth->autoRedirect = false;
        }
       
        // Allows a user to sign up for a new account
        function register() {
                if (!empty($this->data)) {
                        // See my previous post if this is forgien to you
                        $this->data[‘User’][‘password’] = $this->Auth->password($this->data[‘User’][‘passwrd’]);
                        $this->User->data = Sanitize::clean($this->data);
                        // Successfully created account - send activation email                
                        if ($this->User->save()) {
                                $this->__sendActivationEmail($this->User->getLastInsertID());

                                // this view is not show / listed - use your imagination and inform
                                // users that an activation email has been sent out to them.
                                $this->redirect(‘/users/thanks’);
                        }
                        // Failed, clear password field
                        else {
                                $this->data[‘User’][‘passwrd’] = null;
                        }
                }
        }

        /**
         * Send out an activation email to the user.id specified by $user_id
         *  @param Int $user_id User to send activation email to
         *  @return Boolean indicates success
        */

        function __sendActivationEmail($user_id) {
                $user = $this->User->find(array(‘User.id’ => $user_id), array(‘User.email’, ‘User.username’), null, false);
                if ($user === false) {
                        debug(__METHOD__." failed to retrieve User data for user.id: {$user_id}");
                        return false;
                }

                // Set data for the "view" of the Email
                $this->set(‘activate_url’, ‘http://’ . env(‘SERVER_NAME’) . ‘/users/activate/’ . $user[‘User’][‘id’] . ‘/’ . $this->User->getActivationHash());
                $this->set(‘username’, $this->data[‘User’][‘username’]);
               
                $this->Email->to = $user[‘User’][‘email’];
                $this->Email->subject = env(‘SERVER_NAME’) . ‘ - Please confirm your email address’;
                $this->Email->from = ‘noreply@’ . env(‘SERVER_NAME’);
                $this->Email->template = ‘user_confirm’;
                $this->Email->sendAs = ‘text’;   // you probably want to use both :)   
                return $this->Email->send();
        }
}
?>

Okay, now we’re cooking - time to create the Email “views” which will be sent out with the emails - in case you are not familiar with the EmailComponent then now would be a good time to refer to the CookBook. So, let’s create the plain text email template which will contain the activation link set above:

<?php
# /app/views/elements/email/text/user_confirm.ctp
?>
Hey there <?= $username ?>, we will have you up and running in no time, but first we just need you to confirm your user account by clicking the link below:

<?= $activate_url ?>

 

Phew! The end is in sight, just one more controller action to hook up (and probably the most important one) - /users/activate - I’m sure you can figure out what this is going to do.

<?php
# /controllers/user_controller.php
# note that only the activate function is shown…

/**
 * Activates a user account from an incoming link
 *
 *  @param Int $user_id User.id to activate
 *  @param String $in_hash Incoming Activation Hash from the email
*/

function activate($user_id = null, $in_hash = null) {
        $this->User->id = $id;
        if ($this->User->exists() && ($in_hash == $this->User->getActivationHash()))
        {
                // Update the active flag in the database
                $this->User->saveField(‘active’, 1);
               
                // Let the user know they can now log in!
                $this->Session->setFlash(‘Your account has been activated, please log in below’);
                $this->redirect(‘login’);
        }
       
        // Activation failed, render ‘/views/user/activate.ctp’ which should tell the user.
}
?>

And there we have it, now when your users register they have to confirm their user accounts via Email - job done!

  1. 14 Responses to “CakePHP - Activating User Account via Email”

  2. I don’t understand how adding the current Ymd will translate to a 24 hour activation period. Say the user registers for the account at 11:55pm and doesn’t activate the email until 12:05am (10 mins later). Won’t the activation fail because the date changed?

    By Moses on Jul 4, 2008

  3. @Moses
    You are indeed correct, the activation link will expire at the end of each day - this is definitely a limitation and could be removed simply by replacing the date(Ymd); part and checking that the user’s account was created within a given time period. (ie: 24 hours from the point of creation).

    By Jonny on Jul 4, 2008

  4. Just wondering… you never passed $user_id into the function __sendActivationEmail($user_id) in the register() function… how does it know the id?

    By Jason on Jul 10, 2008

  5. @Jason
    Well spotted! I’ve fixed the codeblock above to include the lastInsertId from the User Model.

    By Jonny on Jul 10, 2008

  6. Hi Jonny,
    Greetings from Ireland! Thanks for the tutorial, I’m finding it really helpful for a cake app I’m building at the moment.

    Just a couple of things I noticed while using some of your code.

    In the activate action, you’re missing an closing bracket at: if ($this->User->exists() && ($in_hash == $this->User->getActivationHash())

    ** should be an extra ) on the end **

    also, just in the user_confirm.ctp email view, the variable should be $activate_url not $activation_link, otherwise you’ll end up email a pile of cake debug info to your new users (depending on your debug level, of course)

    Otherwise, everything is solid, and exactly what I was looking for, so thanks again!

    Paul McClean

    By Paul McClean on Jul 15, 2008

  7. Thanks for your feedback; I must admit that I was writing the code from memory rather than copying and pasting code from a live application - therefore there are a few parsing errors in my examples. In future I will make sure I test all code examples given.

    Or maybe I’m should just leave those mistakes in there to keep developers on their toes? ;)

    By Jonny on Jul 15, 2008

  8. Thank you, nice work!

    Only one question: If someone sends not only username and passwrd values to the server, but posts also “activate=1″, this value is apparently not deleted from the $data Array by the register-action, right? If this is correct and I haven’t overlooked something, this should be checked before writing the data to the db, otherwise one could register directly with an acitvated account without email verification.

    By Thomas on Oct 30, 2008

  9. Very useful article Jonny. Thanks for the writeup.

    You’re probably already aware of this, but, if you’re not too concerned about showing a custom message when the trying to login with an inactive account, you could use:

    $this->Auth->userScope = array(’User.active’=>1);

    This lets the Auth component automatically handle the ‘active’ condition while logging in.

    API: http://snipr.com/93wb4

    Cheers,
    Farez

    By Farez on Dec 25, 2008

  10. Hi there and may i say what a great article you’ve compiled here, exactly what i was looking for. I was just wondering if there are some compatibility issues with 1.2: i cant seem to get it to work. The problem is i’m getting sent to the activate.ctp every time. The string thats getting sent to my email address is 8 characters long - is that right? Also in the activate function im getting a undefined variable error for this line:

    function activate($user_id = null, $in_hash = null) {
    $this->User->id = $id;

    Im probably overlooking something obvious but i just cant get it to work - can anybody help me?

    Thank you in advance for your time.

    By Steppio on Jan 27, 2009

  11. Thanks so much for this - it taught me a lot and helped greatly.

    Not that it went totally smoothly - because I’m a neophyte programmer teaching myself cake and php simultaneously and my application is laid out differently with additional code embedded within your email activation framework.

    My learning’s from yesterday - beware when you are getting only part of the expected result. I use an Auth based permission system (adapted from another tutorial) that defaults to login when a user isn’t permitted to access a URL. When I clicked the email and went to login I thought everything was working - EXCEPT the active field wasn’t getting updated and I couldn’t understand why.

    It took a day, and traipsing through increasingly hair-brained possibilities (and learning new stuff along the way) before doing the obvious, change the redirect page and see whether the activate action was being used. It wasn’t. From there it was just a small step understand what was really going on and add activate to Auth’s before filter list.

    It’s a long story - but without tutorials like yours, teaching oneself to code would be incredibly difficult, and building a functional site (even if it lives on localhost) would be totally impossible. Thanks!

    By Peter Childs on Feb 18, 2009

  12. I couldn’t get the email to send out an activation link with the user id in it.

    To get that to work i had to change in the email function:

    $user = $this->User->find(array(‘User.id’ => $user_id), array(‘User.email’, ‘User.username’), null, false);

    to this:

    $user = $this->User->find(array(’User.id’ => $user_id), array(’User.id’, ‘User.user_email’, ‘User.username’), null, false);

    By james on Mar 26, 2009

  13. Hi!!

    How do I authenticate user without login when user clicks on activate link from email & redirect to an action of different controller(this too authenticated). I dont want the user to login when clicked on activation link & is authenticated automatically. I am new to cakephp. Any help will be appreciated. Thanks.

    By ZedR on May 14, 2009

  14. This article is exactly what I was looking for but I am experiencing a lot of trouble adjusting the code to work with Cake 1.2 - I am new to Cake. Is there a chance someone can clarify what to do for things such as:

    __sendActivationEmail(…)
    $user['User']['id']
    giving me Undefined index: id

    function activate(…)
    $this->User->id = $id;
    giving me Undefined variable: id

    Any chance someone can port this??

    I hope this isn’t a stupid question - Sorry in advance if it is!

    By bryan on Jun 10, 2009

  1. 1 Trackback(s)

  2. Jun 6, 2008: links for 2008-06-06 « Richard@Home

Post a Comment