Clearing up the confusion over session timeouts in PHP and Zend Framework

I’ve recently made a foray into the world of Zend Framework.  If you’ve not come across it, it is one of several popular PHP frameworks that implements the Model View Controller software architecture.

By day I’m mostly devoted to hacking ASP.NET MVC, but having had some experience with PHP I decided to get my teeth stuck into Zend for a new PHP project I’ve been working on.  Somewhere in my head I have another blog post to write about the dyer state that is documentation for Zend, but that’s a topic for another night.

This post details my journey through figuring out how PHP sessions really work and how to fix them for properly bringing down a user’s session after a period of inactivity, in the context of Zend Framework (although most of the post applies to PHP proper).

Get your server’s clock synchronised

The PHP session files are date-stamped when they are created/updated.  The PHP Session cookie that gets sent to the browser has an expiry date-stamp on it.  While you can’t rely on the client’s system clock to be correct, making sure your server’s is makes debugging less of a headache.

On a Windows server this should already be set up – if not check the Date & Time Properties in Control Panel and pay attention to the Internet Time tab.  On a Linux server it’s a case of installing and configuring the ntp package – a bit of Googling should show you the way with whichever distribution you are using.

The options for session timeouts

There are a number of options as to how you can handle session timeouts, as I see it:

  1. Keep the session alive until the user closes their browser
  2. End the session after a fixed period of time (regardless of activity)
  3. End the session after a fixed period of time with no activity
  4. Some combination of the above

I’d put my money on the best user experience being a combination of point 1) and point 3).  This, rather surprisingly, isn’t as straightforward as you would hope.

PHP Session Settings – What they mean

image

There are a handful of PHP settings relating to sessions, and understanding how sessions work and what these settings do is critical to getting the behaviour you desire.

PHP sessions are stored as files (by default) – the files contain a serialised version of the $_SESSION superglobal arrray.  The path to these files is set by session.save_path.

N.B. Security Warning – make sure no other users can get to the session directory.  Files are stored unencrypted and therefore should be treated with the same caution as password files.

An interesting side note: if you inspect the cookie PHPSESSID (the default name for the PHP Session Cookie), you’ll notice a funny looking value.  Now take a look at the contents of the PHP session directory – hey presto, there’s a file called sess_<SESSIONID>. Simple eh.

Each time PHP session_start() is called (which is not just when you first start a session, but every time you want to load session data into your app, i.e. on every request for most apps), PHP works out whether or not it should run the session garbage collector.

Garbage – what garbage?

The session garbage collector runs around and checks all of the timestamps on the session files and compares them to the current time less the session.gc_maxlifetime value, to work out if a file should be deleted (because it has exceeded it’s lifetime).

You might think that the garbage collector would run on every session_start(), to avoid any expired session files being left hanging around.  However there is a performance overhead with this and so PHP uses two settings, session.gc_probability and session.gc_divisor to calculate the probability that it should run the garbage collector (it does this by dividing the two and then rolling it’s own dice to see whether it should run).

N.B. Remember that each time you access the session the timestamp on the session file gets updated, this stops the garbage collector from killing a user’s session while they are still active.

This all sounds very cosy and like it will do just what I want – dump the user’s session after a period of inactivity (as specified by session.gc_maxlifetime).  And on low volume sites you could force the garbage collector to run every time by making the probability 100%.

However, all is not quite so straightforward!

The order of things

I’m sure there’s a good reason for this, but I’m not aware of what it is.  PHP first loads up the session, then works out if it should run the garbage collector.  So even if the garbage collector runs every time and the session file has expired, on the first request the user will appear to still have a session, and then only on the next request they won’t.

Imagine this scenario.  A user has logged in to your site.  They go away for lunch, and during that time their session has expired.  They come back and hit refresh – the screen reloads with their session apparently intact.  All is looking good.  Then they click somewhere else and bam – their session is gone.  Kinda confusing really.

In an ideal world there would be a way to check for dead sessions before the session fires up, and in fact there is, but you have to roll it yourself.

Did I mention Zend Framework?

So far everything I have covered is PHP proper, no mention of Zend.  However the Zend_Session object is based on all of these settings, and therefore it warrants understanding the underlying PHP behaviour first.

The solution to this problem is to roll your own inactivity timeout check.  I needed this to implement inactivity logouts.  I used the following code to do my tracking:

$idleTimeout = 3600; // timeout after 1 hour

if(isset($_SESSION[‘timeout_idle’]) && $_SESSION[‘timeout_idle’] < time()) {
Zend_Session::destroy();
Zend_Session::regenerateId();
header(‘Location: /account/signin’);
exit();
}

$_SESSION[‘timeout_idle’] = time() + $idleTimeout;

N.B. If you’re not using Zend Framework you can exchange the Zend_Session lines for their standard PHP equivalents.

All this does is check if we have an idle timeout set in our session (remember this gets loaded even if the session has technically expired) and if it has fallen past the current time we tear down the session, regenerate the session ID (to avoid picking up the existing session file) and head off to the log in screen.

Specifically to Zend, you can set the following settings in the application.ini file:

resources.session.gc_probability = 1
resources.session.gc_divisor = 1
resources.session.gc_maxlifetime = 3600
resources.session.idle_timeout = 3600

And then place the custom idle time out code inside this function in Bootstrap.php:

protected function _initSession()
{
# set up the session as per the config.
$options = $this->getOptions();
$sessionOptions = array(
‘gc_probability’    =>    $options[‘resources’][‘session’][‘gc_probability’],
‘gc_divisor’        =>    $options[‘resources’][‘session’][‘gc_divisor’],
‘gc_maxlifetime’    =>    $options[‘resources’][‘session’][‘gc_maxlifetime’]
);

$idleTimeout = $options[‘resources’][‘session’][‘idle_timeout’];

Zend_Session::setOptions($sessionOptions);
Zend_Session::start();

# now check for idle timeout.
if(isset($_SESSION[‘timeout_idle’]) && $_SESSION[‘timeout_idle’] < time()) {
Zend_Session::destroy();
Zend_Session::regenerateId();
header(‘Location: /account/signin’);
exit();
}

$_SESSION[‘timeout_idle’] = time() + $idleTimeout;
}

And that’s all there is to it!

If you know the reasoning behind some of these apparently strange PHP session operations then please post them here – I’m left scratching my head in a daze at how complex a matter it has been to implement such a standard bit of user experience.

Advertisements