Permanent logins with PHP sessions
Disclaimer: I think this works, but it’s only endured light testing so far. I may be wrong. Please let me know if you think I am.
PHP sessions are a great addition to the application programming environment. The first time I used cookies I used PHP’s setcookie()
function to roll my own system, which can only be described as flaky. Browsers seem to have an overwhelming disdain for cookies in general, each one requiring a complex set of chicken dances to work. PHP sessions may or may not do lots of clever stuff under the hood to address these problems, but they have always worked 100% reliably for me and that confidence has been enough for me to deploy and forget the technology, something which is critical if we are to get on with the business of developing actual software and not constantly writing our own libraries.
But eventually the time came when I wanted to have the option for people to be logged in permanently (or at least for a very long time, let’s say 90 days). Finding a solution to this problem took me a little while, so I thought I’d write it up. I got quite a lot of help from this discussion at the Experts Exchange and obviously the sessions section of the PHP manual.
First piece of knowledge: PHP garbage collects session files when their modified time reaches a predefined timeout, the default being 1440 seconds - about 24 minutes. PHP has a series of INI file settings that govern the sessions system. These allow you, amongst other things, to control this garbage collection, by either raising the timeout, or reducing the probability of collection after that timeout has expired. For my purposes, I wanted the session files to remain intact on disk for up to 90 days (7776000 seconds). The php.ini key for garbage collection is session.gc_maxlifetime
. If you have control over your php.ini file, simply locate and alter that value. If you don’t, you can change these options via a .htaccess file (see below). It’s not enough to change these options using the ini_set
function as the value needs to be maintained for all instances of PHP that are working in the session dir.
According to one expert at the Experts Exchange, changing your session.gc_maxlifetime
will cause problems when PHP instances running other scripts (e.g. belonging to others on a shared server). This can be fixed by moving the session save path to a different location. This can be acheived with session.save_path
parameter.
Presumably, if you set your session.gc_maxlifetime
and then move your session path with ini_set()
, your sessions will be untouched by other PHP instances, meaning you can use just ini_set
to do the initial lifetime change. I haven’t tested this however.
I also decided to specify that my sessions should use cookies only. I set both session.use_cookies
and session.use_only_cookies
to “on” and session.use_trans_sid
to “off”.
As I don’t have access to the php.ini on my production server, I put the following block into the .htaccess for my application:
<IfModule mod_php4.c>
php_value session.gc_maxlifetime "7776000"
php_value session.save_path "sessions"
php_value session.use_cookies "on"
php_value session.use_only_cookies "on"
php_value session.use_trans_sid "off"
</IfModule>
OK, so now a user’s session will be waiting on the server for them to come back, we have to ensure that the other half of the equation - the browser cookie - will wait just as long.
Second piece of knowledge: session_start()
always sends a Set-Cookie header with the default path and no expiry time. No expiry time means that the cookie will be removed when the browser is closed. If session_start()
sends a cookie with an expiry we don’t want, don’t we have something of a chicken egg situation? We need to run session_start()
before we can access our session data, but to make the cookie optionally permanent you have to store that somebody wants a permanent cookie somewhere: the session. There are other options, but they are less then ideal. For example you could store the option in a database, but what if the user wants to be permanently logged in on their home machine, but not when they visit your site from a net café?
The solution is to stop and then restart the session with the new timeout parameter. The algorithm looks like this:
- Call
session_start()
, this sends a duff cookie, but it does give us access to$_SESSION
- Look in the session for the permanent flag, and copy it
- If the user wants a permanent login:
- Run
session_write_close()
, this commits the session to file and closes it - Use
session_set_cookie_params()
with just one argument to set the cookie timeout, 7776000 seconds in our case. - Run
session_start
again, this time it sends a good cookie
- Run
All you need to do when the user logs in is create the permanent flag in the session if they want it.
This solution has been tested and works on Mozilla, IE5/Mac, Safari, IE6/PC and IE5/PC.
Because two Set-Cookie headers are sent with each response, it’s conceivable, even likely, that some browser somewhere will get confused and set the expiry time wrong. In this case it should be possible to do the work of the original session_start()
call manually. Get the cookie using the $_COOKIE
array, find the session file, parse the file for the perma flag (unserialize()
didn’t work as I hoped it might have done) and then resume the original process at step 3. I haven’t implemented such a system myself as yet, so this approach is untested.