Creating a Persistent Login Mechanism

I’ve been working on a project recently where one of the requirements was to have a persistent login mechanism. I’m not a great fan of this kind of feature simply because it significantly degrades the security of the application. As the only mechanism whereby data can be persisted across sessions is cookies creating a persistent login means storing data in a cookie on the client. Since the cookie has to contain some sort of value to identify the user when they visit the site again there’s a significant risk that if the cookie is stolen an attacker can log into the application without knowing any user credentials at all. That said, I decided that the risks for this particular application were fairly minimal. The application is open to users by invitation only and all communication with the server is conducted using SSL. Because of this I was able to set the persistent login cookie to be only sent over an SSL connection. There is still a risk if the user chooses to login from a public computer, selecting the option to ‘remember me’, but unfortunately there is nothing I can do about this. It seems to be one of the paradoxes of software development is that making an application easier to use can adversely affect the security.

In creating the persistent login token I followed the principles outlined by Chris Shiflet in ‘Essential PHP Security’. This book is fantastic and would be my top recommendation for any web developer who is concerned about application security (which should be all of us!) to read. The basic principle is to create an identifier and a token if a user chooses to persist a login. These are then set in a cookie and recorded in the database along with a ‘timeout’ value. Shiflet recommends that the maximum time a persistent login cookie should be valid for is seven days to minimise the risk of loss or exposure of the cookie. Since a new cookie is generated every time the user visits the site as long as they visit at least once every seven days they will never need to login again. Finally, if a user chooses to logout the persistent login cookie is deleted so that they really are logged out.

Creating the Cookie

My login form displays a checkbox to the user which, if selected, triggers the mechanism whereby a cookie is set. If the user can be logged in from the submitted username and password I call a method to create the necessary token and identifier and record the values in the database. The code to do this follows:


$identifier = sha1(SALT . sha1($organisation . SALT));
 $token = sha1(uniqid(mt_rand(), true));
 $user->setPersistentLogin($identifier, $token, $organisation);
 setcookie('auth', "$identifier:$token", time() + 60 * 60 * 24 * 7, '/', '' , true);

The identifier is generated from the user’s username for the application, which is then double hashed along with a salt. By creating a secondary identifier in this way I ensure that no details that an attacker could use to log into the application through the login form are set in the cookie. A random token is then generated and hashed before the information is recorded in the database in the setPersistentLogin method of the user object. Finally the cookie holding the login information is set, with an expiry time of one week. My database table includes columns for the identifier and token and a datetime column for the expiry date. The SQL to record the information looks like this (my database access is done using PDO prepared statements):


UPDATE users SET Identifier = :identifier, Token = :token, Timeout = DATE_ADD(NOW(), INTERVAL 7 DAY) WHERE Organisation = :Organisation

Logging in Using the Cookie

When a user requests the login page my controller for that page executes the following code:


if ((isset($_POST['token']) && $_POST['token'] === $_SESSION['token']) || isset($_COOKIE['auth'])) {

if (isset($_POST['token']) && $_POST['token'] === $_SESSION['token']) {

$captcha = isset($_SESSION['captcha']) ? $_SESSION['captcha'] : false;

$this->model->inputs = $_POST;

$this->model->captcha = $captcha;

$this->model->setContent();

} else if (isset($_COOKIE['auth'])) {

list($identifier, $token) = explode(':', $_COOKIE['auth']);

$this->model->identifier = $identifier;

$this->model->token = $token;

$this->model->setContent();

}

if ($this->model->valid) {

//Log the user in and redirect them.

}

} else {

//Call setContent with no values set.

$this->model->setContent();

}

This code looks for the existence of either a form submission or a persistent login cookie. If either is set appropriate properties are set in the model for the page before the setContent() method is called. The part of the setContent() method handling the login looks like this:

