"Keep Me Logged In" - the best approach

194,485

Solution 1

OK, let me put this bluntly: if you're putting user data, or anything derived from user data into a cookie for this purpose, you're doing something wrong.

There. I said it. Now we can move on to the actual answer.

What's wrong with hashing user data, you ask? Well, it comes down to exposure surface and security through obscurity.

Imagine for a second that you're an attacker. You see a cryptographic cookie set for the remember-me on your session. It's 32 characters wide. Gee. That may be an MD5...

Let's also imagine for a second that they know the algorithm that you used. For example:

md5(salt+username+ip+salt)

Now, all an attacker needs to do is brute force the "salt" (which isn't really a salt, but more on that later), and he can now generate all the fake tokens he wants with any username for his IP address! But brute-forcing a salt is hard, right? Absolutely. But modern day GPUs are exceedingly good at it. And unless you use sufficient randomness in it (make it large enough), it's going to fall quickly, and with it the keys to your castle.

In short, the only thing protecting you is the salt, which isn't really protecting you as much as you think.

But Wait!

All of that was predicated that the attacker knows the algorithm! If it's secret and confusing, then you're safe, right? WRONG. That line of thinking has a name: Security Through Obscurity, which should NEVER be relied upon.

The Better Way

The better way is to never let a user's information leave the server, except for the id.

When the user logs in, generate a large (128 to 256 bit) random token. Add that to a database table which maps the token to the userid, and then send it to the client in the cookie.

What if the attacker guesses the random token of another user?

Well, let's do some math here. We're generating a 128 bit random token. That means that there are:

possibilities = 2^128
possibilities = 3.4 * 10^38

Now, to show how absurdly large that number is, let's imagine every server on the internet (let's say 50,000,000 today) trying to brute-force that number at a rate of 1,000,000,000 per second each. In reality your servers would melt under such load, but let's play this out.

guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000

So 50 quadrillion guesses per second. That's fast! Right?

time_to_guess = possibilities / guesses_per_second
time_to_guess = 3.4e38 / 50,000,000,000,000,000
time_to_guess = 6,800,000,000,000,000,000,000

So 6.8 sextillion seconds...

Let's try to bring that down to more friendly numbers.

215,626,585,489,599 years

Or even better:

47917 times the age of the universe

Yes, that's 47917 times the age of the universe...

Basically, it's not going to be cracked.

So to sum up:

The better approach that I recommend is to store the cookie with three parts.

function onLogin($user) {
    $token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit
    storeTokenForUser($user, $token);
    $cookie = $user . ':' . $token;
    $mac = hash_hmac('sha256', $cookie, SECRET_KEY);
    $cookie .= ':' . $mac;
    setcookie('rememberme', $cookie);
}

Then, to validate:

function rememberMe() {
    $cookie = isset($_COOKIE['rememberme']) ? $_COOKIE['rememberme'] : '';
    if ($cookie) {
        list ($user, $token, $mac) = explode(':', $cookie);
        if (!hash_equals(hash_hmac('sha256', $user . ':' . $token, SECRET_KEY), $mac)) {
            return false;
        }
        $usertoken = fetchTokenByUserName($user);
        if (hash_equals($usertoken, $token)) {
            logUserIn($user);
        }
    }
}

Note: Do not use the token or combination of user and token to lookup a record in your database. Always be sure to fetch a record based on the user and use a timing-safe comparison function to compare the fetched token afterwards. More about timing attacks.

Now, it's very important that the SECRET_KEY be a cryptographic secret (generated by something like /dev/urandom and/or derived from a high-entropy input). Also, GenerateRandomToken() needs to be a strong random source (mt_rand() is not nearly strong enough. Use a library, such as RandomLib or random_compat, or mcrypt_create_iv() with DEV_URANDOM)...

The hash_equals() is to prevent timing attacks. If you use a PHP version below PHP 5.6 the function hash_equals() is not supported. In this case you can replace hash_equals() with the timingSafeCompare function:

/**
 * A timing safe equals comparison
 *
 * To prevent leaking length information, it is important
 * that user input is always used as the second parameter.
 *
 * @param string $safe The internal (safe) value to be checked
 * @param string $user The user submitted (unsafe) value
 *
 * @return boolean True if the two strings are identical.
 */
