«

»

Feb 28

Secure PHP Login

When perusing the internet for discussions on PHP sessions and cookies in regards to credential validation and user logins, I’ve never been satisfied with the approaches I find. Many of the tutorials are just plain lousy or incomplete. And the others seem to imply that you should only use sessions or cookies and never mix-and-match, a confusion that would probably trip up many PHP novices. So I’ve decided to post a tutorial explaining the complete PHP login format I use for my sites and web applications.

How it Works

The way to create secure pages using PHP is a simple enough concept: determine the pages that can only be visited by logged in users and put a piece of code at the top of them to redirect logged out users to a login page. If a user visits the login page and is already logged in, they should be redirected to the main page.

So, how do you determine if a user has been logged in? You have PHP to see if there’s a fingerprint that pairs the server to the client’s computer. To do this, PHP provides access to two mechanisms: sessions and cookies. Once a user has logged in with a valid username and password, you fingerprint either the server (session) or the client’s computer (cookie). Once the fingerprint is in place, each secured page just needs to check to see if it exists. If it does, show the page to the user; if not, kick the user back to the login page.

It’s that simple.

Comparing Sessions and Cookies

Before you can really proceed, you need to understand the primary differences between sessions and cookies in PHP (and, well, anywhere). Let’s break them down for comparison:

Cookie

  • Stored on client’s computer
  • Slower, since they have to be sent to the server from the client’s computer
  • Limited on size and how many can be stored on the client’s computer
  • Can be used across multiple servers
  • Can have a lengthy lifespan
  • Can be viewed and modified by client and can therefore be a security risk, depending on the content
  • Not available until page reloads, since cookies will be sent to the server on page load

Session

  • Stored on server
  • Faster, since they are already on the server
  • Less bandwidth transfer since, rather than sending all data from client to server, the session only sends the session ID to be stored in a cookie on the client’s computer
  • Size of a session is dependent on the PHP memory limit set in php.ini, but my guess is that limit is significantly higher on your server than the 4k generally allotted to cookies
  • Cannot be used across multiple servers
  • Lifespan is very short; always destroyed when browser has been closed
  • Can only be accessed through the server, so much more secure than cookies
  • Available immediately in code without a page reload

From the above, you should be able to deduce that if you are working with sensitive data (passwords, credit card data, etc.), a session should be used. If you simply want to carry non-sensitive data between pages (the contents of a shopping cart), a cookie may be used.

Now that we understand the differences between sessions and cookies functionally speaking, what are they? Basically, as far as the code is concerned, they’re just arrays. The cookie array can be accessed using $_COOKIE['project-name']['val-name'], and the session array is conditionally accessible by referencing $_SESSION['project-name']['val-name']. The session array is only accessible if you have started a session by calling session_start().

To store a value into a cookie, we use the provided function setcookie(‘project-name[val-name]‘, $myData, time () + $keepAlive). Now let’s break this down: val-name will be the string used to reference this cookie as shown in the paragraph above. Whatever is in $myData is the string that will be stored in the cookie, and the cookie will stay alive until $keepAlive seconds from the current time have passed.

To store a value into a session is much easier. After a session has started, you simply execute $_SESSION['project-name']['val-name'] = $myData. The values will be accessible as shown above so long as the session exists—that is to say, so long as the browser has not been closed and session_destory() has not been called.

With this understanding of sessions and cookies now, you should be able to see that a session will be useful in allowing a user to login to a secured page, but that it will not allow a user to close the browser and return to that page still logged in. We’re just about to dive into the code that will allow for both of those things, but first let’s look at a common oversight.

The Shared Server Conundrum

This is a sneaky issue, because you likely won’t know that it exists until your security has been compromised, so I’ll let you in on the secret now.

PHP session variables are stored in /tmp by default, and this is true for any user on a server. Since the HTTP server software has access to read and write from this folder, and all users of a shared server execute from that same user, there is never a complete guarantee that your sessions are completely safe when you’re in a shared server environment. It is also possible for session collisions to occur because of this, for instance, if you and another user on a shared server are using the same session string. For this reason, it’s a good idea to regularly regenerate the session ID, and it’s also smart to use session strings that are related to the application you’re working with.

Another issue with shared server sessions in PHP is their timeout time. Though you may set a session timeout to be five hours, if another user on the shared server sets the timeout to be something else, say two hours, all of your sessions will also timeout in two hours, since PHP does not disambiguate between users within the /tmp folder.

I don’t know of a remedy for the timeout issue, though you may be able to contact your server admin to ask if there is a user-based php.ini file that could be configured to store your sessions somewhere other than /tmp. There are also ways to store your sessions in a database, which would get rid of both of these potential issues.

