<

Want to win a PS4? Go Premium and enter to win our High-Tech Treats giveaway. Enter to Win

x

PHP Client Registration, Login, Logout and Easy Access Control

Published on
249,416 Points
62,516 Views
59 Endorsements
Last Modified:
Awarded
Foreword
In the years since this article was written, numerous hacking attacks have targeted password-protected web sites.  The storage of client passwords has become a subject of much discussion, some of it useful and some of it misguided.  Of course nobody would store client passwords in clear-text, but many of the attempted solutions (message digests, encryption, etc) are little better than clear-text.  A companion article about password storage is available here: Password Hashing in PHP.  The discussion and article segment below, An Afterword: About Storing Passwords, is obsolete and should not be used as the basis for your applications.

Introduction
A frequent design pattern question for new PHP developers goes something like this, "How do I handle client registration and login?"  It's done in every framework and CMS, and all of them use a similar pattern.  This article builds the pattern step-by-step so you can see what is going on at each part of the code.

For this example, we rely on the PHP session handler to tell us if the client is logged in.  We also use cookies so that we can "remember" that a client is logged in, and we employ a data base table that contains our client information.  

Our implementation of this design pattern gives us the ability to password-protect a web page with a single line of PHP code like this:
access_control();

Open in new window

Furthermore, we can test for a client login (without actually requiring a login) with this:

if (access_control(TRUE)) { /* CLIENT IS ALREADY LOGGED IN */ }

Open in new window


There are some notes and afterwords at the end of this article.  If you've got time, you might want to scroll down and read those now.  They go deeper into issues that you might encounter as you try to use this design.  Then come back and follow along.

Conventions and Standards
1. We agree to the convention that we use session_start() on all pages, no exceptions.  You probably want to add session_start() to the top of a common script that gets included at the top of all your page scripts, so that each page of your web site starts something like this:
<?php 
require_once('RAY_EE_config.php'); 
// PHP AND HTML CODE FOLLOWS BELOW

Open in new window

The "config" script is also a good place to have your data base connection and selection code, as well as your define() statements and your local variables, classes and functions  You probably already knew that you want to have those common elements in a single, easy to find, script.

2. We agree to the convention that we add the access_control() statement to the VERY TOP of every page you want to protect.  Why the top?  Because we may need to use the header() function and it is a protocol restriction of HTTP that all headers must be presented and completed before any browser output.  The header() statement will fail, and your script will fail if this protocol is violated.  There are ways to comply with this protocol while also creating browser output.  The PHP function ob_start() can help with this.  But ob_start() is not necessary for our tasks here -- we process the data in the correct order and produce browser output, if any, only after we have finished with all the header commands.  With those understandings in place, let's begin building the framework for our access control system.  

Common Elements - the Config Script
The first step will be to create the "config" script that contains the common elements needed by all our web page scripts.  You will need to make some customization of these examples.  In particular, you will want to add your own data base credentials near lines 15-20 of the script below.  With that information in place, you should be able to install each of these code snippets on your own server and run them to see the "moving parts" in action.

With our data base credentials in place, we connect to the DB server and select our data base (lines 21-34).

Have a look at the access_control() function that is defined on line 36.  The first thing it does is save the client's entry point so we can return to the right page after client authentication.  We use the REQUEST_URI string instead of PHP_SELF because REQUEST_URI contains not only our URL address, but also the URL arguments.  PHP_SELF has only the URL address.  Next, we test the session UID variable.  If this is set, the client is already logged in, so no further processing is needed, and we return TRUE.  We only get to the next lines of this function if the client is not logged in.  If the client is not logged in, we look at the $test argument.  By default it is set to FALSE, but if the calling script has set it to TRUE, we will return an indicator that the client is not logged in.  If the client is not logged in, and this is NOT a test, we must protect the page.  So we redirect the client browser to our login page and exit the script.

We need one more function to complete our work -- the function that sets the "remember me" cookie.  This definition begins on line 53.  We name the cookie "uuk" for "unique user key" and we make its lifetime dependent on the definition of the constant REMEMBER that we defined on line 10.  More information on setcookie() is available in the PHP online manual, here: http://php.net/manual/en/function.setcookie.php

With the "config" framework in place, we can begin every web page by executing the common code found starting on line 80.  The first thing we test for is the presence of the "uid" in the session array.  If we have that, we have an authenticated client and no other processing is needed.  But if $_SESSION["uid"] is not set we still may be able to remember this client from an earlier visit, via the information in the "uuk" cookie.  If that is cookie is set, we may be able to look up the client in our data base and accomplish the login automatically, thus fulfilling our "remember me" promise.

Because the $_COOKIE array contains data stored on the client computer, it must be considered "tainted" external data.  Therefore a minimum sanity check is to run the data through mysqli::real_escape_string() before using it in our query.  With the data thus prepared, we can query the user table and try to find the UID that matches this unique user key.  If we find that UID in the data base, we copy it into the session array (line 101) and the client is now logged in.  Our last step is to call the remember_me() function on line 104, extending our memory of this client.  By calling this function here, a client who has asked to be remembered and who visits the site frequently will be remembered repeatedly, perhaps forever.  Here is the common "config" script:
<?php // RAY_EE_config.php

// WHEN WE ARE DEBUGGING OUR CODE, WE WANT TO SEE ALL THE ERRORS!
error_reporting(E_ALL);

// REQUIRED FOR PHP 5.1+
date_default_timezone_set('America/Chicago');

// THE LIFE OF THE "REMEMBER ME" COOKIE
define('REMEMBER', 60*60*24*7); // ONE WEEK IN SECONDS

// WE WANT TO START THE SESSION ON EVERY PAGE
session_start();

// CONNECTION AND SELECTION VARIABLES FOR THE DATABASE
$db_host = "localhost"; // PROBABLY THIS IS OK
$db_name = "??";        // GET THESE FROM YOUR HOSTING COMPANY
$db_user = "??";
$db_word = "??";

// OPEN A CONNECTION TO THE DATA BASE SERVER AND SELECT THE DB
$mysqli = new mysqli($db_host, $db_user, $db_word, $db_name);

// DID THE CONNECT/SELECT WORK OR FAIL?
if ($mysqli->connect_errno)
{
    $err
    = "CONNECT FAIL: "
    . $mysqli->connect_errno
    . ' '
    . $mysqli->connect_error
    ;
    trigger_error($err, E_USER_ERROR);
}

// DEFINE THE ACCESS CONTROL FUNCTION
function access_control($test=FALSE)
{
    // REMEMBER HOW WE GOT HERE
    $_SESSION["entry_uri"] = $_SERVER["REQUEST_URI"];

    // IF THE UID IS SET, WE ARE LOGGED IN
    if (isset($_SESSION["uid"])) return $_SESSION["uid"];

    // IF WE ARE NOT LOGGED IN - RESPOND TO THE TEST REQUEST
    if ($test) return FALSE;

    // IF THIS IS NOT A TEST, REDIRECT TO CALL FOR A LOGIN
    header("Location: RAY_EE_login.php");
    exit;
}

// DEFINE THE "REMEMBER ME" COOKIE FUNCTION
function remember_me($uuk)
{
    // CONSTRUCT A "REMEMBER ME" COOKIE WITH THE UNIQUE USER KEY
    $cookie_name    = 'uuk';
    $cookie_value   = $uuk;
    $cookie_expires = time() + date('Z') + REMEMBER;
    $cookie_path    = '/';
    $cookie_domain  = NULL;
    $cookie_secure  = FALSE;
    $cookie_http    = TRUE; // HIDE COOKIE FROM JAVASCRIPT (PHP 5.2+)

    // SEE http://php.net/manual/en/function.setcookie.php
    setcookie
    ( $cookie_name
    , $cookie_value
    , $cookie_expires
    , $cookie_path
    , $cookie_domain
    , $cookie_secure
    , $cookie_http
    )
    ;
}


