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.

7 thoughts on “Creating a Persistent Login Mechanism

  1. 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

    1. 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.

  2. Working on a mobile app and will hopefully be integrating some login functionality; so naturally persistent logins are “essential” for mobile apps… Anyway… Great article – definitely helps me logically think through things… one thing I thought about with your logout code is that if you clear the cookie on the remote computer; what about if another computer still has the cookie. There must be some type of clearing/marking of the userID on the database side of things also. Maybe you just didn’t mention it; or I didn’t understand fully. Does what I said sound like a valid addition to the logout code?

    1. Hey Nathanael, thanks for the comment! I don’t think that the issue you raise is really a concern. The contents of the cookie should be unique per user and login session and should therefore never appear on another computer. Of course, if a user logs in from another computer or device then the most recent login will be the one persisted in the database in this scheme. As the token part of the cookie is uniquely generated there should never be an occasion where the same information appears on more than one device. Of course if you wanted to allow persistent logins from the same user from more than one device you’d need to use a different database schema than in the article or store the login tokens in something like APC or Memcache.

  3. Just a quick note for yourself or anyone else reading this…


    $token = sha1(uniqid(mt_rand(), true));

    Is not a secure method for generating tokens, as it’s not nearly random enough (even with the “true” on it) and can be guessed / brute forced by an attacker pretty easily. You should be using something like this:


    $token = bin2hex(openssl_random_pseudo_bytes(20));

    Which will generate a fully random (or as near as possible) string. And as a bonus, no need to sha1 it. 🙂

      1. Yes I figured that, and I did see the date on the original blog post, but I figured since it’s showing up in Google results I’d drop a quick note for others that wander along this way.

        Cheers! 🙂

Leave a Reply