function timingSafeCompare($safe, $user) {
    if (function_exists('hash_equals')) {
        return hash_equals($safe, $user); // PHP 5.6
    }
    // Prevent issues if string length is 0
    $safe .= chr(0);
    $user .= chr(0);

    // mbstring.func_overload can make strlen() return invalid numbers
    // when operating on raw binary strings; force an 8bit charset here:
    if (function_exists('mb_strlen')) {
        $safeLen = mb_strlen($safe, '8bit');
        $userLen = mb_strlen($user, '8bit');
    } else {
        $safeLen = strlen($safe);
        $userLen = strlen($user);
    }

    // Set the result to the difference between the lengths
    $result = $safeLen - $userLen;

    // Note that we ALWAYS iterate over the user-supplied length
    // This is to prevent leaking length information
    for ($i = 0; $i < $userLen; $i++) {
        // Using % here is a trick to prevent notices
        // It's safe, since if the lengths are different
        // $result is already non-0
        $result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
    }

    // They are only identical strings if $result is exactly 0...
    return $result === 0;
}

Solution 2

Security Notice: Basing the cookie off an MD5 hash of deterministic data is a bad idea; it's better to use a random token derived from a CSPRNG. See ircmaxell's answer to this question for a more secure approach.

Usually I do something like this:

  1. User logs in with 'keep me logged in'
  2. Create session
  3. Create a cookie called SOMETHING containing: md5(salt+username+ip+salt) and a cookie called somethingElse containing id
  4. Store cookie in database
  5. User does stuff and leaves ----
  6. User returns, check for somethingElse cookie, if it exists, get the old hash from the database for that user, check of the contents of cookie SOMETHING match with the hash from the database, which should also match with a newly calculated hash (for the ip) thus: cookieHash==databaseHash==md5(salt+username+ip+salt), if they do, goto 2, if they don't goto 1

Of course you can use different cookie names etc. also you can change the content of the cookie a bit, just make sure it isn't to easily created. You can for example also create a user_salt when the user is created and also put that in the cookie.

Also you could use sha1 instead of md5 (or pretty much any algorithm)

Solution 3

Introduction

Your title “Keep Me Logged In” - the best approach make it difficult for me to know where to start because if you are looking at best approach then you would have to consideration the following :

  • Identification
  • Security

Cookies

Cookies are vulnerable, Between common browser cookie-theft vulnerabilities and cross-site scripting attacks we must accept that cookies are not safe. To help improve security you must note that php setcookies has additional functionality such as

bool setcookie ( string $name [, string $value [, int $expire = 0 [, string $path [, string $domain [, bool $secure = false [, bool $httponly = false ]]]]]] )

  • secure (Using HTTPS connection)
  • httponly (Reduce identity theft through XSS attack)

Definitions

  • Token ( Unpredictable random string of n length eg. /dev/urandom)
  • Reference ( Unpredictable random string of n length eg. /dev/urandom)
  • Signature (Generate a keyed hash value using the HMAC method)

Simple Approach

A simple solution would be :

  • User is logged on with Remember Me
  • Login Cookie issued with token & Signature
  • When is returning, Signature is checked
  • If Signature is ok .. then username & token is looked up in the database
  • if not valid .. return to login page
  • If valid automatically login

The above case study summarizes all example given on this page but they disadvantages is that

  • There is no way to know if the cookies was stolen
  • Attacker may be access sensitive operations such as change of password or data such as personal and baking information etc.
  • The compromised cookie would still be valid for the cookie life span

Better Solution

A better solution would be

  • User is logged in and remember me is selected
  • Generate Token & signature and store in cookie
  • The tokens are random and are only valid for single autentication
  • The token are replace on each visit to the site
  • When a non-logged user visit the site the signature, token and username are verified
  • Remember me login should have limited access and not allow modification of password, personal information etc.

Example Code

// Set privateKey
// This should be saved securely 
$key = 'fc4d57ed55a78de1a7b31e711866ef5a2848442349f52cd470008f6d30d47282';
$key = pack("H*", $key); // They key is used in binary form

// Am Using Memecahe as Sample Database
$db = new Memcache();
$db->addserver("127.0.0.1");

try {
    // Start Remember Me
    $rememberMe = new RememberMe($key);
    $rememberMe->setDB($db); // set example database

    // Check if remember me is present
    if ($data = $rememberMe->auth()) {
        printf("Returning User %s\n", $data['user']);

        // Limit Acces Level
        // Disable Change of password and private information etc

    } else {
        // Sample user
        $user = "baba";

        // Do normal login
        $rememberMe->remember($user);
        printf("New Account %s\n", $user);
    }
} catch (Exception $e) {
    printf("#Error  %s\n", $e->getMessage());
}

Class Used

class RememberMe {
    private $key = null;
    private $db;