Regardless, neither of these issues are extreme vulnerabilities, but they should be something you’re aware of. If your application simply cannot share its sessions with other users, or your session data needs to be tightly maintained and secured, your best bet is to go with a dedicated server.

User Database

Before we can make a secured page that only certain users have access to, we need an access list of those users and their credentials, right? The way we achieve that goal is with a database. In our code example below, we’re using a MySQL database, so you’ll need to perform the following steps using MySQL:

  • Create a database named project_name
  • Create a table within project_name named Users
  • Users should have (at least) three columns: UserID int(11), Username char(25), and Password char(60)
    • The UserID column needs to be unique and auto-incrementing, starting at one (1)—the code below checks for a UserID equal to zero, which means that the user was not in the database
    • Ideally, the UserID column should be the primary index for the table
  • Users should have (at least) one row added: plain text Username, and hashed Password

Once a MySQL database setup like this, you’re ready to write the PHP code.

If you are a PHP beginner, please look into database sanitization. Anytime you are going to be accepting input from a web form and passing that input into a database (for example, in the case of accepting user credentials and logging that user into the website), you need to sanitize the inputs to prevent potential attacks on your website. In the source code below, database inputs are sanitized through the use PHP’s PDO library.

The Code

The snippets of PHP code below are robust enough to be deployed with a large-scale web application. If all you require is a simple authentication page and don’t much plan on using the session variables throughout your user’s stay, this code can easily be trimmed down to fit those needs as well. So, let’s walk through the code, shall we?

class-databasehelpers.php

If you are making a large-scale web application a database helpers class can help streamline repetitive database calls. If you are making a more simple login interface, you can move the functionality within this class to functions.php.

If your application eventually has a settings.php file, it’d make more sense to move the defined database constants out there.

<?php

define ('DB_HOST', 'localhost');
define ('DB_NAME', 'project_name');
define ('DB_USERNAME', 'sql-username');
define ('DB_PASSWORD', 'sql-password');

class DatabaseHelpers
{
   function blowfishCrypt($password, $length)
   {
      $chars = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
      $salt = sprintf ('$2a$%02d$', $length);
      for ($i=0; $i < 22; $i++)
      {
         $salt .= $chars[rand (0,63)];
      }

      return crypt ($password, $salt);
   }

   public function getDatabaseConnection()
   {
      $dbh = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME, DB_USERNAME, DB_PASSWORD);

      $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

      return $dbh;
   }
}

?>

class-userdata.php

The UserData class should be an almost identical interface to the MySQL Users table. Almost identical. You should not have the Password field, as PHP will handle checking that value and beyond that the user’s password, hashed or not, should never need to be displayed.

This class is unused by this tutorial, but it is a template that can be used to easily retrieve information from a database table. When you’re ready to move on beyond the login page, you can easily use PDO to fill class variables from corresponding variables in a database table with a call like $stmt->setFetchMode(PDO::FETCH_CLASS, ‘UserData’), and then calling $stmt->fetch(PDO::FETCH_CLASS) to fill the class variables.

<?php

class UserData
{
   public $UserID;
   public $Username;
}

?>

class-users.php

The Users class is used to retrieve, assess, and modify data stored in the UserData class. For our purposes, we only need a checkCredentials() function to validate the given username and password against MySQL database elements.

<?php

require_once ('class-databasehelpers.php');
require_once ('class-userdata.php');

class Users
{
   public function checkCredentials($username, $password)
   {
      // A UserID of 0 from the database indicates that the username/password pair
      // could not be found in the database
      $userID = 0;
      $digest = '';

      try
      {
         $dbh = DatabaseHelpers::getDatabaseConnection();

         // Build a prepared statement that looks for a row containing the given
         // username/password pair
         $stmt = $dbh->prepare('SELECT UserID, Password FROM Users WHERE ' .
                               'Username=:username ' .
                               'LIMIT 1');

         $stmt->bindParam(':username', $username, PDO::PARAM_STR);

         $success = $stmt->execute();

         // If results were returned from executing the MySQL command, we
         // have found the user
         if ($success)
         {
            // Ensure provided password matches stored hash
            $userData = $stmt->fetch();
            $digest = $userData['Password'];
            if (crypt ($password, $digest) == $digest)
            {
               $userID = $userData['UserID'];
            }
         }

         $dbh = null;
      }
      catch (PDOException $e)
      {
         $userID = 0;
         $digest = '';
      }

      return array ($userID, $username, $digest);
   }
}

?>

pages.php

