How to achieve persistent sessions with cookies?
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.