    function __construct($privatekey) {
        $this->key = $privatekey;
    }

    public function setDB($db) {
        $this->db = $db;
    }

    public function auth() {

        // Check if remeber me cookie is present
        if (! isset($_COOKIE["auto"]) || empty($_COOKIE["auto"])) {
            return false;
        }

        // Decode cookie value
        if (! $cookie = @json_decode($_COOKIE["auto"], true)) {
            return false;
        }

        // Check all parameters
        if (! (isset($cookie['user']) || isset($cookie['token']) || isset($cookie['signature']))) {
            return false;
        }

        $var = $cookie['user'] . $cookie['token'];

        // Check Signature
        if (! $this->verify($var, $cookie['signature'])) {
            throw new Exception("Cokies has been tampared with");
        }

        // Check Database
        $info = $this->db->get($cookie['user']);
        if (! $info) {
            return false; // User must have deleted accout
        }

        // Check User Data
        if (! $info = json_decode($info, true)) {
            throw new Exception("User Data corrupted");
        }

        // Verify Token
        if ($info['token'] !== $cookie['token']) {
            throw new Exception("System Hijacked or User use another browser");
        }

        /**
         * Important
         * To make sure the cookie is always change
         * reset the Token information
         */

        $this->remember($info['user']);
        return $info;
    }

    public function remember($user) {
        $cookie = [
                "user" => $user,
                "token" => $this->getRand(64),
                "signature" => null
        ];
        $cookie['signature'] = $this->hash($cookie['user'] . $cookie['token']);
        $encoded = json_encode($cookie);

        // Add User to database
        $this->db->set($user, $encoded);

        /**
         * Set Cookies
         * In production enviroment Use
         * setcookie("auto", $encoded, time() + $expiration, "/~root/",
         * "example.com", 1, 1);
         */
        setcookie("auto", $encoded); // Sample
    }

    public function verify($data, $hash) {
        $rand = substr($hash, 0, 4);
        return $this->hash($data, $rand) === $hash;
    }

    private function hash($value, $rand = null) {
        $rand = $rand === null ? $this->getRand(4) : $rand;
        return $rand . bin2hex(hash_hmac('sha256', $value . $rand, $this->key, true));
    }

    private function getRand($length) {
        switch (true) {
            case function_exists("mcrypt_create_iv") :
                $r = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
                break;
            case function_exists("openssl_random_pseudo_bytes") :
                $r = openssl_random_pseudo_bytes($length);
                break;
            case is_readable('/dev/urandom') : // deceze
                $r = file_get_contents('/dev/urandom', false, null, 0, $length);
                break;
            default :
                $i = 0;
                $r = "";
                while($i ++ < $length) {
                    $r .= chr(mt_rand(0, 255));
                }
                break;
        }
        return substr(bin2hex($r), 0, $length);
    }
}

Testing in Firefox & Chrome

enter image description here

Advantage

  • Better Security
  • Limited access for attacker
  • When cookie is stolen its only valid for single access
  • When next the original user access the site you can automatically detect and notify the user of theft

Disadvantage

  • Does not support persistent connection via multiple browser (Mobile & Web)
  • The cookie can still be stolen because the user only gets the notification after the next login.

Quick Fix

  • Introduction of approval system for each system that must have persistent connection
  • Use multiple cookies for the authentication

Multiple Cookie Approach

When an attacker is about to steal cookies the only focus it on a particular website or domain eg. example.com

But really you can authenticate a user from 2 different domains (example.com & fakeaddsite.com) and make it look like "Advert Cookie"

  • User Logged on to example.com with remember me
  • Store username, token, reference in cookie
  • Store username, token, reference in Database eg. Memcache
  • Send refrence id via get and iframe to fakeaddsite.com
  • fakeaddsite.com uses the reference to fetch user & token from Database
  • fakeaddsite.com stores the signature
  • When a user is returning fetch signature information with iframe from fakeaddsite.com
  • Combine it data and do the validation
  • ..... you know the remaining

Some people might wonder how can you use 2 different cookies ? Well its possible, imagine example.com = localhost and fakeaddsite.com = 192.168.1.120. If you inspect the cookies it would look like this

enter image description here

From the image above

  • The current site visited is localhost
  • It also contains cookies set from 192.168.1.120

192.168.1.120

  • Only accepts defined HTTP_REFERER
  • Only accepts connection from specified REMOTE_ADDR
  • No JavaScript, No content but consist nothing rather than sign information and add or retrieve it from cookie

Advantage

  • 99% percent of the time you have tricked the attacker
  • You can easily lock the account in the attacker first attempt
  • Attack can be prevented even before the next login like the other methods

