“Keep Me Logged In” - the best approach

后端 未结 12 1551
Happy的楠姐
Happy的楠姐 2020-11-22 08:30

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 ap

相关标签:
12条回答
  • 2020-11-22 08:41

    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

    Off 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)

    0 讨论(0)
  • 2020-11-22 08:44

    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;
    }
    
    0 讨论(0)
  • 2020-11-22 08:49

    I read all the answers and still found it difficult to extract what I was supposed to do. If a picture is worth 1k words I hope this helps others implement a secure persistent storage based on Barry Jaspan's Improved Persistent Login Cookie Best Practice

    If you have questions, feedback, or suggestions, I will try to update the diagram to reflect for the newbie trying to implement a secure persistent login.

    0 讨论(0)
  • 2020-11-22 08:53

    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
    0 讨论(0)
  • 2020-11-22 08:54

    I would recommend the approach mentioned by Stefan (i.e. follow the guidelines in Improved Persistent Login Cookie Best Practice) and also recommend that you make sure your cookies are HttpOnly cookies so they are not accessible to, potentially malicious, JavaScript.

    0 讨论(0)
  • 2020-11-22 08:56

    Generate a hash, maybe with a secret only you know, then store it in your DB so it can be associated with the user. Should work quite well.

    0 讨论(0)
提交回复
热议问题