This class acts as an enum of pages on your site.

<?php

// To get around the fact that PHP won't allow you to declare
// a const with an expression, define our constants outside
// the Page class, then use these variables within the class
define ('LOGIN', 'Login');
define ('INDEX', 'Index');

class Page
{
   const LOGIN = LOGIN;
   const INDEX = INDEX;
}

?>

functions.php

Here’s where it gets fun. As you create more pages that should only be accessible to validated users, make sure you add them as an OR to the return of isSecuredPage().

The checkLoggedIn() function is our primary work house. This function checks to see if the current page requires validation. If the page requires validation and the user is not logged in, they are redirected to login.php. If a user has been logged in and visits the login page, they are redirected to the main page. If the user has been logged in, this function allows them to access secured pages. The checkLoggedIn() function is also responsible for completing both the login and logout process, and on successful login it sets the proper session and cookie variables.

Take note of how the secondDigest cookie parameter is being used. We need to store authentication information in the cookie so we can securely implement the “Remember me” functionality, but if all we store are credentials, the cookie could still be stolen and used. To prevent against this, we also store physical characteristics of the connection, in this case IP address and HTTP User Agent information. That data should be hashed as well so a hijacker can’t just spoof it when they steal the cookie. Now, if a hijacker takes our cookie to their own computer, the cookie will pass user authentication but fail the second digest, and the hijacker will be prompted to login again.

You would be wise to modify what exactly is in the second digest. If a standard were used, hashing it would pointless, even with the salt. Additional salt beyond the Blowfish cypher would be good, adding additional information, reordering the information before it’s hashed, etc. For increased security, you could also store the second digest on the server in the Users table, comparing the cookie’s value with that value (which would need to be updated after each successful login).

<?php

require_once ('class-databasehelpers.php');
require_once ('class-users.php');
require_once ('functions.php');
require_once ('pages.php');

function isSecuredPage($page)
{
   // Return true if the given page should only be accessible to validation users
   return $page == Page::INDEX;
}

function checkLoggedIn($page)
{
   $loginDiv = '';
   $action = '';
   if (isset($_POST['action']))
   {
      $action = stripslashes ($_POST['action']);
   }

   session_start ();

   // Check if we're already logged in, and check session information against cookies
   // credentials to protect against session hijacking
   if (isset ($_COOKIE['project-name']['userID']) &&
       crypt($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT'],
             $_COOKIE['project-name']['secondDigest']) ==
       $_COOKIE['project-name']['secondDigest'] &&
       (!isset ($_COOKIE['project-name']['username']) ||
        (isset ($_COOKIE['project-name']['username']) &&
         Users::checkCredentials($_COOKIE['project-name']['username'],
                                 $_COOKIE['project-name']['digest']))))
   {
      // Regenerate the ID to prevent session fixation
      session_regenerate_id ();

      // Restore the session variables, if they don't exist
      if (!isset ($_SESSION['project-name']['userID']))
      {
         $_SESSION['project-name']['userID'] = $_COOKIE['project-name']['userID'];
      }

      // Only redirect us if we're not already on a secured page and are not
      // receiving a logout request
      if (!isSecuredPage ($page) &&
          $action != 'logout')
      {
         header ('Location: ./');

         exit;
      }
   }
   else
   {
      // If we're not already the login page, redirect us to the login page
      if ($page != Page::LOGIN)
      {
         header ('Location: login.php');

         exit;
      }
   }

   // If we're not already logged in, check if we're trying to login or logout
   if ($page == Page::LOGIN && $action != '')
   {
      switch ($action)
      {
         case 'login':
         {
            $userData = Users::checkCredentials (stripslashes ($_POST['login-username']),
                                                 stripslashes ($_POST['password']));
            if ($userData[0] != 0)
            {
               $_SESSION['project-name']['userID'] = $userData[0];
               $_SESSION['project-name']['ip'] = $_SERVER['REMOTE_ADDR'];
               $_SESSION['project-name']['userAgent'] = $_SERVER['HTTP_USER_AGENT'];
               if (isset ($_POST['remember']))
               {
                  // We set a cookie if the user wants to remain logged in after the
                  // browser is closed
                  // This will leave the user logged in for 168 hours, or one week
                  setcookie('project-name[userID]', $userData[0], time () + (3600 * 168));
                  setcookie('project-name[username]',
                  $userData[1], time () + (3600 * 168));
                  setcookie('project-name[digest]', $userData[2], time () + (3600 * 168));
                  setcookie('project-name[secondDigest]',
                  DatabaseHelpers::blowfishCrypt($_SERVER['REMOTE_ADDR'] .
                                                 $_SERVER['HTTP_USER_AGENT'], 10), time () + (3600 * 168));
               }
               else
               {
                  setcookie('project-name[userID]', $userData[0], false);
                  setcookie('project-name[username]', '', false);
                  setcookie('project-name[digest]', '', false);
                  setcookie('project-name[secondDigest]',
                  DatabaseHelpers::blowfishCrypt($_SERVER['REMOTE_ADDR'] .
                                                 $_SERVER['HTTP_USER_AGENT'], 10), time () + (3600 * 168));
               }

               header ('Location: ./');

               exit;
            }
            else
            {
               $loginDiv = '<div id="login-box" class="error">The username or password ' .
                           'you entered is incorrect.</div>';
            }
            break;
         }
         // Destroy the session if we received a logout or don't know the action received
         case 'logout':
         default:
         {
            // Destroy all session and cookie variables
            $_SESSION = array ();
            setcookie('project-name[userID]', '', time () - (3600 * 168));
            setcookie('project-name[username]', '', time () - (3600 * 168));
            setcookie('project-name[digest]', '', time () - (3600 * 168));
            setcookie('project-name[secondDigest]', '', time () - (3600 * 168));

            // Destory the session
            session_destroy ();

            $loginDiv = '<div id="login-box" class="info">Thank you. Come again!</div>';

            break;
         }
      }
   }

   return $loginDiv;
}