Disadvantage

  • Multiple Request to server just for a single login

Improvement

  • Done use iframe use ajax

Solution 4

Old thread, but still a valid concern. I noticed some good responses about security, and avoiding use of 'security through obscurity', but the actual technical methods given were not sufficient in my eyes. Things I must say before I contribute my method:

  • NEVER store a password in clear text...EVER!
  • NEVER store a user's hashed password in more than one location in your database. Your server backend is always capable of pulling the hashed password from the users table. It's not more efficient to store redundant data in lieu of additional DB transactions, the inverse is true.
  • Your Session ID's should be unique, so no two users could ever share an ID, hence the purpose of an ID (could your Driver's License ID number ever match another persons? No.) This generates a two-piece unique combination, based on 2 unique strings. Your Sessions table should use the ID as the PK. To allow multiple devices to be trusted for auto-signin, use another table for trusted devices which contains the list of all validated devices (see my example below), and is mapped using the username.
  • It serves no purpose to hash known data into a cookie, the cookie can be copied. What we are looking for is a complying user device to provide authentic information that cannot be obtained without an attacker compromising the user's machine (again, see my example). This would mean, however, that a legitimate user who forbids his machine's static information (i.e. MAC address, device hostname, useragent if restricted by browser, etc.) from remaining consistent (or spoofs it in the first place) will not be able to use this feature. But if this is a concern, consider the fact that you are offering auto-signin to users whom identify themselves uniquely, so if they refuse to be known by spoofing their MAC, spoofing their useragent, spoofing/changing their hostname, hiding behind proxies, etc., then they are not identifiable, and should never be authenticated for an automatic service. If you want this, you need to look into smart-card access bundled with client-side software that establishes identity for the device being used.

That all being said, there are two great ways to have auto-signin on your system.

First, the cheap, easy way that puts it all on someone else. If you make your site support logging in with, say, your google+ account, you probably have a streamlined google+ button that will log the user in if they are already signed into google (I did that here to answer this question, as I am always signed into google). If you want the user automatically signed in if they are already signed in with a trusted and supported authenticator, and checked the box to do so, have your client-side scripts perform the code behind the corresponding 'sign-in with' button before loading, just be sure to have the server store a unique ID in an auto-signin table that has the username, session ID, and the authenticator used for the user. Since these sign-in methods use AJAX, you are waiting for a response anyway, and that response is either a validated response or a rejection. If you get a validated response, use it as normal, then continue loading the logged in user as normal. Otherwise, the login failed, but don't tell the user, just continue as not logged in, they will notice. This is to prevent an attacker who stole cookies (or forged them in an attempt to escalate privileges) from learning that the user auto-signs into the site.

This is cheap, and might also be considered dirty by some because it tries to validate your potentially already signed in self with places like Google and Facebook, without even telling you. It should, however, not be used on users who have not asked to auto-signin your site, and this particular method is only for external authentication, like with Google+ or FB.

Because an external authenticator was used to tell the server behind the scenes whether or not a user was validated, an attacker cannot obtain anything other than a unique ID, which is useless on its own. I'll elaborate:

  • User 'joe' visits site for first time, Session ID placed in cookie 'session'.
  • User 'joe' Logs in, escalates privileges, gets new Session ID and renews cookie 'session'.
  • User 'joe' elects to auto-signin using google+, gets a unique ID placed in cookie 'keepmesignedin'.
  • User 'joe' has google keep them signed in, allowing your site to auto-signin the user using google in your backend.
  • Attacker systematically tries unique IDs for 'keepmesignedin' (this is public knowledge handed out to every user), and is not signed into anywhere else; tries unique ID given to 'joe'.
  • Server receives Unique ID for 'joe', pulls match in DB for a google+ account.
  • Server sends Attacker to login page that runs an AJAX request to google to login.
  • Google server receives request, uses its API to see Attacker is not logged in currently.
  • Google sends response that there is no currently signed in user over this connection.
  • Attacker's page receives response, script automatically redirects to login page with a POST value encoded in the url.
  • Login page gets the POST value, sends the cookie for 'keepmesignedin' to an empty value and a valid until date of 1-1-1970 to deter an automatic attempt, causing the Attacker's browser to simply delete the cookie.
  • Attacker is given normal first-time login page.

No matter what, even if an attacker uses an ID that does not exist, the attempt should fail on all attempts except when a validated response is received.

This method can and should be used in conjunction with your internal authenticator for those who sign into your site using an external authenticator.