// DETERMINE IF THE CLIENT IS ALREADY LOGGED IN BECAUSE OF THE SESSION ARRAY
if (!isset($_SESSION["uid"]))
{

    // DETERMINE IF THE CLIENT IS ALREADY LOGGED IN BECAUSE OF "REMEMBER ME" FEATURE
    if (isset($_COOKIE["uuk"]))
    {
        $uuk = $mysqli->real_escape_string($_COOKIE["uuk"]);
        $sql = "SELECT uid FROM EE_userTable WHERE uuk = '$uuk' LIMIT 1";
        $res = $mysqli->query($sql);

        // IF THE QUERY SUCCEEDED
        if ($res)
        {
            // THERE SHOULD BE ONE ROW
            $num = $res->num_rows;
            if ($num)
            {
                // RETRIEVE THE ROW FROM THE QUERY RESULTS SET
                $row = $res->fetch_assoc();

                // STORE THE USER-ID IN THE SESSION ARRAY
                $_SESSION["uid"] = $row["uid"];

                // EXTEND THE "REMEMBER ME" COOKIE
                remember_me($uuk);
            }
        }
    }
}

Open in new window


The DB Table - Our Client Data Model
Now that we have created our common "config" script, it is time to create the data base table that will facilitate the access control functionality.  This code in the "create" snippet, below, should do it nicely.  The EE_userTable contains three columns.  These are the user identity in the "uid" column, the user password in the "pwd" column and the user's unique key in the "uuk" column.  In real life we would never store the password in clear text; we would store an abstraction or encrypted version of the password. (Please see the Afterword at the end of this article).  But for this example, it makes the design easier to understand if we process the password in clear text.  And in real life we would probably have many other fields associated with the client record - perhaps an email address and a DATETIME field indicating when the client registered, etc.
<?php // RAY_EE_create.php
require_once('RAY_EE_config.php');

// ACTIVATE THIS TO DROP THE OLD EE_userTable
// $mysqli->query("DROP TABLE EE_userTable");

$sql
= "CREATE TABLE EE_userTable
( _key INT         NOT NULL AUTO_INCREMENT
, uid  VARCHAR(16) NOT NULL DEFAULT '?'
, pwd  VARCHAR(16) NOT NULL DEFAULT '?'
, uuk  VARCHAR(32) NOT NULL DEFAULT '?'
, PRIMARY KEY (_key)
)
"
;
if (!$res = $mysqli->query($sql)) trigger_error( $mysqli->error, E_USER_ERROR );

Open in new window


The Tools for Testing
At this point it would be a good idea to create a test bed for our new scripts and our new data base table.  We will want two scripts to test with.  One will be completely access-controlled.  The other will be a public page that simply tests for access control and uses the response to determine what kind of output to create.  These two scripts meet those requirements.
<?php // RAY_EE_controlled.php
require_once('RAY_EE_config.php');

// ACCESS TO THIS PAGE IS CONTROLLED
$uid = access_control();

echo "<br/>HELLO $uid AND WELCOME TO THE ACCESS CONTROLLED PAGE";

Open in new window


<?php // RAY_EE_public.php
require_once('RAY_EE_config.php');

// ACCESS TO THIS PAGE IS TESTED BUT NOT CONTROLLED
if ($uid = access_control(TRUE))
{
    echo "<br/>HELLO $uid AND WELCOME TO THE PUBLIC PAGE";
}
else
{
    echo "<br/>HELLO STRANGER.";
    echo "<br/>YOU MIGHT WANT TO <a href=\"RAY_EE_register.php\">REGISTER</a> ON THIS SITE";
    echo "<br/>IF YOU ARE ALREADY REGISTERED, YOU CAN <a href=\"RAY_EE_login.php\">LOG IN HERE</a>";
}

Open in new window


Convenience for Clients - the Registration Page
If we only have a few users, we could register all our users by hand with phpMyAdmin, but if we have a public-facing web site that would be a lot of work.  Instead we want to create a page so our users can register themselves.  The page below page will do the registration work for us.  Sidebar note: If you want to extend this idea a little bit, the EE article here might be helpful to you.
http://www.experts-exchange.com/Web_Development/Web_Languages-Standards/PHP/A_3939-Registration-and-Email-Confirmation-in-PHP.html

Like all our scripts, this one starts with the PHP command to load our "config" script, require_once('RAY_EE_config.php'), on line 2.  We use the require form of the include() function because our scripts cannot run without our config file.  We use the once form of the include() function because the config file contains function definitions, and any attempt to redefine a PHP function results in a fatal error.  

To start our registration process, we optimistically set the $err variable to NULL (line 5), assuming that there are no errors.  We can check it later to see if we had any errors.   Then we look at the $_POST array to see if we have all the information needed to process the registration.  If we do not have the registration information yet, the script will fall to line 53, where it will present the registration form.  If we have all the registration information from the form we can try to create the client record.

Our first step in creating the client record is to make some effort to sanitize the external input.  We do not do anything beyond the basics here; in real life, there might be much more extensive filtering and testing.  But for this example, we simply protect our data base by escaping the three text fields that we received from the form inputs.  

Our first important test is to see if the two password fields match.  We do this because the client is typing into a form input field of type="password" and the browser will obscure the input as it is typed.  So to be nice to our client, we ask her to type the password twice and we check for a match.  If she typed her password twice the same way, we can be fairly sure that a typographical error will not cause a bogus password to be assigned to her account.   If there is a mismatch, we append an error message to the $err variable.