?>

login.php

This is the base for a login form on the login page. Notice that now we’re modifying front-centric PHP files, the only reference you see to heavy lifting is a simple call to our checkLoggedIn() function. The form handles POSTing to this page to log the user in and redirect them to index.php.

The $loginDiv that we receive from checkLoggedIn() allows us to display informative statuses to the user, for instance, if they try to login with the wrong password.

<?php

require_once ('functions.php');

// Check to see if we're already logged in or if we have a special status div to report
$loginDiv = checkLoggedIn (Page::LOGIN);

?>

<html>
   <body>
      <h2>Sign in</h2>
      <form name="login" method="post" action="login.php">
         <input type="hidden" name="action" value="login" />
         <label for="login-username">Username:</label><br />
         <input id="login-username" name="login-username" type="text" /><br />
         <label for="password">Password:</label><br />
         <input name="password" type="password" /><br />
         <input id="remember" name="remember" type="checkbox" />
         <label for="remember">Remember me</label><br />
         <?php echo $loginDiv ?>
         <input type="submit" value="Login" />
      </form>
   </body>
</html>

index.php

Last, but certainly not least, our secured pages. All the work we’ve done above to ensure a robust application allows us to make one simple call from a secured page: checkLoggedIn(). Everything we’ve done above handles the rest. Add this call to any page you want to be secured and you’re good to go!

One thing to note is the logout button, which simple POSTs a logout action to login.php.


<?php

require_once ('functions.php');

checkLoggedIn (Page::INDEX);

?>

<html>
   <body>
      <form name="logout" method="post" action="login.php">
         <input type="hidden" name="action" value="logout" />
         <input type="submit" value="Logout" />
      </form>
   </body>
</html>

The Common Exit Issue

Take special note that as soon as it has been determined that checkLoggedIn() in functions.php succeeded or failed (i.e. following a header call to redirect), exit has been called. This is crucial if your secured page makes ready use of your session or cookie variables, because it tells PHP to cease construction of the page immediately. It is a common mistake to not call exit after a header redirect, which is not necessarily insecure, but it is poor practice. If you fail to call exit immediately, the remainder of the page will still be evaluated by PHP (though the variables may not have been initialized), and error reports may occur. Not data will be displayed to the user, but you neglecting to call exit may fill up your PHP error logs.

The Payoff

You now have login page, secured content areas, cookie storage for returning users, and working sessions throughout your pages. What’s cool about this from this point forward is that you can easily apply this new knowledge of cookies and sessions outside of the credentials realm.

You now have live sessions on your pages, so you can store additional values in the $_SESSION variable to carry them between pages. You’ve seen how cookies work, so you can curse your clients with crumbles of your website for the next time they return (don’t be evil).