=========

Now, for your very own authenticator system that can auto-signin users, this is how I do it:

DB has a few tables:

TABLE users:
UID - auto increment, PK
username - varchar(255), unique, indexed, NOT NULL
password_hash - varchar(255), NOT NULL
...

Note that the username is capable of being 255 characters long. I have my server program limit usernames in my system to 32 characters, but external authenticators might have usernames with their @domain.tld be larger than that, so I just support the maximum length of an email address for maximum compatibility.

TABLE sessions:
session_id - varchar(?), PK
session_token - varchar(?), NOT NULL
session_data - MediumText, NOT NULL

Note that there is no user field in this table, because the username, when logged in, is in the session data, and the program does not allow null data. The session_id and the session_token can be generated using random md5 hashes, sha1/128/256 hashes, datetime stamps with random strings added to them then hashed, or whatever you would like, but the entropy of your output should remain as high as tolerable to mitigate brute-force attacks from even getting off the ground, and all hashes generated by your session class should be checked for matches in the sessions table prior to attempting to add them.

TABLE autologin:
UID - auto increment, PK
username - varchar(255), NOT NULL, allow duplicates
hostname - varchar(255), NOT NULL, allow duplicates
mac_address - char(23), NOT NULL, unique
token - varchar(?), NOT NULL, allow duplicates
expires - datetime code

MAC addresses by their nature are supposed to be UNIQUE, therefore it makes sense that each entry has a unique value. Hostnames, on the other hand, could be duplicated on separate networks legitimately. How many people use "Home-PC" as one of their computer names? The username is taken from the session data by the server backend, so manipulating it is impossible. As for the token, the same method to generate session tokens for pages should be used to generate tokens in cookies for the user auto-signin. Lastly, the datetime code is added for when the user would need to revalidate their credentials. Either update this datetime on user login keeping it within a few days, or force it to expire regardless of last login keeping it only for a month or so, whichever your design dictates.

This prevents someone from systematically spoofing the MAC and hostname for a user they know auto-signs in. NEVER have the user keep a cookie with their password, clear text or otherwise. Have the token be regenerated on each page navigation, just as you would the session token. This massively reduces the likelihood that an attacker could obtain a valid token cookie and use it to login. Some people will try to say that an attacker could steal the cookies from the victim and do a session replay attack to login. If an attacker could steal the cookies (which is possible), they would certainly have compromised the entire device, meaning they could just use the device to login anyway, which defeats the purpose of stealing cookies entirely. As long as your site runs over HTTPS (which it should when dealing with passwords, CC numbers, or other login systems), you have afforded all the protection to the user that you can within a browser.

One thing to keep in mind: session data should not expire if you use auto-signin. You can expire the ability to continue the session falsely, but validating into the system should resume the session data if it is persistent data that is expected to continue between sessions. If you want both persistent AND non-persistent session data, use another table for persistent session data with the username as the PK, and have the server retrieve it like it would the normal session data, just use another variable.

Once a login has been achieved in this way, the server should still validate the session. This is where you can code expectations for stolen or compromised systems; patterns and other expected results of logins to session data can often lead to conclusions that a system was hijacked or cookies were forged in order to gain access. This is where your ISS Tech can put rules that would trigger an account lockdown or auto-removal of a user from the auto-signin system, keeping attackers out long enough for the user to determine how the attacker succeeded and how to cut them off.

As a closing note, be sure that any recovery attempt, password changes, or login failures past the threshold result in auto-signin being disabled until the user validates properly and acknowledges this has occurred.

I apologize if anyone was expecting code to be given out in my answer, that's not going to happen here. I will say that I use PHP, jQuery, and AJAX to run my sites, and I NEVER use Windows as a server... ever.

Solution 5

I asked one angle of this question here, and the answers will lead you to all the token-based timing-out cookie links you need.

Basically, you do not store the userId in the cookie. You store a one-time token (huge string) which the user uses to pick-up their old login session. Then to make it really secure, you ask for a password for heavy operations (like changing the password itself).

Share:
194,485

Related videos on Youtube

Matthew
Author by

Matthew

Updated on October 21, 2021

