Skip to content
Caution: You are browsing the legacy symfony 1.x part of this website.

How to achieve persistent sessions with cookies?

Symfony version
Language

Overview

Symfony offers access to cookies via the sfWebRequest and sfWebResponse objects. It makes the use of cookies very easy, and persistent sessions are easily achieved.

Cookie getter and setter

A cookie is a string stored on the client's computer, written by a web application and readable only by the same application - or domain.

In symfony, the setter and getter for cookies are methods of different objects, but that makes sense. To get a cookie, you inspect the request that was sent to the server, thus using the sfWebRequest object. On the other hand, to set a cookie, you modify the response that will be sent to the user, thus using the sfWebResponse object. To manipulate cookies from within an action, use the following shorcuts:

// cookie getter
$string = $this->getRequest()->getCookie('mycookie');
 
// cookie setter
$this->getResponse()->setCookie('mycookie', $value);
 
// cookie setter with options
$this->getResponse()->setCookie('mycookie', $value, $expire, $path, $domain, $secure);

The syntax of the setCookie() method is the same as the one of the basic PHP setcookie() function (refer to the PHP manual for more information). The main advantage of using the sfWebResponse method is that symfony logs cookies, and that you can keep on reading and modifying them until the response is actually sent.

note

If you want to manipulate cookies outside of an action, you will need to access the Request and Answer objects without shortcut:

$request  = sfContext::getInstance()->getRequest();
$response = sfContext::getInstance()->getResponse();

Example of cookie use: Persistent sessions

A good use for cookies (apart from basic session handling, which is completely transparent in symfony) is the persistent sessions functionality. Most of the login forms offer a "remember me" check-box which, when clicked, allows the user to bypass the login process for future sessions.

Basic login

Let's imagine an application where all the modules are secure except the security module. The settings.yml is configured to handle the request of unauthenticated users to the security/index action:

all:
  .settings:
    login_module:           security
    login_action:           index

The model has a User class with at the very least a login and a password field. The indexSuccess.php template shows a login form (without the "remember me" checkbox for now), and handles the submission to the security/login action:

public function executeIndex()
{
}
 
public function executeLogin()
{
  // check if the user exists
  $c = new Criteria();
  $c->add(UserPeer::LOGIN, $this->getRequestParameter('login'));
  $user = UserPeer::doSelectOne($c);
  if ($user)
  {
    // check if the password is correct
    if ($this->getRequestParameter('password') == $user->getPassword())
    {
      // sign in
      $this->getContext()->getUser()->signIn();
      // proceed to home page
      return $this->redirect('main/index');
    }
    else
    {
      $this->getRequest()->setError('password', 'wrong password');
    }
  }
  else
  {
    $this->getRequest()->setError('email', 'this user does not exist');
  }
 
  // an error was found
  return $this->forward('security', 'index');
}

note

The verification of the login and password could also be handled in a custom validator for a better domain model logic, as explained in the askeet tutorial.

Now, let's have a look at this signIn() method in the myUser class:

class myUser extends sfBasicSecurityUser
{
  public function signIn()
  {
    $this->setAuthenticated(true);
  }
 
  public function signOut()
  {
    $this->setAuthenticated(false);
  }
}

So far this is very basic. But it works fine, as long as you ask the user to login for every session.

Persistent sessions

To allow for persistent sessions, the server has to store some information in the client's computer (that is were the cookie comes in) remembering who the user is and that he/she successfully logged in before. Of course, for security reasons, the password cannot be stored in the cookie (and, by the way, that would be incompatible with the sha1 hash password storage method described in the askeet tutorial). So what should be stored in the cookie then? Whatever the cookie stores, it has to be the some data that can be matched to what is in the database, so that the comparison of the two elements achieves the authentication. So, to minimize the risk, a random string will be stored and regenerated every 15 days (the lifetime that will be given to the cookie).

