Storing encrypted session information in a cookie

cookie

Our session system is due for an upgrade. Currently all PHP sessions are stored in the database, and some things are getting a bit slow. There have been a couple of approaches I've been considering, one of which is simply storing all the information in a browser cookie.

First I want to make clear I don't necessarily condone this. The reason I'm writing this post, is because I'm hoping for some more community feedback. Is this a really bad idea? I would love to know.

The benefits

If all the session data is stored in the browser, it means that I don't need to store it on the server. I actually don't care all that much for having the data on the server (unless it's the only secure way), it's mostly a gigantic map with session tokens and user id's (along with some other info).

I also feel it's more natural for HTTP, as it makes it a bit more stateless.

Sample code

<?php

class BrowserSession {

    public $secret = 'this will need to be a cryptographic random number';
    public $currentUser = null;

    // Sessions time out after 10 minutes
    public $timeout = 600;

    function init() {

        if (!isset($_COOKIE['MYSESSION'])) {
            echo "No session cookie found\n";
            return;
        }

        list($userId, $time, $signature) = explode(':',$_COOKIE['MYSESSION']);

        // The cookie is old
        if ($time> time() + $this->timeout) {
            echo "The session cookie timed out\n";
        }

        if ($signature !== $this->generateSignature($userId,$time)) {
            echo "The secret was incorrect\n";
        }

        $this->currentUser = $userId;

        echo "Logged in as user: $userId\n";

    }

    function login($userId) {

        $this->userId = $userId;

        $time = time();

        $cookie = $this->userId . ':' . time() . ':' . $this->generateSignature($userId,$time);

        setcookie('MYSESSION',$cookie,$time+$this->timeout,null,null,null,true);

        echo "Set cookie: $cookie\n";

    }

    function generateSignature($userId,$time) {

        $stringToSign =
           $userId . "\n" .
           $time . "\n" .
           $_SERVER['HTTP_USER_AGENT'] . "\n" .
           $_SERVER['REMOTE_ADDR'];

        return hash_hmac('SHA1',$stringToSign,$this->secret);

    }

}

ob_start();
$session = new BrowserSession();
$session->init();

if (isset($_GET['login'])) $session->login($_GET['login']);
else {

    echo '<br /><a href="?login=1234">Log in as user 1234</a>';

}
?>

A few notes:

  • The preceeding code was just intended as a proof of concept, it's missing some validation.
  • Currently the secret would be the same for every user. I was thinking of appending some per-user information to the secret. If somebody does guess or bruteforce the secret, they would only have access to a single users' information.
  • If a user changes their password, existing sessions should expire. To do this the signature should also include a sequence number that changes when the password changes.
  • Currently this only stores a user id. It could be extended to contain more data, but this is all I need.

So, is there anything fundamentally wrong with this approach? In general the client should never be trusted, but for setups where the security requirements aren't as high (highly subjective, I know) I feel this might be strong enough. OAuth, OpenID and Amazon AWS all seem to trust HMAC+SHA1, but those applications do work differently.

Credit where it's due

I first asked this question on stack overflow. The users there already gave some great suggestions and pointed out some of the flaws. Thank you!