Next we check the data base to see if the UID has already been taken.  If this is the case, we will consider it to be an error, and we will set the error indicator in the same way we set the error indicator for a password mismatch - by adding the error message to the end of the $err variable.  (You might consider marking the uid column in the MySQL table with UNIQUE.  MySQL will throw error #1062 if an attempt is made to put duplicate information into a UNIQUE column).

Once our edits are complete (line 25) we test the $err value.  Since a NULL string will return FALSE when tested with the PHP if() statement, we can simply check to see if $err remains empty. We use the PHP "not" expression, the exclamation point.  If we had errors, this test will fail and the code will fall to line 48 where we show the error message(s) and present the registration form again.  If there are no errors that prevent registration, we can continue our processing on line 28.  

We use the combination of the user id, password and a random number to create a completely unique and unpredictable value.  This is the "unique user key" that we can use in cookies.  Why not just use the auto_increment key from the data base?  Because a predictable value in the cookie would invite easy hacking.  If a hacker saw that his cookie value was, for example, "123" he might be tempted to try changing his cookie to "122" and see what happens.  When we use the md5() hash of the unique data combination we make it somewhat harder to guess what value might work in a hacked cookie.  Our query puts the user id, password, and unique user key into the table and our registration is complete.  

Our client can log in now.  But why make them log in separately after they register?  With a single line of code (line 33) we can complete their login at the time of registration.  We always want to be nice to our clients!

Next we turn our attention to the question of whether the client wants us to remember the logged-in status.  If the "rme" checkbox was checked, it will be set in the $POST array, and we can test it to see if we need to remember the client.  (Unlike empty input fields of type="text", type="checkbox" fields that are not checked do not appear in the $_POST array at all).  So if "rme" is there, we call the remember_me() function and pass it the unique user key.  If not, we skip this step and no cookie will be set.  With our registration and login work now completed, we welcome the client on line 42 and conclude the script on line 44.  Here is the registration script:
<?php // RAY_EE_register.php
require_once('RAY_EE_config.php');

// WE ASSUME NO ERRORS OCCURRED
$err = NULL;

// WAS EVERYTHING WE NEED POSTED TO THIS SCRIPT?
if ( (!empty($_POST["uid"])) && (!empty($_POST["pwd"])) && (!empty($_POST["vwd"])) )
{
    // YES, WE HAVE THE POSTED DATA. ESCAPE IT FOR USE IN A QUERY
    $uid = $mysqli->real_escape_string($_POST["uid"]);
    $pwd = $mysqli->real_escape_string($_POST["pwd"]);
    $vwd = $mysqli->real_escape_string($_POST["vwd"]);

    // DO THE PASSWORDS MATCH?
    if ($pwd != $vwd) $err .= "<br/>FAIL: CHOOSE AND VERIFY PASSWORDS DO NOT MATCH";

    // DOES THE UID ALREADY EXIST?
    $sql = "SELECT uid FROM EE_userTable WHERE uid = '$uid' LIMIT 1";
    if (!$res = $mysqli->query($sql)) trigger_error( $mysqli->error, E_USER_ERROR );
    $num = $res->num_rows;
    if ($num) $err .= "<br/>FAIL: UID $uid IS ALREADY TAKEN.  CHOOSE ANOTHER";

    // IF THERE WERE NO ERRORS THAT PREVENT REGISTRATION
    if (!$err)
    {
        // MAKE THE UNIQUE USER KEY
        $uuk = md5($uid . $pwd . rand());
        $sql = "INSERT INTO EE_userTable (uid, pwd, uuk) VALUES ('$uid', '$pwd', '$uuk')";
        if (!$res = $mysqli->query($sql)) trigger_error( $mysqli->error, E_USER_ERROR );

        // STORE THE USER-ID IN THE SESSION ARRAY
        $_SESSION["uid"] = $uid;

        // IS THE "REMEMBER ME" CHECKBOX SET?
        if (isset($_POST["rme"]))
        {
            remember_me($uuk);
        }

        // REGISTRATION AND LOGIN COMPLETE
        echo "<br/>WELCOME $uid. REGISTRATION COMPLETE.  YOU ARE LOGGED IN.";
        echo "<br/>CLICK <a href=\"/\">HERE</a> TO GO TO THE HOME PAGE";
        die();
    }

    // IF THERE WERE ERRORS
    else
    {
        echo $err;
        echo "<br/>SORRY, REGISTRATION FAILED";
    }
} // END OF FORM PROCESSING - PUT UP THE FORM
?>
<form method="post">
PLEASE REGISTER
<br/>CHOOSE USERNAME: <input name="uid" />
<br/>CHOOSE PASSWORD: <input name="pwd" type="password" />
<br/>VERIFY PASSWORD: <input name="vwd" type="password" />
<br/><input type="checkbox" name="rme" />KEEP ME LOGGED IN (DO NOT CHECK THIS ON A PUBLIC COMPUTER)
<br/><input type="submit" value="REGISTER" />
</form>

Open in new window


Client Authentication - the Login Page
Now that we can register our users, and we have test pages that let us see the registrations in action, we need to create login and logout pages.  

The login page uses a structure similar to the registration page.  We require our "config" page, then we test to see if the necessary credentials have been provided (line 5).  We filter and sanitize the external inputs (line 8-9), then we query the data base, looking for exactly one match on the UID and PWD fields (lines 12-20).  If we do not find a row that matches on both UID and PWD, the if() statement on line 20 will fail and the script will fall to line 46, where we can tell the client that authentication failed, and we can present the login form again.  If we did find the one row we were looking for, we retrieve the row (line 23) and copy the UID value into the session array (line 26) to show that the client is now logged in.  Our next step is to see if the client checked the "remember me" box.  We test for that checkbox (line 29) and if it is set, we call the remember_me() function, passing the unique user key that we created at the time of registration.  The remember_me() function sets a long-life cookie on the browser.  Without this cookie, only the session cookie will be used to remember the client.  And session cookies expire when the browser window is closed.  

Our final step in login processing is to determine where the client wants to go next.  We do this by testing the "entry_uri" in the session array.  If it was set by the access_control() function in the config script, we can use that address in the header() command to take the client back to the original page.  If it was not set, which would occur if the client came directly to the login page, we can redirect to the home page instead.

Perhaps it should go without saying, but I'll say it anyway: do not put the access_control() function in your login script, or else your code may cause a loop on the server!  Here is the login script:
<?php // RAY_EE_login.php
require_once('RAY_EE_config.php');

// WAS EVERYTHING WE NEED POSTED TO THIS SCRIPT?
if ( (!empty($_POST["uid"])) && (!empty($_POST["pwd"])) )
{
    // YES, WE HAVE THE POSTED DATA. ESCAPE IT FOR USE IN A QUERY
    $uid = $mysqli->real_escape_string($_POST["uid"]);
    $pwd = $mysqli->real_escape_string($_POST["pwd"]);

    // CONSTRUCT AND EXECUTE THE QUERY - COUNT THE NUMBER OF ROWS RETURNED
    $sql = "SELECT uid, uuk FROM EE_userTable WHERE uid = '$uid' AND pwd = '$pwd' LIMIT 1";
    $res = $mysqli->query($sql);

    // IF THE QUERY FAILED, GIVE UP
    if (!$res) trigger_error( $mysqli->error, E_USER_ERROR );

    // THERE SHOULD BE ONE ROW IF THE VALIDATION WAS PROCESSED SUCCESSFULLY
    $num = $res->num_rows;
    if ($num)
    {
        // RETRIEVE THE ROW FROM THE QUERY RESULTS SET
        $row = $res->fetch_assoc();

        // STORE THE USER-ID IN THE SESSION ARRAY
        $_SESSION["uid"] = $row["uid"];

        // IS THE "REMEMBER ME" CHECKBOX SET?
        if (isset($_POST["rme"]))
        {
            remember_me($row["uuk"]);
        }

        // REDIRECT TO THE ENTRY PAGE OR TO THE HOME PAGE
        if (isset($_SESSION["entry_uri"]))
        {
            header("Location: {$_SESSION["entry_uri"]}");
            exit;
        }
        else
        {
            header("Location: /");
            exit;
        }
    } // END OF SUCCESSFUL VALIDATION
    else
    {
        echo "SORRY, VALIDATION FAILED USING $uid AND $pwd \n";
    }
} // END OF FORM PROCESSING - PUT UP THE LOGIN FORM
?>
<form method="post">
PLEASE LOG IN
<br/>UID: <input name="uid" />
<br/>PWD: <input name="pwd" type="password" />
<br/><input type="checkbox" name="rme" />KEEP ME LOGGED IN (DO NOT CHECK THIS ON A PUBLIC COMPUTER)
<br/><input type="submit" value="LOGIN" />
</form>

Open in new window


Client Un-Authentication - the Logout Page
If our client logs in, and does not check the "remember me" box, he will be automatically logged out when his browser window closes, or when the session garbage collection routines detect a long period of inactivity (usually about 24 minutes).  But our client may want to deliberately log out sooner, or may be at a public computer where a logout is a prudent thing to do.  So we must also provide a logout script, shown below.  

As usual, our first step is to load our "config" script.  Next we collect the UID from the session array, or set an alternative value.  We are going to use this in the "goodbye" message, so we try to choose a data string that makes sense even if the client goes to the logout script twice in a row, or somehow manages to go there when she is not logged in.  That's done in the ternary operator statement (line 5).  

We dispose of the "remember me" cookie, if any (lines 7-12).  

Our next step is to clear the session array (line 15).  That may appear a little ham-fisted, and you might consider whether complete elimination of the session makes sense.  If you have other information in the session that you want to keep even after the client logs out, you might just unset($_SESSION["uid"]) on line 15 and skip the rest of the code.  However you should never use unset($_SESSION) to clear the array.  See the cautionary note here:
http://php.net/manual/en/session.examples.basic.php

Finally, you can say "goodbye" using the data string we created on line 5.  Or you can eliminate the browser output and instead activate the header("location: /") command sequence to redirect to the home page.  

Take a look at the "exit;" statement on the last line.  Although it is not strictly needed here, the use of "exit" after a header("Location") statement is a good habit to cultivate.  Why?  Because your script keeps right on running after it has sent the header() and will run for an unpredictable period of time -- until the browser receives the header and stops your script by redirecting.  There are lots of appropriate header() statements that may be used inline as part of a complete script, but when you use one that is intended to be the last statement in a script, you need to take the additional step of ensuring that it is, in fact, the last statement that gets executed.  Here is the logout script:
<?php // RAY_EE_logout.php
require_once('RAY_EE_config.php');

// GRAB THE UID OR A CONSTANT FOR THE GOODBYE MESSAGE
$uid = (isset($_SESSION["uid"])) ? ', ' . $_SESSION["uid"] : ' NOW';

// IF THE "REMEMBER ME" COOKIE IS SET, FORCE IT TO EXPIRE
$cookie_expires	= time() - date('Z') - REMEMBER;
if (isset($_COOKIE["uuk"]))
{
   setcookie("uuk", '', $cookie_expires, '/');
}

// CLEAR THE INFORMATION FROM THE $_SESSION ARRAY
$_SESSION = array();

// IF THE SESSION IS KEPT IN COOKIE, FORCE SESSION COOKIE TO EXPIRE
if (isset($_COOKIE[session_name()]))
{
   setcookie(session_name(), '', $cookie_expires, '/');
}

// TELL PHP TO ELIMINATE THE SESSION
session_destroy();
session_write_close();

// SAY GOODBYE...
echo "YOU ARE LOGGED OUT$uid.  GOODBYE.";

// OR REMOVE THE GOODBYE MESSAGE AND ACTIVATE THESE LINES TO REDIRECT TO THE HOME PAGE
// header("Location: /");
// exit;

Open in new window


Client Security - the Password Page
We all know that we should change our passwords from time to time.  And it is very easy to give our clients this capability.  Similar in design to the registration page, the password page takes in a pair of new passwords, checks for a match and updates the data base to store the new password.  But there are a few important differences.

Whenever we change something in the data model, we need to re-verify our client's password.  Why?  Because we are all human and we might accidentally leave our computer logged in.  A sneaky person might try to change our password (or other information) while we were not looking.  To reduce this risk, we use a design pattern that is similar to one used at modern ATM consoles.  Before each withdrawal of funds, you must re-enter your PIN.  It is a small inconvenience, and a powerful security measure.  So we will do the same thing in our change-the-password script.  We would use this design pattern whenever we change something important in the client records, like the email address or shipping address.

Our first step is to load the "config" script, and call the access control function to retrieve the client uid.  We assume that no errors have occurred (line 8).  We require three pieces of information from the client - the old password and the new password, typed twice to be sure the new password is typed correctly.  If we have all three of these (line 11) we can begin to process the request.

We sanitize the values for use in a query (line 14-17).  Next we test to see if the two password fields match.  We do this because the client is typing into a form input field of type="password" and the browser will obscure the input as it is typed.  So to be nice to our client, we ask her to type the password twice and we check for a match (line 20).  If there is a mismatch, we append an error message to the $err variable.

Next we check the data base to be sure that the UID record exists in combination with the old password.  There should be exactly one such row in the user table.  If this is not the case, we will consider it to be an error, and we will set the error indicator (line 26) in the same way we set the error indicator for a password mismatch - by adding the error message to the end of the $err variable.

Once our edits are complete (line 29) we test the $err value.  If we had errors, this test will fail and the code will fall to line 42 where we show the error message(s) and present the password form again.  If there are no errors that prevent the password change, we can continue our processing on line 32.  

Notice the WHERE clause in the UPDATE query on line 32 - it is exactly the same WHERE clause that we used in the SELECT query on line 23.  We already know that there is one and only one row that satisfies this WHERE clause.  Thus we are sure that the correct row will be updated with the new password.  As a nod to MySQL performance we tell the data base that there is a LIMIT of one row.  This avoids a table scan.  MySQL will stop, knowing its work is complete, as soon as it has updated the matching row.

The password change is finished and we give the client a success message (line 36).  If your real-world implementation had the client's email address in the user table (and it almost certainly would) you might extend this to send her an email message about the password change.  Good security practices would preclude you from sending the actual password.
<?php // RAY_EE_password.php
require_once('RAY_EE_config.php');

// ACCESS TO THIS PAGE IS CONTROLLED
$uid = access_control();

// WE ASSUME NO ERRORS OCCURRED
$err = NULL;

// WAS EVERYTHING WE NEED POSTED TO THIS SCRIPT?
if ( (!empty($_POST["old"])) && (!empty($_POST["pwd"])) && (!empty($_POST["vwd"])) )
{
    // YES, WE HAVE THE NEEDED DATA. ESCAPE IT FOR USE IN A QUERY
    $uid = $mysqli->real_escape_string($uid);
    $old = $mysqli->real_escape_string($_POST["old"]);
    $pwd = $mysqli->real_escape_string($_POST["pwd"]);
    $vwd = $mysqli->real_escape_string($_POST["vwd"]);

    // DO THE PASSWORDS MATCH?
    if ($pwd != $vwd) $err .= "<br/>FAIL: CHOOSE AND VERIFY PASSWORDS DO NOT MATCH";

    // DOES THE UID AND OLD PASSWORD COMBINATION EXIST?
    $sql = "SELECT uid FROM EE_userTable WHERE uid = '$uid' AND pwd = '$old' LIMIT 1";
    if (!$res = $mysqli->query($sql)) trigger_error( $mysqli->error, E_USER_ERROR );
    $num = $res->num_rows;
    if ($num != 1) $err .= "<br/>FAIL: $uid DOES NOT HAVE PASSWORD $old";

    // IF THERE WERE NO ERRORS TO PREVENT THE PASSWORD CHANGE
    if (!$err)
    {
        // UPDATE THE TABLE TO CHANGE THE PASSWORD
        $sql = "UPDATE EE_userTable SET pwd = '$pwd' WHERE uid = '$uid' AND pwd = '$old' LIMIT 1";
        if (!$res = $mysqli->query($sql)) trigger_error( $mysqli->error, E_USER_ERROR );

        // PASSWORD CHANGE IS COMPLETE
        echo "<br/>THANK YOU, $uid. PASSWORD CHANGE IS COMPLETE.";
        echo "<br/>CLICK <a href=\"/\">HERE</a> TO GO TO THE HOME PAGE";
        die();
    }

    // IF THERE WERE ERRORS
    else
    {
        echo $err;
        echo "<br/>SORRY, PASSWORD CHANGE FAILED";
    }
} // END OF FORM PROCESSING - PUT UP THE FORM
?>
<form method="post">
CHANGE YOUR PASSWORD
<br/>FORMER PASSWORD: <input name="old" type="password" />
<br/>CHOOSE PASSWORD: <input name="pwd" type="password" />
<br/>VERIFY PASSWORD: <input name="vwd" type="password" />
<br/><input type="submit" value="CHANGE" />
</form>

Open in new window


Summary - Putting it into Practice
This is everything it takes to password protect your web pages using basic PHP authentication.  Once you have this structure in place, you can present public registration and login pages, and build your site from a combination of public, protected, and partially protected web pages.  Most importantly, you can make the authentication tests with a single line of PHP code.  The scripts use the PHP session to identify the logged-in clients.  The clients can ask your site to remember their status, and you can do them the favor.  They can log out at any time, or can log out automatically after a period of inactivity.   They can protect their account information by changing their passwords whenever they want.

The comments and code here will work correctly in most PHP installations, but they are intended to be teaching examples and are not intended for use "as is" in a production environment - so please feel free to copy and modify them to suit your particular needs.

An Afterword: Understanding PHP Sessions
While these scripts use the PHP session in a way that makes sense, I've found that it is easy to overthink the way PHP sessions work.  They are much easier to use than you might expect!  You might want to read these two articles for a better understanding of the underlying technologies that our PHP client authentication depends upon.  

Article about PHP Sessions.
Article about  HTTP Client/Server Protocols.

An Afterword: Preventing Automated Registrations
Ever wonder how online forums get so many spammy Viagra ads?  The ads are placed by attack 'bot scripts that find the registration form and sign up for an account, then use the account to post unwanted material.  There are two techniques that, when taken together, will reduce the risk of this kind of intrusion.  The first is the use of a CAPTCHA test on the registration form.  The second is the use of a "handshake" which requires an additional step to confirm the registration via instructions in email.  Either of these can be effective alone; taken together they are even more effective.

An Afterword: MySQL and MySQLi  (Spring 2014)
This article was originally written several years ago, when PHP supported the MySQL database extension.  That has since changed, and if you used the older version of this article for guidance you may need to change your scripts.  Fortunately the changes are very easy to accomplish if you choose object-oriented MySQLi.  To learn why PHP is doing away with MySQL support, and what you must do to keep your scripts running, please see this article that teaches how to convert procedural MySQL code into MySQLi or PDO.

An Afterword: PHP session_unregister() (Fall 2014)
Some obsolete code sets contain "Logout" examples that use the session_unregister() function.  PHP deprecated this function many years ago and removed it more recently.  Unfortunately, PHP code examples do not come with expiration dates, so you may encounter obsolete code without any warning labels.  If you have a script that uses session_unregister(), you should replace the function name with unset(), using the same function call arguments.  Going forward, do not use session_unregister(), session_register(), or session_is_registered().  To understand the rest of this article, you may want to refresh your memory of how PHP sessions work.

An Afterword: About Storing Passwords
In practice, you would not store client passwords in clear text.  You can use the PHP md5() function to encode passwords.  MD5 is an abbreviation for "message digest." Please read the critique, here.  I disagree with the notion that md5() hashing is inadequate for most use cases if you understand what you're doing and use md5() correctly.  However many experts favor password hashing.  Or at least encryption.  Here is my thinking on the subject of md5().

1. Let's say you have foolishly stored the client password in clear text.  Advantage: Your scripts can send the client password when the client forgets the password.  Disadvantage: If your DB is compromised, all of the client passwords are exposed.

2. Let's say you have not stored clear-text passwords, but have instead foolishly stored the md5() digest of the password.  It looks something like this...

5f4dcc3b5aa765d61d8327deb882cf99

...which is the md5() for "password."  You can't readily tell that this md5() string matches "password" just by looking at it, but the problem with md5() digests is that they are programmatically idempotent.  No matter how many times you hash "password" you will always get the same md5() string.  If two people choose the same password, the md5() string will be the same.  Hackers know the md5() strings of the most popular passwords, so if your DB is compromised, many of the client passwords are exposed.  How many of the common passwords are know to hackers?  Would you believe millions?  You better believe it!

3. If you let your clients choose their own passwords and one of them chooses "password," it is all the more likely that a hacker can decipher the other common passwords.  A brute-force approach that matches the md5() strings of the popular passwords is computationally trivial.  It would take at most a few seconds to match every dictionary word with its md5() digest.

4. In an effort to make the passwords harder to crack, a sub-industry has sprung up full of all sorts of voodoo and nonsense about cryptography.  You can avoid the nonsense and learn more about this from the OWASP project.  You can use password hashing to make your password safety stronger.

5. Because md5() is idempotent, security demands that something must be done to break the direct links between, eg, "password" and 5f4dcc3b5aa765d61d8327deb882cf99.  That something is a "salt" added to the password during the encoding process.  With a salt string appended to the password, the idempotent relationship requires not only the password, but also the salt.  So if a client chooses "password" the server stores the md5 string of something like "passwordXYZ" and the resulting digest is cceef54fde042f058f571084338e2c40.  Compare these md5 strings and see if you can see a relationship.  I can't.

5f4dcc3b5aa765d61d8327deb882cf99
cceef54fde042f058f571084338e2c40

6. Your salt does not have to be easy to guess.  But it has to be stored somewhere that makes it programmatically available to PHP during the execution of your scripts.  You would want to guard it carefully.  If the salting string(s) and algorithm were compromised, your client's passwords are potentially exposed.  Maybe you want to put this into a PHP script that is stored above the WWW root and brought into the scope of the web root scripts via the include() function.

7. You can salt both ends of the password (salt and pepper).  You can use very long and arbitrary strings for the salt and pepper.  As long as you add the same salt and pepper to whatever the client types into the password box, your md5() algorithm will create the idempotent message digest.  And you can match the password with a simple SQL query.

8. The likelihood of md5() collision (two different input strings matching the same message digest) is about the same as the likelihood that you will meet someone else with your DNA sequence.  This is theoretically possible, but don't bet on it.

9. How sturdy is a salted password?  I will buy anyone a beer who can tell me the original input string that created this md5() digest:  

e0f1299ed629d3c8826e2dd2be4780cf

To facilitate your search for the original input string, here is a link to the explanation of the md5() algorithm.
http://www.faqs.org/rfcs/rfc1321.html

And I have installed this easy-to-use script on my server.  Experiment here:
http://www.iconoun.com/demo/md5.php

10. If you choose good salt and pepper strings and keep them secret, your md5() digest will be adequate to protect client passwords in most cases.  But you can't fix stupid.  If a client chooses "password" and your login process requires only an email address and a password, there isn't much protecting that client from exposure.  It doesn't matter how you encode "password" internally.  Anyone who knows the email addresses in the site can try pairing each one with "password" or the other common passwords to see if the pairing is effective.  No doubt, if your population is big enough, there is somebody in your client community using "password" and she is going to be among the first victims of any attack.

11. For reason 10, some password selection processes require you to use a combination of letters and numbers, upper and lower, etc.  I find these annoying and tend to prefer the idea of a multi-word pass-phrase instead of a single password.  I'm sure there will be popular and common phrases, and you would be wise to choose random words in the phrase, and include some things that are not even words at all.  And a well-salted phrase is at least as good as a well-salted password.  

12. The md5() string is always a 32 character hexadecimal number, no matter what the input string contains.  It follows that data base storage must have a 32-byte column width.

This article from ArsTechnica gives an insider's view of how hackers might go about attacking your encoded passwords.  Ultimately your passwords are like the doors of a fire safe.  They are rated on the basis of time and temperature.  Stronger doors mean you have more time before the contents are incinerated.  Unfortunately, each exercise like the one conducted by ArsTechnica adds to the basic dictionary of passwords and pass-phrases, and the dictionary attacks are the fastest to succeed.

And it matters what you're trying to protect.  If you have bowling scores, or purchase histories, or medical records, or financial details, or nuclear launch codes the security measures are likely to be different.

Further Reading on Security (updated periodically)
PHP Security (required reading for anyone who writes even one line of PHP code)
Password Hashing in PHP (better than message digests or encryption)
SitePoint (the original is old, but updated recently)
The danger of Register Globals
OWASP is worth joining
http://xkcd.com/327/
CodingHorror

Please give us your feedback!
If you found this article helpful, please click the "thumb's up" button below. Doing so lets the E-E community know what is valuable for E-E members and helps provide direction for future articles.  If you have questions or comments, please add them.  Thanks!
 
59
Comment
Author:Ray Paseur
[X]
Welcome to Experts Exchange

Add your voice to the tech community where 5M+ people just like you are talking about what matters.

  • Help others & share knowledge
  • Earn cash & points
  • Learn & ask questions
47 Comments
 
LVL 12

Expert Comment

by:jazzIIIlove
lovely but what if cookies are disabled in client side?
0
 
LVL 29

Expert Comment

by:rdivilbiss
It is perfectly acceptable to have a authentication policy that requires the user to accept cookies.

If there is value on the site for the user they will enable cookies for the site. If there is no value to the user, the user may choose not to register or login to that site.
0
 
LVL 111

Author Comment

by:Ray Paseur
If cookies are disabled, the client can use URL session-id strings.  Information about this (and all kinds of things about PHP) is contained in the online man pages, required reading for PHP developers.
http://us3.php.net/manual/en/session.idpassing.php

In practical experience I find that very few clients disable cookies or Javascript.  Without cookies or Javascript you cannot use Google, Facebook, Twitter, and a lot of other popular sites.  I do not think that clients who disable cookies are a credible audience for authentication web sites, but for those few outliers the PHP session handler has support for the URL strings.
0
Looking for the Wi-Fi vendor that's right for you?

We know how difficult it can be to evaluate Wi-Fi vendors, so we created this helpful Wi-Fi Buyer's Guide to help you find the Wi-Fi vendor that's right for your business! Download the guide and get started on our checklist today!

 
LVL 26

Expert Comment

by:arober11
Your very unlikely to encounter a browser that doesn't support cookies, but it's possible and may be a requirement, in which case you may wish to consider HTTP auth as an alternative scheme, see:

http://www.peej.co.uk/articles/http-auth-with-html-forms.html
OR:
http://www.experts-exchange.com/A_3270.html
0
 
LVL 38

Expert Comment

by:younghv
Articles such as this (and Authors such as Ray_Paseur) are what makes EE stand out for all to see.
Great work and a big "Yes" vote above.
0
 
LVL 14

Expert Comment

by:systan
i Voted Yes
0
 

Expert Comment

by:prileyosborne
Great article, but I was wondering about one segment of it.  In the opening config segment, tehre is a piece of code similar to this (I modified it a bit for my existing database structure)

function remember_me($uuk)
{
    // CONSTRUCT A "REMEMBER ME" COOKIE WITH THE UNIQUE USER KEY
    $cookie_name    = 'uuk';
    $cookie_value   = $uuk;
    $cookie_expires = time() + date('Z') + REMEMBER;
    $cookie_path    = '/';
    $cookie_domain  = NULL;
    $cookie_secure  = FALSE;
    $cookie_http    = TRUE; // HIDE COOKIE FROM JAVASCRIPT (PHP 5.2+)

    // SEE http://us3.php.net/manual/en/function.setcookie.php
    setcookie
    ( $cookie_name
    , $cookie_value
    , $cookie_expires
    , $cookie_path
    , $cookie_domain
    , $cookie_secure
    , $cookie_http
    )
    ;
}

// DETERMINE IF THE CLIENT IS ALREADY LOGGED IN BECAUSE OF THE SESSION ARRAY
if (!isset($_SESSION["user"]))
{
    // DETERMINE IF THE CLIENT IS ALREADY LOGGED IN BECAUSE OF "REMEMBER ME" FEATURE
    if (isset($_COOKIE["uuk"]))
    {
        $uuk = mysql_real_escape_string($_COOKIE["uuk"]);
        $sql = "SELECT user FROM foo_users WHERE user = '$uuk' LIMIT 1";
        $res = mysql_query($sql);

        // IF THE QUERY SUCCEEDED
        if ($res)
        {
            // THERE SHOULD BE ONE ROW
            $num = mysql_num_rows($res);
            if ($num)
            {
                // RETRIEVE THE ROW FROM THE QUERY RESULTS SET
                $row = mysql_fetch_assoc($res);

                // STORE THE USER-ID IN THE SESSION ARRAY
                $_SESSION["user"] = $row["user"];

                // EXTEND THE "REMEMBER ME" COOKIE
                remember_me($uuk);
            }
        }
    }

Open in new window


My question is in the select statement it asks for a value from, int he case of my database, user = '$uuk'.  In our database structure, and there is only a handful of users that will be logging in, we the column 'users' is a username value.  But I don't see where $uuk is set to be the uid in the code.

I know I am totally missing something so any help would be awesome, but as I am customizing this I wanted to know how everything worked. Thanks again!
0
 
LVL 111

Author Comment

by:Ray Paseur
The $uuk variable is created during the client registration process on line 28, where it says something like this.
// MAKE THE UNIQUE USER KEY
        $uuk = md5($uid . $pwd . rand());

Open in new window

 The $uuk variable is a pseudo-nonsense string that is unique for each user of the site.  It is stored in the cookie and in the data base.  When the cookie is retrieved, we get the value of the $uuk variable and we can use it for a lookup in the data base.  It is not the same as a user-id or user-name; it is only there to associate the cookie with the user record.

Does that help?
0
 

Expert Comment

by:prileyosborne
That is perfect!  Thanks so much. everything worked I just didn't know how that bit worked. Thanks again for such a great article!

0
 

Expert Comment

by:LezlyPrime
I found this article, & the code samples, to be more clear & concise than the authenticate chapter of the book I'm working through. I will add security, but this code is beautiful.

Again, Experts Exchange - & it's Experts - shine, as an excellent technical resource.
0
 

Expert Comment

by:eyedropp
I have implemented this series of code to one of my sites, and want to know - for existing clients, how would I go about assigning them the uuk since this is a field I have added since a number of clients already registered?
0
 
LVL 111

Author Comment

by:Ray Paseur
@eyedropp, If your uuk columns are empty for several of your clients., just run a DB query to SELECT the rows with empty columns, then create an md5() string from a concatenation of their email address and the value of time().  Store this in the uuk column and you should be good to go.  Best of luck with it, ~Ray
0
 

Expert Comment

by:eyedropp
Hi Ray,
Could you explain how to write the sql for that.  I've tried to figure it out but I just dont know what I'm doing...
I feel really stupid asking because I know it's not a difficult thing.... I just dont understand this stuff enough yet...
0
 
LVL 111

Author Comment

by:Ray Paseur
@eyedropp: Suggest you post a question in the PHP and the PHP and Databases Zones.  If you can show us your CREATE TABLE statement for your clients table we can show you a good strategy.  Best regards, ~Ray
0
 

Expert Comment

by:eyedropp
Will do. Thanks
0
 
LVL 25

Expert Comment

by:Kyle Hamilton
Hi Ray,

This is awesome.

Thank you.
0
 
LVL 2

Expert Comment

by:pleug
Hello Ray,

Thank's for this great job.

I have just a question for you.

in this code :

$uuk = md5($uid . $pwd . rand());

Open in new window


Are you sure $uuk is really unique ?
no way to create a $uuk alway into the database for another user ?

Best regards,

Pascal
0
 
LVL 111

Author Comment

by:Ray Paseur
If you're worried about whether $uuk is really unique, you can mark the 'uuk' column in the data base table UNIQUE.  As a practical matter, you might want to try this little exercise. Create a data base table with a 'uuk' column that you have marked UNIQUE.  Choose some values for the $uid and $pwd variables.  Run the instruction above to generate $uuk and insert the $uuk variable into the 'uuk' column of the data base table.  If you try to insert a duplicate value into a MySQL UNIQUE column, MySQL will respond with mysql_errno() = 1062.  Count the number of times you can insert the regenerated $uuk variable into the table before you get the 1062 error signal.

If you have any questions about this process, please feel free to post your question in the PHP zone.  There are many knowledgeable experts who monitor that zone, and we are glad to help.  Best regards, ~Ray
0
 
LVL 2

Expert Comment

by:pleug
thank's for your answer, i wanna know the probability to gernerate a duplicated $uuk is very small but i think i prefer to verify it before the insertion not to have a mysql error at any time.

Best regards,

Pascal
0
 
LVL 111

Author Comment

by:Ray Paseur
If you want to post a question about how to handle duplicate key issues in PHP and MySQL, please post your question in the PHP Zone.  I will be glad to show you exactly how it is done.
0
 

Expert Comment

by:hibbsusan
I use this script, but noticed today that when I go to the sign out page, I am still able to press the back button and load the last access_controlled page again. Is this how it works for you, or have I fumbled my implementation somehow?

Thanks for the script!!
0
 

Expert Comment

by:hibbsusan
Though in IE8, it redirects to the log in page as you would expect. In all other browsers I've tested, it allows me to go back to the previous page, but when I hit reload, I'm redirected to the login page..
0
 
LVL 111

Author Comment

by:Ray Paseur
I don't think you've done anything wrong.  Some browsers cache the last page. Apparently IE8 works in an intuitive way.  The other browsers that appear to reload the last page are not actually processing the HTTP request to reload the page -- they are just showing you what they had left in the cache.
0
 

Expert Comment

by:hibbsusan
i hate to trouble you, but would you able to take a look at the code i posted on this question...

http://www.experts-exchange.com/Web_Development/Web_Languages-Standards/PHP/Q_27793440.html

I feel I may have mixed up the uuk variable/cookie/session. Not that this has to do with the problem in the previous comments, I just thought you may be able to help with my preventing butchering your design.

Thanks!
0
 
LVL 111

Author Comment

by:Ray Paseur
No trouble at all -- I'll look at the question soon.  Best, ~Ray
0
 
LVL 35

Expert Comment

by:gr8gonzo
Ray,

Good article, but I'm still of the mind that an auto-incrementing ID is a best practice on most tables. I understand not relying solely on the security of looking up a single value, but using a hash in conjunction with the auto-incrementing ID has worked well for me in the past.

For example, let's say that your user record has an ID (auto-inc), username, password, and md5 hash of md5(ID,username,password).

If your cookie contains an ID (encrypting it can be a further step) and a hash, then the server can quickly look up the ID in the users table (far faster than being able to look up a string value of any kind), and then verify the matching hash as a "token" of sorts. You still achieve security with the additional check while still retaining the usefulness of the integer user ID.

I'd also recommend an additional step of inserting a sleep(1) into the page during login attempts. It's virtually unnoticeable by normal users but will greatly discourage brute force attack attempts (which, even if unsuccessful, still take up resources, so the sooner they end...).
0
 
LVL 111

Author Comment

by:Ray Paseur
Jonathan: Good points, all.  If I ever get so many clients that the lookup time for login becomes a limiting factor, I will be too busy drinking champagne with the IPO lawyers to care ;-)

I also like the idea of inserting sleep() into the page during login attempts.  I use this a lot in my live sites, usually putting it into the process when the login attempt fails.

All the best,
Ray
0
 
LVL 25

Expert Comment

by:Kyle Hamilton
Hi Ray,

In real life we would never store the password in clear text (we would store an abstraction or encrypted version of the password)

how would I go about encrypting the password?

I tried simply doing $pwd = md5($pwd);, which encrypts it, but then how does it get read back in when the user enters their password in the login page? Is there a reverse md5 function? Or am I talking non-sense :O

Thanks,
Koza
0
 
LVL 35

Expert Comment

by:gr8gonzo
Koza,
The purpose of MD5 is that it is one way. Imagine you had a secret code book that said:

A through G = 1
H through M = 2
N through Z = 3

Now, let's take the following password: "PASSWORD" - that would get converted into 31333331. You couldn't convert 31333331 back into "PASSWORD" because a 3 could be anything from N through Z, for example. So anyone who was able to steal the password list wouldn't be able to do anything with it without the codebook.

If someone had the same codebook, then if they put in "PASSWORD", it would ALSO result in 31333331. But if they put in "SECRET" it would result in 311313. Since 311313 does not equal 31333331, the system knows that the original password was incorrect.

So with one-way hashes, you're losing the original value intentionally. This way, if it ever gets compromised or stolen, it is useless to someone, because they won't have the exact code to reproduce the value. However, if someone types the same password in multiple times, every single time it will go through the same steps and arrive at the same final value.

This means, you store the one-way-hashed version of the password in the database. Then when it is time to validate, you simply take their login ATTEMPT, and then try to use it to reproduce the same hashed version. If the attempted version's hash matches the stored hash, then you can safely assume it was the correct original password.
0
 
LVL 25

Expert Comment

by:Kyle Hamilton
thanks. that is what i thought.

But:

I INSERT the password like this:

$pwd = md5($pwd);

then I try to SELECT the password when user is logging in:

$pwd = md5($_POST["pwd"]);

this does not work. I get password incorrect.

What am I doing wrong?
0
 
LVL 25

Expert Comment

by:Kyle Hamilton
Never mind - I see what happened..

my password field in the database was only 16 chars, thus was getting truncated, and not matching.

It's fixed now, and works like a charm.

Thanks!
0
 
LVL 2

Expert Comment

by:shdwmage
Ray, since there isn't really a way I know of to send you a message directly I will post this here.  I want to thank you for condensing all of this information into a format that is quick, easy to read and even easier to understand.

I'm sure you have saved me a lot of hours of work by your thorough methodology above. As thanks, I have made sure to source this article and thank you in comments in the code as well.
0
 
LVL 111

Author Comment

by:Ray Paseur
@shdwmage: Thanks for your kind words!  Glad that Experts-Exchange has been helpful for you, ~Ray
0
 
LVL 30

Expert Comment

by:Olaf Doschke
The script is storing the password in cleartext, and while you have several advice about this in the text accompanying the code, you have no comment in the code section. I think this is where the warnings not to use this code "as is" in production should be in the first place.

To make it very clear, I have read this article in full, but I fear people, who just think they can get a good foundation of this from the PHP expert and one of the top EE expert are not warned in the code itself. You may argue as much as you like, such users are self responsible for any harm, I'd at least ask you to add a strong comment within the code at the place you store the password as is in clear text, too. Not only in the accompanying text.

Again said: I have read your warnings, I know you know better and you point to a very important resource and authority about security questions:OWASP.

Still let me summarize a bit to stress this out: Let's begin with the essential code lines, I want to talk about.

First part, the registration form html:
<br/>CHOOSE USERNAME: <input name="uid" />
<br/>CHOOSE PASSWORD: <input name="pwd" type="password" />
<br/>VERIFY PASSWORD: <input name="vwd" type="password" />

Open in new window


Second part, the creation of the user record:
$uid = $mysqli->real_escape_string($_POST["uid"]);
$pwd = $mysqli->real_escape_string($_POST["pwd"]);
$vwd = $mysqli->real_escape_string($_POST["vwd"]);
...
$sql= "INSERT INTO EE_userTable (uid, pwd, uuk) VALUES ('$uid', '$pwd', '$uuk')";
if (!$res = $mysqli->query($sql)) trigger_error( $mysqli->error, E_USER_ERROR );

Open in new window

I skipped a few lines here, testing the $pwd and $vwd match (ensuring the user really means this password) and the check, whether the username already is taken. That in itself is fine, but you then store the $pwd as is in clear text form. At this point let's continue with quoting what you say about this, and I only take two things out:

Quote1:
In real life we would never store the password in clear text; we would store an abstraction or encrypted version of the password. (Please see the Afterword at the end of this article).  But for this example, it makes the design easier to understand if we process the password in clear text.
Quote2 (from the mentioned afterword):
In practice, you would not store client passwords in clear text...
Followed up by an externsive explanation in 12 bullet points on how to really do this. That's fine, but what I am missing is just one little thing: a comment hinting on this within the CODE section above. If not also a slightest simple hashing of the password even though it may outdate, too.

Why am I so concerend about this and stressing this out now after this article is over 4 years old? I recently was involved in a thread where you referenced this article as answer, and it is a comprehensive answer, but I fear people do not read it thoroughly and just pick out the code sections to use them. Even, if they only use it for the login to their local sports club or something else with a small user base, such things could become a key to other accounts and let someone gain control about more important things. You can always argue people are dumb, when reusing passwords everywhere, but they are lazy and the best thing developers can do is ensuring a secret isn't revealed. Not even in the smallest community sites. And the best way to do so will change in time, which makes it a mute point to tell about this. The OWASP project is one place to look at.

The term "password" was used 125 times on this page including comments, which stresses out the importance of this type of key, though today we have better ways to protect with hardware like smartcards, but passwords are still the most used authentication principle, no matter what mechanisms are used on them on site. I now added a few more occurrences of this word. I hope you don't take this as affront. I can live with the response, you are fed up with such critic, but it can't be stressed out, that developers have responsibility about the security of our users.

The strongest lock on a cardbord box isn't worth it, eg even a strongly hashed spiced password can be copied into any users record, when a hacker is at the point of being able to read tables and at that point he also can change data without knowing any password anyway, but the main argument is about the reusability of harvested passwords to crack into further accounts of more importance. You can blame users to be foolish about password reusage, but you don't need to blame yourself to store them in clear. I hope anybody having used this code now also has a motivation to revisit it and change it.

I didn't address the remember me feature, but I add a reference here to https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence#title.2 and as the most important advice also encourage anyone to add www.owasp.org to your favorites and update on this topic regularly.

With kind regards, Olaf.
0
 
LVL 111

Author Comment

by:Ray Paseur
Greetings, Olaf.  I completely agree with everything you said in your comment.  References to OWASP have been a part of this article since its publication, several years ago.  Regarding this:
I fear people do not read it thoroughly...
Well, computer programming is the sort of activity that requires meticulous attention to every sort of minute detail, and if someone does not read the article thoroughly, I simply cannot help them.  You can't teach "everything" in one article.  Here are some of the things that are not addressed in this article, but are important to understand if you're going to write a client-authentication script.
 
You should never transmit passwords over HTTP.
You should use tamper-resistant cookies.
Passphrases are better than passwords.
Facebook and other social APIs can handle authentication.
A CAPTCHA test on a login page is a good idea.
Two-factor authentication is a good idea.
You need the ability to suspend a user's access.
You should check the USER_AGENT and prompt for a password if it changes.
I could go on and on, but hopefully this makes the point. Technology is always advancing and simple design patterns (small enough to fit in one article) can only show part of the picture.  This article shows the design of PHP code for a simple client authentication model.  You can install it and run it to see the moving parts, and once you understand it, you can customize it to suit your own requirements.

I hope these bullet points inspire you to write supplementary articles showing the E-E community how to improve on my basic client authentication scripts.
1
 
LVL 30

Expert Comment

by:Olaf Doschke
Valid points about the overall security topic, but no article needs to cover all topics at once. I disagree still, though, as the storing of the password in the right way is a core point of this specific topic.

Two further suggestions you could do:
1. change the insert statement to this:
$sql= "INSERT INTO EE_userTable (uid, pwd, uuk) VALUES ('$uid', <<your way of storing passwords safely here>>, '$uuk')";

Open in new window

Users would need to tackle that part with current practices then.
2. simply call an empty hook function here like
$sql= "INSERT INTO EE_userTable (uid, pwd, uuk) VALUES ('$uid', passwordprocessing($pwd), '$uuk')";

Open in new window

Users of the code again will be brought to thinking here.

Bye, Olaf.
0
 
LVL 35

Expert Comment

by:gr8gonzo
@Olaf - I'd agree with Ray that if someone doesn't read thoroughly, they're going to have a variety of problems. I once had a developer who was easily distracted and he would "tune out" randomly during training sessions. Every other developer would catch the important parts of the training (which I could see from their resulting code during code reviews), but this guy kept coming back to me later on and asking questions that had already been answered.

While it was good that he asked the questions, there are plenty of people who won't and will end up screwing up their code because they've "tuned out" while reading something that they chose to start reading. Nobody can fully control the reader. If you add in a "passwordprocessing" hook, the reader might not read the explanation of it, and when they try to run the code and it keeps failing because they don't have that function defined, then they might assume it's just a bad or obsolete explanation and they'll try to find something else that works "out of the box."

You also mention copying passwords across to other records. If you use part of the username as the salt (or some other record-specific salt), then this can help avoid that problem. So if a hacker is able to copy HIS password to the administrator's record, then HIS password wouldn't work because the system would be using the salt from the admin's record.

At a certain point, some of the more advanced security concepts might be lost on new readers.
0
 
LVL 1

Expert Comment

by:Braveheartli
Dear Ray,
do you have a download link for the PHP Client Registration, Login, Logout and Easy Access Control php files?
0
 
LVL 111

Author Comment

by:Ray Paseur
@Braveheartli:  Sorry, I don't.  Each code snippet has "Select all" and "Open in new window" links.  I believe you would need to copy the code snippets one-at-a-time.
0
 
LVL 1

Expert Comment

by:Braveheartli
I did it, thank you
0
 
LVL 1

Expert Comment

by:Braveheartli
thank you for the update.
I used to use the old one, now I use the new version. Thank you Ray Paseur
1
 
LVL 1

Expert Comment

by:mlemos
This is nice. More recently an article was published about PHP secure login and registration practices that make sites more secure. In this case it uses PDO but it could be mysqli based prepared statements. It also uses password_hash to store password hashes more securely,  as well provides a mechanlsm to limit the number of failed login attempts to avoid brute force attacks.
0
 
LVL 111

Author Comment

by:Ray Paseur
Yes, technology is always advancing.  You might want to publish the article on Experts-Exchange so it can become part of the dialog here!
1
 
LVL 1

Expert Comment

by:mlemos
Good idea @Ray. How do I do that?
0
 
LVL 17

Expert Comment

by:Kyle Santos
Hi mlemos,

In the navigation menu, click Contribute > Write an Article.

eg
Screenshot_1.png
We look forward to seeing an article. :)
1
 
LVL 1

Expert Comment

by:mlemos
Thanks Kyle, would it be OK to republish articles published elsewhere?
0
 
LVL 17

Expert Comment

by:Kyle Santos
Yes, that would be fine.  Just make sure you reference the original source so our Page Editors are aware.  This will let them know the content is not plagiarized.
1

Featured Post

Concerto's Cloud Advisory Services

Want to avoid the missteps to gaining all the benefits of the cloud? Learn more about the different assessment options from our Cloud Advisory team.

Join & Write a Comment

Sometimes it takes a new vantage point, apart from our everyday security practices, to truly see our Active Directory (AD) vulnerabilities. We get used to implementing the same techniques and checking the same areas for a breach. This pattern can re…
Is your data getting by on basic protection measures? In today’s climate of debilitating malware and ransomware—like WannaCry—that may not be enough. You need to establish more than basics, like a recovery plan that protects both data and endpoints.…

Keep in touch with Experts Exchange

Tech news and trends delivered to your inbox every month