Comments

  • Matthew
    Matthew over 2 years

    My web application uses sessions to store information about the user once they've logged in, and to maintain that information as they travel from page to page within the app. In this specific application, I'm storing the user_id, first_name and last_name of the person.

    I'd like to offer a "Keep Me Logged In" option on log in that will put a cookie on the user's machine for two weeks, that will restart their session with the same details when they return to the app.

    What is the best approach for doing this? I don't want to store their user_id in the cookie, as it seems like that would make it easy for one user to try and forge the identity of another user.

  • Jani Hartikainen
    Jani Hartikainen over 14 years
    Tim Jansson's answer describes a good approach to producing the hash though I'd feel safer if it didn't include the password
  • Scott Mitchell
    Scott Mitchell about 14 years
    Why include the IP in the hash? Also, make sure to include timestamp information in the cookie and use this information to establish a maximum age for the cookie so that you are not creating an identity token that is good for eternity.
  • barfoon
    barfoon almost 14 years
    Nice answer. So I am assuming when they log out, the session is destroyed, the cookie is removed, and the record in the DB is deleted?
  • Pim Jager
    Pim Jager almost 14 years
    Yes, on logout you destroy/remove everything, that way the user won't accidentaly be logged in on return.
  • Admin
    Admin almost 13 years
    very nice one... well its nice to use the hash of all the mixture but we can use Session ID (use just the long session id string instead of the hash) generated by the php or any other language those are too very robust!... thanks Pim :)
  • Partack
    Partack almost 13 years
    @Abhishek Dilliwal: This is a pretty old thread but i came across it looking for the same answer as Mathew. I don't think using the session_ID would work for Pim's answer because you can't check the db hash, cookie hash and current session_ID because the session_ID changes every session_start(); just thought i'd point this out.
  • itsmequinn
    itsmequinn over 12 years
    I'm sorry to be dull but what is the purpose of the second cookie somethingELSE? What is id in this case? Is it just a simple sort of "true/false" value to indicate whether the user wants to use the keep me logged in feature at all? If so, why not just check to see if the cookie SOMETHING exists in the first place? If the user didn't want their login to persist, the SOMETHING cookie wouldn't be there in the first place right? Finally, are you generating the hash again dynamically and checking it against the cookie and the DB as an extra measure of security?
  • Danny Bullis
    Danny Bullis over 11 years
    @Pim Jager: I'm also curious why you would need to include the IP - was your solution primarily focused on desktop web applications that would not be expected to be assigned different IPs or would you stand firm on including the IP if the app is intended to be used with mobile / laptop devices across various networks as well?
  • Danny Bullis
    Danny Bullis over 11 years
    @itsmequinn: the purpose of the second cookie somethingELSE is to be stored on the client machine; the app needs to be able to compare the value stored in somethingELSE (e.g. a username, email address, or unique id) with the hashed value that is stored in the database. Notice the comparison step above of "cookieHash==databaseHash". This is the hashed value of the somethingELSE cookie being compared to the value that was stored at a prior time in the database, like when the user originally clicked "keep me logged in." In short, it's how you verify the current client machine is the original one.
  • Danny Bullis
    Danny Bullis over 11 years
    @itsmequinn: sorry ran out of room. Yes, you generate the hash again dynamically with the value stored in somethingELSE, and compare that to the hashed value stored in the database (make sure you hash them the same way). Yes, if somethingELSE isn't there at all, then the user is taken through the login process all over again.
  • Dan Carter
    Dan Carter about 11 years
    If you have your own MD5_encrypt and decrypt functions it will save you from storing the md5 value in the DB
  • Sliq
    Sliq almost 11 years
    In europe most consumer internet providers renew the IP after 24hrs (or earlier) which makes IP based keep-me-logged-in-identification impossible.
  • pamil
    pamil almost 11 years
    Token should be RANDOM, not connected with user/his IP/his useragent/anything in any way. It's major security flaw.
  • DarkBee
    DarkBee almost 11 years
    @ircmaxell : note that $COOKIE in function rememberMe() should be $_COOKIE
  • Aaron Kreider
    Aaron Kreider almost 11 years
    Why do you use two salts? md5(salt+username+ip+salt)
  • Simpler
    Simpler over 10 years
    But doesn't this approach mean that anyone can take this username and cookie and log in as this user from any other device?
  • aborted
    aborted over 10 years
    Excellent! I would suggest providing a complete demonstration on a site like Github - This would become very famous.
  • Al Hennessey
    Al Hennessey over 10 years
    Do you have to store the SECRET_KEY in the database as well as the token?
  • Capsule
    Capsule over 10 years
    @Arken no, the SECRET_KEY must stay somewhere safe in your code. If your database is hacked but not your code, you'll definitely be in trouble but at least your secret is safe.
  • Al Hennessey
    Al Hennessey over 10 years
    ok, so if the secret stays in the code is it randomnly generated everytime the user loads the page, or is a constant variable that never changes?
  • Dan
    Dan over 10 years
    Do you recommend storing the rememberme cookie for say 30 days and if it expires then update to a new token or just keep updating the remember me cookie expiration and keep the token the same?
  • storm_buster
    storm_buster over 10 years
    lol :-), note that 47917 years is the maximum time to guess, the random token could be guessed in 1 hour too.
  • Troy Daniels
    Troy Daniels over 10 years
    @storm_buster It could be guessed in an hour. (Also, it is 47917 ages of the universe, not years.) However, the probability of guessing it in a ages of the universe is a/47917. The expectation value is 47917/2, which seems safe. :-)
  • Leven
    Leven about 10 years
    It's weird because your code contradicts your answer. You say "if you're putting user data into a cookie [...] you're doing something wrong", but that is exactly what your code is doing! Isn't it better to remove the username from the cookie, calculate the hash over the token only (and maybe add the ip address to prevent cookie theft) and then do fetchUsernameByToken instead of fetchTokenByUserName in rememberMe()?
  • Lakshay Dulani
    Lakshay Dulani about 10 years
    @leven the username is kept in the cookie since it can happen that the same random token is generated for two different username.. username becomes the unique identifier here
  • Enigma Plus
    Enigma Plus about 10 years
    As far as I can see, the cookie might as well just be a user id with no encryption. OK - a big, hard to guess user id, but what you have stored is exactly what you need to log in. Am I missing something?
  • ircmaxell
    ircmaxell about 10 years
    @TractionBusinessSolutions yes, if you have it, you're logged in. It's a secret in that case. How else could you do it without it being a secret (yet still functioning to auto-login)? The key here is not obfuscation or anything, but making it hard to guess. So that if you know 1 remember me cookie, you can't generate or predict others. And this solves that attack surface...
  • F21
    F21 over 9 years
    Since PHP 5.6, hash_equals can be used to prevent timing attacks when doing string comparisons.
  • Levite
    Levite over 9 years
    I absolutely love this answer, this said: How does the hmac make a difference in this example. If there was additional unknown information (time, browser info, ...) in it, it would make sense, but as it stands, it seems to make no difference. If the cookie was stolen, then with or without the hmac it would be accepted. If not, then with our without it, it should take an average of 47917/2 life-times of the universe to guess it (ok, plus some security through obscurity added by the SECRET_KEY ;-)). Or am I missing something here?
  • ircmaxell
    ircmaxell over 9 years
    @Levit it prevents someone from taking a valid token, and changing the userid attached to it.
  • Levite
    Levite over 9 years
    @ircmaxell: Ah thx, I see! I store tokens already with associated userid in the database, so I did not think about that. Good point, and great answer!
  • Jan Derk
    Jan Derk over 9 years
    Lot's of good stuff in this answer, but as @levit notices, the hashing inside the cookie is unnecessary as both the user and token are stored in the database. Changing only the userid in the cookie won't help an attacker. The token in the database needs to be hashed (which it isn't here) to prevent misuse in case of database theft. fetchTokenByUserName is a bad idea as you can login from multiple pc's, tablets, smartphones. it is better to check if the combination of token and username is in the database.
  • Jan Derk
    Jan Derk over 9 years
    And if you check on token and userid in the database no timing safe compare is needed either.
  • Levite
    Levite over 9 years
    @JanDerk: Really good points too! Especially multilple tokens per user and storing hashed versions of the passwords instead. I also really like your point on timing-safety on comparing hashes, all hashes being equal in length (I actually wrote this answer recently, about the exact same issue)!
  • user3091530
    user3091530 about 9 years
    Setting a longer session is probably bad because it wastes server resources and it will affect the performance adversely
  • Marcello Mönkemeyer
    Marcello Mönkemeyer about 9 years
    Even though @ircmaxell described the theory quite well, I prefer this approach, as it works excellent without the need to store the user ID (which would be an unwanted disclosure) and also includes more fingerprints than just the user ID and hash to identify the user, such as the browser. This makes it even harder for an attacker to make use of a stolen cookie. It's the best and most secure approach I've seen so far. +1
  • sfy
    sfy about 9 years
    @JanDerk Am I misunderstood, finally what we have got is just store user id in cookie, and compare it with the token and username in database?
  • Jan Derk
    Jan Derk about 9 years
    @Jacob You should store the userid and the token in a cookie and compare that to the hashed(token) / userid combination in the database. You can have multiple hashed(token) / userid combinations for the same user if she logs in from multiple devices.
  • Fernando
    Fernando over 8 years
    Sorry but why do you return false in the middle of the validate function?
  • Jeremy Belolo
    Jeremy Belolo over 8 years
    Hello, and thanks for this answer, I like it. One question though : why do you have to generate 2 random strings - bigUserID & bigKey ? Why don't you generate just 1 and use it ?
  • Enigma Plus
    Enigma Plus over 8 years
    bigKey expires after a predefined amount of time, but bigUserID doesn't. bigUserID is to allow you to have multiple sessions on different devices at the same IP address. Hope that makes sense - I had to think for a moment:)
  • Eugenio
    Eugenio over 8 years
    Regardless of the solution, we cannot do anything if an attacker A steals the cookie directly from the computer of the victim V, am I right? A can login as he was V, without the password. At least until the next time V logs in (if you change the cookie every time). IP checking is not a real solution since people change IP frequently. Someone can say that if A can access V's machine, at that point he can do whatever he wants; it is not true, he could have the possibility to access it just for a very short time (enough to steal the cookie) and then do the rest of the "work" with his machine.
  • Jelle Veraa
    Jelle Veraa about 8 years
    @Fernando I'm copying this script for a site I'm using, as it seems sound, and I encountered the same issue. I'm thinking this may be a bug/error in the script, and it should be negated, i.e. if(!...). I've changed the behavior for if the check fails to redirect to my logout script (to delete the incorrect/not valid cookie) using header(Refresh: ...) and then exit() -ing the validation script. if (!timingSafeCompare( ... )) { header('Refresh: 3; url=login_logout.php'); echo 'Cookie not recognized, sending you to login form.'; exit(); }
  • Adam
    Adam about 8 years
    @Fernando Yes there was an exclamation point missing. This error was smuggled in from an edit in May 2015. The answer is now edited.
  • Adam
    Adam about 8 years
    @JanDerk I understand that you say its better to save the hashed token in the database and remove the $mac value - its cleaner. But there is nothing wrong with the approach of ircmaxell. In case of database theft, the hacker still has no access, because he doesn't know the $mac value. Thus, in ircmaxell's approach it is not necessary to save the hashed token.
  • Martin AJ
    Martin AJ almost 8 years
    You don't store cookie into database?
  • NEW2WEB
    NEW2WEB over 7 years
    Folks, I'd really recommend taking this one step further. First, Don't just have a KEY, why not have a session ID as well. With the combination, you can avoid brute force attacks. If I have a session ID: ABCDEF, and a key 123456, then I can prevent someone from hacking both. If I get a request for session ABCDEF with a key of 123457, then I destroy the ABCDEF session. This way, even if they 'guess' a valid id, unless their first 'guess' at a KEY is perfect, they no longer have a valid session to guess for. As IP concerns, store this in a session table somewhere and compare for VALID requests.
  • Suraj Jain
    Suraj Jain over 6 years
    @ircmaxell You said hmac is for " it prevents someone from taking a valid token, and changing the userid attached to it. ", but even if a person changes that , you are doing fetchTokenByUserName($user), so in that case also it is protected, or am i wrong?
  • Suraj Jain
    Suraj Jain over 6 years
    One thing having hmac can help, is if you found hmac tampered, you can surely know that someone tried to steal cookie, then you can reset every login state. Am i right?
  • Vikas
    Vikas over 6 years
    I have one question @ircmaxell What if a user logs in from different devices? I need to store tokens for both of them with same user id. Now, if a user logs in again and again from same device (or maybe a browser), I have duplicate entries (duplicate in terms of that user, NOT token). How can I solve it?
  • slevin
    slevin almost 6 years
    @Eugenio I am no expert, but I guess that is a vulnerability that cannot be stopped and the same vulnerability is true for tokens also.
  • slevin
    slevin almost 6 years
    @VikasKumar Why not use the same coockie for all the different devices? By the way, you may find JSON Web Token useful for your app, especially if you are creating an SPA.
  • Mohsen Nemati
    Mohsen Nemati about 5 years
    Great Answer! But let me ask a question. Assume a person logs in to his account in a public computer and forgets to log out. So in this case if the second person copies the 128-bit code can login to his account any time he wants by setting a fake cookie in his own pc? I mean shouldn't we do something to let the user only login with a specific device?
  • Thiago A. Klein
    Thiago A. Klein over 4 years
    @ircmaxell: When you use setcookie with : (Colon), in some browsers it's automatically converted to %3A (url encoded format). Maybe you need to use str_replace (%3A -> Colon) before explode.
  • k.b
    k.b almost 3 years
    does any one has whole picture of the discussion or some GitHub repo code?