By adding a new remember_key column to the User table (and rebuilding the model). This new field will store the random key, the key will thus be stored both in the cookie on the client's computer and in the database as part of the user's record. The remember key will be set when a user requests to be remembered, so change the sign-in line in the login action by:

// sign in
$remember = $this->getRequestParameter('remember_me');
$this->getContext()->getUser()->signIn($user, $remember);

Don't forget to add a remember_me checkbox to the modules/security/templates/indexSuccess.php form for this to work.

The signIn() method of the myUser class has to be modified to set the remember key both in the database and in the cookie:

public function signIn($user, $remember = false)
{
  $this->setAuthenticated(true);
 
  if ($remember)
  {
    // determine a random key
    if (!$user->getRememberKey())
    {
      $rememberKey = myTools::generate_random_key();
 
      // save the key to the User table
      $user->setRememberKey($rememberKey);
      $user->save();
    }
 
    // save the key to the cookie
    $value = base64_encode(serialize(array($user->getRememberKey(), $user->getLogin())));
    sfContext::getInstance()->getResponse()->setCookie('MyWebSite', $value, time()+60*60*24*15, '/');
  }
}

The generate_random_key() method can be anything that you choose which meets with your security requirements. Now, you just need to change the security/index action a little bit:

public function executeIndex()
{
  if ($this->getRequest()->getCookie('MyWebSite'))
  {
    $value = unserialize(base64_decode($this->getRequest()->getCookie('MyWebSite')));
    $c = new Criteria();
    $c->add(UserPeer::REMEMBER_KEY, $value[0]);
    $c->add(UserPeer::LOGIN, $value[1]);
    $user = UserPeer::doSelectOne($c);
    if ($user)
    {
      // sign in
      $this->getContext()->getUser()->signIn($user);
      // proceed to home page
      return $this->redirect('main/index');
    }
  }
}

This new process reads the cookie and you are done.

note

If some pages of your website are accessible without authentication, then the security/index action is no longer the first action to be executed every time. In order to automatically log users in such cases, you will probably prefer to add a new rememberFilter in your application lib/ directory instead of doing the cookie check in a single action:

class rememberFilter extends sfFilter
{
  public function execute ($filterChain)
  {
    // execute this filter only once
    if ($this->isFirstCall())
    {
      if ($cookie = $this->getContext()->getRequest()->getCookie('MyWebSite'))
      {
        $value = unserialize(base64_decode($cookie));
        $c = new Criteria();
        $c->add(UserPeer::REMEMBER_KEY, $value[0]);
        $c->add(UserPeer::LOGIN, $value[1]);
        $user = UserPeer::doSelectOne($c);
        if ($user)
        {
          // sign in
          $this->getContext()->getUser()->signIn($user);
        }
      }
    }
    // execute next filter
    $filterChain->execute();
  }
}

Of course, you will have to declare this filter in your application filters.yml configuration file:

 rememberFilter:
   class: rememberFilter

One last thing: If the user logs out, don't forget to remove the cookie!

public function signOut()
{
  $this->setAuthenticated(false);
  sfContext::getInstance()->getResponse()->setCookie('MyWebSite', '', time() - 3600, '/');
}

note

This solution works only for non-secure pages, since the security check for secure pages occurs before the custom remember filter. Consequently, if a user with a proper cookie tries to access a secure page without having logged to the site in a non-secure page before, he/she will be redirected to the login page as anybody else. If you want the remember me feature to work for secure pages as well, the implementation has to be slightly different. You must create a myBasicSecurityFilter class, specializing the sfBasicSecurityFilter class, and put the cookie control in it. Then, in the filters.yml, change the name of the security_filter class to this myBasicSecurityFilter. The details of the implementation are left to your sagacity.

Persistent sessions: don't reinvent the wheel

The code descibed above can be quite a pain to rewrite for every new project. Fortunately, you can use the sfGuardPlugin for this purpose. Not only does it automate user management, permissions and credentials with a database, it also includes the "remember me" feature with a technique similar to that described here. So, the good way to enable persistent sessions is to install the sfGuard plugin.

This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.