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. 11 Responses to “CakePHP - Activating User Account via Email”

  2. Good stuff! Thanks for posting :-)

    By Richard@Home on Jun 5, 2008

  3. 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

  4. @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

  5. 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

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

    By Jonny on Jul 10, 2008

  7. 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

  8. 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

  9. The article is good can you please tell me how this can be done in j2ee i’m new to php so i didn’t understanding the above codes. Can you please post the j2ee or jsp version of above code

    By Vasudev on Sep 7, 2008

  10. Hmm. nice stuff .let me look through it . brb

    By mzee.richo on Oct 9, 2008

  11. 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

  1. 1 Trackback(s)

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

Post a Comment