If you have any further questions regarding the login process, sessions, or cookies, or if you just found this tutorial useful, let me know in a comment.

  • Alan

    How would you add different privilages to different login details? e.g. Admin, standard user, etc
    Also, I’m trying to set up an app she users can create ‘events’ or other items and I need them to be set to be adminned by those users. Any idea how I could achieve this?

    • http://alexlaird.com/ Alex Laird

      This would be acheived by simply adding more columns to the user table. Add a simple int column for the privilege level, then at the same point that retrieve the userID, retrieve the privilege at the same time and stored it in the sessions as well. So, something like:

      $userID = $userData['UserID'];
      $privLegel = $userData['PrivilegeLevel'];

      You would add this in checkCredentials, and you’d return $privLevel in the array as well, so it would then be an element in the $userData array for you to store in cookies/sessions in the same way as you store the userID.

      • Willem

        Thank you so much for this tutorial. I was looking for something like
        this for a very long time. When I want to log in the system keeps on
        telling me that I entered the wrong username or password. As far as I
        can see is that when the user logs in the crypt the password entered by
        the user and then compare it to the password in the database. Shoulded
        the password in the database also not been stored crypted? And if so,
        how can I get to register a user and then store the password in the
        crypt format so I can get authenticated correctly please?

        • http://alexlaird.com/ Alex Laird

          When you create the new user and store the password in the database, you
          can’t call PHP’s md5(). You need to call blowfishCrypt(), which is in
          the DatabaseHelpers class I created in the tutorial. This is the same
          function you use to verify the stored password, and it’s also the
          function you pass a password to acquire the hashed version for storage
          in the database.

          • Rendy Air

            I tried the php code above, but why it always return different result each execution? The login page always return incorrect password.

          • http://alexlaird.com/ Alex Laird

            Code only returns a different result if variables are changing. Something is not setup correctly in your configuration, but not knowing specifically what return value you’re talking about, or how you’ve set things up, it’s hard to tell.

            I do know that this code has worked for many other users, so I’m willing to help you figure out what’s different about your setup, but I would need more information.

          • Nelson

            hello,
            very good tutorial.
            I created the file with the code in this
            tutorial as well as the “password.php” file, but if I run several times it returns different codes for the same password. It’s the same problem like Rendy Air.

            Can you help me?

            Many thanks,
            Nelson

          • http://www.alexlaird.com/ Alex Laird

            If the results are different, the code must be receiving different input at execution, or something is not coded correctly. It’s impossible to assist with this situation without seeing the code you’re using.

          • Nelson

            the code of “password.php” is:

            “”

            thanks,

            Nelson

          • http://www.alexlaird.com/ Alex Laird

            Well, there’s your problem … ;)

          • Nelson

            ok, “password.php” is :
            “require_once (‘functions.php’);
            $crypt = DatabaseHelpers::blowfishCrypt(‘password’, 10);
            echo $crypt;
            “, but the file start with the comands of php

          • http://www.alexlaird.com/ Alex Laird

            Here is an example of the code working. If you’re receiving variable results, what you’re passing in as password must be changing, otherwise a hash will always resolve to the same string. If it didn’t, it wouldn’t be considered a reliable hash.

            http://phpfiddle.org/lite/code/rgy-fee

          • Nelson

            many thank’s,

            Like you see, I’m a beginner. The problem with password was solved and now I understand better the Crypt function .
            My next question is about your correction to make a correct login, you said:
            “The error was in class-users.php, when making the “SELECT” MySQL statement, there was a comma after UserID. The line of code should look like this:

            $stmt = $dbh->prepare(‘SELECT UserID FROM Users WHERE ‘
            . ‘Username=:username ‘
            . ‘AND Password=:hashedPassword ‘
            . ‘LIMIT 1′);”

            why are you use this instruction “:hashedPassword” ?

          • http://www.alexlaird.com/ Alex Laird

            This is old code that does not match the tutorial any longer as I’m using Blowfish Crypt rather than SHA-1 to hash the password now. However, in a prepared statement, something like :hashedPassword would just be interpreted as a variable reference (same as :username), so following that code you would need to pair that variable with an actual value from PHP like so:

            $stmt->bindParam(‘:hashedPassword’, $hashedPassword, PDO::PARAM_STR);

      • jim

        I just stumbled across this tutorial and have tried to access the session variables with no luck. My goal is to implement a privilege system like you are explaining.

        I added a column in the user table with filled it in. I also added the privilegelevel to the SQL statement in the checkCredentials function. then tried storing the variable under the $userID like you show above. Even with the $privLevel added to the $userData array, I cannot retrieve that info. Nor can I even retrieve the $userID.

        I’m not sure why I can’t even echo the $userID. Is this because of the session being regenerated? Any ideas?

  • http://alexlaird.com/ Alex Laird

    The issue lies with a syntax error in the code. I have updated the code and re-tested it, and it should work fine now.

    The error was in class-users.php, when making the “SELECT” MySQL statement, there was a comma after UserID. The line of code should look like this:

    $stmt = $dbh->prepare(‘SELECT UserID FROM Users WHERE ‘
    . ‘Username=:username ‘
    . ‘AND Password=:hashedPassword ‘
    . ‘LIMIT 1′);

  • http://alexlaird.com/ Alex Laird

    Read the section on functions.php, specifically on the isSecuredPage() function.

  • Giorgio

    Thanks for the tutorial, very useful and complete.
    I’ve got a doubt. I’ve seen that a lot of programmers suggest to use ways to escape strings when querying the database. I’ve noticed that you don’t use them. Isn’t it dangerous against sql injections attacks?

    • http://alexlaird.com/ Alex Laird

      Giorgio, thank you for your comment. What you’ve asked is an excellent question!

      I will update the post to clarify this, as it is a post for beginners, and as you pointed out, you wouldn’t want PHP beginners to be under the false assumption that you don’t need to sanitize your database inputs.

      However, in the case of the tutorial above, I am still sanitizing the inputs, just not manually (as many other tutorials do) with PHP functions like mysql_real_escape_string(…), htmlspecialchars(…), etc. If you use PHP Data Objects (the PDO library) to build your database, the PDO library handles the sanitation upon injection. Therefore, to come explicitly answer your question, when using:

      $stmt = $dbh->prepare(…)
      $stmt->bindParam(…)
      $stmt->execute()
      Those are the points in the tutorial when I am sanitizing inputs.

      For more leisurely reading on prepared statements and their awesomeness, I recommend this: http://php.net/manual/en/pdo.prepared-statements.php

  • Giorgio

    Hi Alex and thanks for the detailed reply. I’ve started from your to build a little snippet that may be added to the DatabaseHelpers class, in order to have a more secure input control. Here’s my code:

    public static function escapeData ($data)
    { $dbh = self::getDatabaseConnection(); $data = $dbh->quote(stripslashes($data)); return utf8_encode($data);}

    and a possible usage example:

    // Prepare statement$stmt = $dbh->prepare(‘SELECT author, date, comment ‘ . ‘FROM comments WHERE ‘ . ‘visible=:Visible’); // Execute query
    $visible = DatabaseHelpers::escapeData($_POST['visible']);$stmt->bindParam(‘:Visible’, $visible, PDO::PARAM_STR); $success = $stmt->execute();

    Do you think that this code would ensure a sufficiently high security level to applications?
    Thanks in advance.

  • Hal

    Seems nice this. However, you don’t mention session hijacking. Did you consider potential hijecking risks when writing this code?

    • http://alexlaird.com/ Alex Laird

      I considered it, but I was aiming at a simple tutorial when I originally wrote this article. However, as it has been fleshed out quite a bit since it was originally written, a small section and chunk of a code about session hijacking might not be a bad idea. Thanks for the suggestion.

  • curieuxmurray

    Hi there! Absolutely love this tutorial! I actually use this now as a base in most of my projects! Thanks! One question though… Maybe I just really miss the point, but I don’t see the use (or even where/why it’s used!) of the class in class-userdata.php. Care to explain that to me? :)

    Thanks a lot,

    • http://alexlaird.com/ Alex Laird

      Thanks for pointing that out. You’re correct, I completely don’t explain that :).

      The UserData class is there actually as a part of what session interactions may eventually flesh out to be. It is unused by checkCredentials because we don’t want to flesh out the entire class (though, granted, our UserData class is so small in this tutorial, that doesn’t much matter).

      However, if you’re building a larger session where you’re going to be wanting to look at all UserData frequently, this would be used in a function similar to the following:

      public static function getUserData($userID)
      {
      $userData = null;

      try
      {
      $dbh = DatabaseHelpers::getDatabaseConnection();

      $stmt = $dbh->prepare(‘SELECT UserID, Username FROM Users ‘
      . ‘WHERE UserID = :userID LIMIT 1′);

      $stmt->bindParam(‘:userID’, $userID, PDO::PARAM_INT);

      $success = $stmt->execute();

      # If we executed the statement, then check to make sure we found the
      # record before fetching it.
      if ($success && $stmt->rowCount() > 0)
      {
      # We can use fetch because we only want the first element
      $stmt->setFetchMode(PDO::FETCH_CLASS, ‘UserData’);
      $userData = $stmt->fetch(PDO::FETCH_CLASS);
      }
      }
      catch (PDOException $e)
      {
      trigger_error(Errors::DATABASE_ERROR .
      ‘::PDO exception in getUserData – ‘ .
      $e->getMessage(), E_USER_ERROR);
      }

      return $userData;
      }

      PDO will take care of fill the class. You can review PDO::fetch documentation here: http://php.net/manual/en/pdostatement.fetch.php

      Might not be a bad thing to include in the tutorial. Thanks for the suggestion.

      • http://www.facebook.com/jflr1910 Francisco Jose LR

        could you make an example of protected registration too??? Thank you,

      • Willem

        If I use this would I then do $data = new userData();
        $userID = $data->userID; and so on?

      • curieuxmurray

        Sorry, it took me a while to read the email response I received when you commented! :) Thanks for the info, this is exactly what I though that’d be used for, but I wanted to be sure, maybe you had something else in mind! :)

  • malthe

    Thanks for spending the time creating this tutorial.

    i have a hard time figuring out how to create a user that can actually login. i have all the scripts in place, and dont get any errors. i created a user in the database with name = boss and pass = f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0 which is the sha1 for ‘Hello’ , but it wont log me in. it keeps saying that my username or password is wrong. how can i test this?

    thanks

    • http://alexlaird.com/ Alex Laird

      The tutorial has been updated to use crypt rather than sha1, as sha1 is lousy hashing and does not properly incorporate salting. If you’re still having troubles with the update tutorial, let me know.

      • deagle25

        Great tutorial! I have it all in created all the php files now and have created a user and password encrypted with crypt in the db. I can see the username and salted password in the db but I can’t log on with it because it says “The username or password you entered is incorrect.” Do you have any suggestions to what I might be doing wrong?

        • http://alexlaird.com/ Alex Laird
          • deagle25

            Thanks, Alex! I adapted my user registration page using you code and it now works. I was just calling “crypt” instead of “DatabaseHelpers::blowfishCrypt”. That was the error.

  • Eric

    First: this tutorial is excellent, thank you for taking the time to not only write the code but to spell everything out for us that are less experienced. I do have a couple of questions though, what do you mean by: “As you create more pages that should only be accessible to validated users, make sure you add them as an OR to the return of isSecuredPage().” Could you provide an example?

    • http://alexlaird.com/ Alex Laird

      Notice that in index.php, we call the following:

      checkLoggedIn(Page::INDEX);

      The value to note here is that we pass our current page, Page::INDEX. Let’s assume you have a control panel for your users, so another page called cp.php. This page should only be accessible to a logged in user as well, so we need to add it to our list of secured pages.

      First, add it to pages.php under INDEX:

      const CONTROL_PANEL = CONTROL_PANEL;

      Then, add it to isSecuredPage:

      function isSecuredPage($page)
      {
      // Return true if the given page should only be accessible to validation users
      return ($page == Page::INDEX || $page == Page::CONTROL_PANEL);
      }

      Then, ensure your cp.php file starts the same way your index.php file starts:

      require_once (‘functions.php’);
      checkLoggedIn (Page::CONTROL_PANEL);

      If you do not add the new Page::CONTROL_PANEL check to the isSecuredPage() function, that page will not be considered a locked page, so a user that has not logged in could manually visit the URL and gain access to the page. Since the page would likely use information from the database, the user probably wouldn’t actually gain any significant access, but they would be greeted with many errors, and it would lend to unpredictable results on the server.

      The isSecuredPage() is an overly simplistic way of verifying what pages require a logged in user. However, it could be used as a template for various access levels as well. For instance, let’s say you have admin users, which can do more than standard users. Another function called isAdminPage(), listing valid admin pages, would allow your checkLoggedIn() function to validate a user’s access level when they visit a page as well.

  • romeovs

    great post! I was wondering if there is an easy solution to the form resubmission dialogs I keep getting when I refresh login.php after a logout or a false password entry.

  • http://twitter.com/janineketting Janine Ketting

    Great tutorial, thanks!!

    I have pasted the code (unchanged) into php files. It works fine, but when I push the logout button, the following error appears in my error_log:

    PHP Warning: session_destroy() [function.session-destroy]: Session object destruction failed in /var/www/vhosts/********/httpdocs/********/functions.php on line 126

    I don’t understand, because with the command print_r($_SESSION) I can see the session has started and all session variables.
    Please help!

  • medalla

    Nice work. This was much help.

  • Guy

    Hey your code looks great and is exactly what I was looking for, but I am having issues replacing my current (not so secure) login. Could you be able to help me out?

  • Hidden Below

    Tried to use this, but it lacks a registration page. :(

  • Matthew

    This is great, thanks Alex!!

    Although, I’m having trouble logging in using the set passwords currently stored in my DB. How do I make them work using, lets say, the password of 1234….

    I created a $salt hash password but that failed to log me in… Slightly confused. #Newbie to all of this.

    • http://www.alexlaird.com/ Alex Laird

      The passwords in your database were likely generated with a different salt, or possibly just using SHA, so the values will not match.

      To generate passwords with the algorithm in this tutorial, you simply need to call blowfishCrypt(‘test123′, 10) and check the return value. That is the password you would store in your database. For test123 with a length of 10, that would be $2a$10$HQgAtWPn5zDgaI4Zo0Lpbuxm10xbnJfeLkgTV86zUqmvk30JPxwum

      • Matthew

        I keep getting this…

        Call to undefined function blowfishCrypt() in ………. on line 46.

        I’ve never used function before so I’m not sure how to correctly use them; TBH, I’m not sure how I’ve coped without the up until now! Done some reading and they’re pretty awesome!

        • http://www.alexlaird.com/ Alex Laird

          A function has to be declared before it can be called. Sounds like you’re trying to call blowfishCrypt() on a line that comes before you declaring it as a function.

          • Matthew

            When i took the blowfishCrypt() out of the class it worked…. I need to know more about functions and class’s.

          • http://www.alexlaird.com/ Alex Laird

            Certainly, it should either be global, or if it’s static and in a class (like in this tutorial) you’d need to fully qualify the call, so DatabaseHelpers::blowfishCrypt().

            If you’re struggling through this tutorial, it assumes you have a working knowledge of PHP but have not yet implemented login/authentication before. Zend has a great compilation of tutorials on PHP basics you might have a look at: http://devzone.zend.com/6/php-101-php-for-the-absolute-beginner/

          • Matthew

            Ahhh – Perfect. – Never used PDO before, wether that has anything to do with my lack of skill. I’m reasonably familiar with much of the basic features of PHP, and some advanced, but I shall look at this site, I need a site like this! Ta.

  • Michael

    Hi Alex, great tutorial, just a quick question regarding the Page class and isSecuredPage() function. Are they really necessary? Except for checking if the current page is the login page, I can’t find the point of the check that the current page is the PAGE::current page. If it wasn’t meant to be secured wouldn’t just not add the check? It’s 12:15am at the moment and I may be starved of sleep but nothing is popping out why..

    • http://www.alexlaird.com/ Alex Laird

      Michael,

      This is a helper function and isn’t necessary for a logn page. I like keeping my code modular, so when I wrote this tutorial, this was one way of separating the list of secured pages away from the verification check implementation.

      You wouldn’t need to use this function if you modified the tutorial slightly, it’s just the way that made most sense to me for this basic tutorial (though I don’t implement this way on my own auth pages).

  • Dean Anthony

    Superb tutorial. Really appreciate the explanations. You must be a busy man, but is there any scope to creating the associated registration page for this secure log-in tutorial?

  • SyWill

    First, GREAT tutorial, touched on alot of great subjects, I just wanted to add my two cents about the script based page detection. I modified it slightly and incorporated two of my own str contains functions so it is simple enough now to just pass the requested uri to isSecurePage function and it will check against the array created just in that one function so only one place to maintain list plus it allows for complete directory protection with minimal changes to your code. Just figured id post to see if it would help anyone and get thoughts. Thanks though on the amazing tut friend! keep up the great work.

    function str_contains($haystack, $needle, $ignoreCase = false){
    if($ignoreCase){
    $haystack = strtolower($haystack);
    $needle = strtolower($needle);
    }
    $needlePos = strpos($haystack, $needle);
    return ($needlePos === false ? false : ($needlePos+1));
    }

    function str_contains_arr($haystack, $needles, $ignoreCase = false){
    $result = array();
    $bFound = false;
    if(!is_array($needles)){
    return str_contains($haystack, $needles, $ignoreCase);
    }
    for($x = 0; $x $x, ‘position’=>$strPos);
    }
    }
    return $bFound == false ? false : $result;
    }

    function isSecuredPage($uri){
    $secPageList = array();
    $secPageList[] = “/admin”;
    $secPageList[] = “/restricted”;
    $secPageList[] = “randompage.php”;
    return str_contains_arr($uri, $secPageList, false);
    }

  • 遊斗

    the query,

    $stmt = $dbh->prepare(‘SELECT UserID, Password FROM Users WHERE ‘ .
    ‘Username=:username ‘ .
    ‘LIMIT 1′);

    here you can freely change the values according to you tablename right?
    but still I got the error “The username or password you entered is incorrect.”

    on functions.php,

    the part with ‘project-name’
    what is it?

    I also tried copying all the codes without changing the variables and creating the database but still I got the error “The username or password you entered is incorrect.”

    what I might be doing wrong