try {
if (($this->identifier && $this->token) || $this->inputs) {
 $user = new userAuth();
 if ($this->identifier && $this->token) {
 //Attempt a login based on the persistent token values
 $result = $user->checkPersistentLogin($this->identifier, $this->token);
//No exceptions thrown, successful login.
$this->setPersistentLogin($result['Organisation'], $user);
} else if (isset($this->inputs)) {
 if (! $this->validate($this->inputs)) {
 throw new InvalidArgumentException('Errors in form submission values');
 }
 //Attempt a login based on the form submitted
 $result = $user->checkLogIn($this->results['username'], $this->results['password']);
 }
 //No exceptions thrown, successful login.
 $this->organisation = $result['Organisation'];
 $this->contactName = $result['ContactName'];
 $this->email = $result['Email'];
 $this->questionsAnswered = $result['Questions Answered'] === 'True' ? true : false;
 if (isset($this->results['remember']) && $this->results['remember'] === 'rememberMe') {
 $this->setPersistentLogin($this->organisation, $user);
 }
 $this->valid = true;
 return;
 }
 }
 catch (InvalidArgumentException $e) {
 /**
 * Invalid login attempt.
 * If inputs is set display an error message as there was an invalid form submission.
 * Invalid login from a token won't display this message but the user will see a login form
 */
 if ($this->inputs) {
 $this->content['error'] = 'Either your username or password is incorrect. Please try again.';
 $this->content['captcha'] = true;
 $this->content['username'] = is_array($this->results['username']) ? $this->results['username']['value'] : $this->results['username'];
 $this->content['password'] = is_array($this->results['password']) ? $this->results['password']['value'] : $this->results['password'];
 }
 }

This code looks for the existence of either form values ($this->inputs) or values from a persistent login cookie ($this->identifier and $this->token). For the code checking the persistent login cookie the checkPersistentLogin() method is then called. This throws an InvalidArgumentException if the users details cannot be verified, which is caught later in the example above. If the user is verified a result array is returned and a new persistent login cookie is set. The SQL to check the persistent login is as follows:


SELECT Organisation, ContactName, Email
FROM users
WHERE Identifier = :identifier
AND Token = :token
AND Timeout > DATE_SUB(NOW(), INTERVAL 7 DAY)

Logging a User Out

When logging a user out it’s necessary to delete a persistent login cookie to make sure that they really are logged out. This is done in the following code:


if (isset($_COOKIE['auth'])) {
 setcookie('auth', false, time() + 60 * 60 * 24 * 7, '/', '', true);
 }

The php manual states that:

If the value argument is an empty string, or FALSE, and all other arguments match a previous call to setcookie, then the cookie with the specified name will be deleted from the remote client. This is internally achieved by setting value to ‘deleted’ and expiration time to one year in past.

By using the same arguments to set cookie but setting the value to false this code makes sure that the persistent login cookie is deleted.

I’m fairly confident that this implementation of a persistent login is as secure as I can make it but it can never be as secure as requiring a manual login from a user on each visit. In deciding that a persistent login mechanism was appropriate for this application I considered the user base and how they would likely be using the application. While a persistent login was appropriate for this application it may well not be for others. I’d appreciate any comments or ideas anyone may have on how I’ve done this or on the benefits and risks of persistent login.

2 Comments
  1. Craig says:

    Thanks for the article, useful pointers, I’m building something very similar.

    Any reason you don’t just do something like

    cookieName=”pl” (for persisted login)
    cookieValue=”BigRandomGarbagesdffjkshjdifhioshdfijsdfjisdjfsdf”

    I can’t see any good reason for two DB fields, could you explain your reasoning a bit more?

    Thanks,

    Craig

    Reply
    • Good point and to be totally truthful I’m not entirely sure why I suggested two fields. In my defense I wrote that quite a while ago.. The only reason for the two random fields is that it obfusticates things a little more. If an attacker was able to observe a number of cookies they might be able to work out that the ones called ‘pl’ have a common purpose. However, that would only provide a negligible benefit. It looks like one db field would be quite enough.

      Reply
Leave a Reply




XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>