PHP Client Registration, Login, Logout and Easy Access Control

AID: 2391
  • Status: Published

49196 points

  • ByRay_Paseur
  • TypeBest Practices
  • Posted on2010-02-03 at 18:05:23
Awards
  • Community Pick
  • Experts Exchange Approved

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();
                                  
1:

Select allOpen 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 */ }
                                  
1:

Select allOpen in new window



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
                                  
1:
2:
3:

Select allOpen 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-38).

Have a look at the access_control() function that is defined on line 40.  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 57.  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://us3.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 85.  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 mysql_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 105) and the client is now logged in.  Our last step is to call the remember_me() function on line 108, 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
// MAN PAGE: http://us2.php.net/manual/en/function.mysql-connect.php
if (!$db_connection = mysql_connect("$db_host", "$db_user", "$db_word"))
{
    $errmsg = mysql_errno() . ' ' . mysql_error();
    echo "<br/>NO DB CONNECTION: ";
    echo "<br/> $errmsg <br/>";
}

// SELECT THE MYSQL DATA BASE
// MAN PAGE: http://us2.php.net/manual/en/function.mysql-select-db.php
if (!$db_sel = mysql_select_db($db_name, $db_connection))
{
    $errmsg = mysql_errno() . ' ' . mysql_error();
    echo "<br/>NO DB SELECTION: ";
    echo "<br/> $errmsg <br/>";
    die("NO DATA BASE $db_name");
}

// 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://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["uid"]))
{

    // 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 uid FROM EE_userTable WHERE uuk = '$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["uid"] = $row["uid"];

                // EXTEND THE "REMEMBER ME" COOKIE
                remember_me($uuk);
            }
        }
    }
}
                                  
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:

Select allOpen 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).  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
// mysql_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= mysql_query($sql)) die( mysql_error() );
                                  
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:

Select allOpen 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";
                                  
1:
2:
3:
4:
5:
6:
7:

Select allOpen 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>";
}
                                  
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:

Select allOpen 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.

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 = mysql_real_escape_string($_POST["uid"]);
    $pwd = mysql_real_escape_string($_POST["pwd"]);
    $vwd = mysql_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= mysql_query($sql)) die( mysql_error() );
    $num = mysql_num_rows($res);
    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 = mysql_query($sql)) die( mysql_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>
                                  
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:

Select allOpen 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 = mysql_real_escape_string($_POST["uid"]);
    $pwd = mysql_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 = mysql_query($sql);

    // IF THE QUERY FAILED, GIVE UP
    if (!$res) die( mysql_error() );

    // THERE SHOULD BE ONE ROW IF THE VALIDATION WAS PROCESSED SUCCESSFULLY
    $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["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>
                                  
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:

Select allOpen 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 code in lines 17-24.  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 on line 27 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.  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();

// 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;
                                  
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:

Select allOpen 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.

The first step is to 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.

<?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 = mysql_real_escape_string($uid);
    $old = mysql_real_escape_string($_POST["old"]);
    $pwd = mysql_real_escape_string($_POST["pwd"]);
    $vwd = mysql_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= mysql_query($sql)) die( mysql_error() );
    $num = mysql_num_rows($res);
    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 = mysql_query($sql)) die( mysql_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>
                                  
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:

Select allOpen 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.

Asked On
2010-02-03 at 18:05:23ID2391
Tags

login

,

client validation

,

session

,

cookie

,

password

Topic

PHP Scripting Language

Views
7489

Comments

Expert Comment

by: jazzIIIlove on 2010-07-25 at 19:17:31ID: 17457

lovely but what if cookies are disabled in client side?

Expert Comment

by: rdivilbiss on 2010-07-26 at 11:44:43ID: 17500

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.

Author Comment

by: Ray_Paseur on 2010-07-26 at 13:42:13ID: 17503

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.

Expert Comment

by: arober11 on 2010-08-04 at 09:48:28ID: 17803

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

Expert Comment

by: younghv on 2010-11-09 at 03:58:24ID: 21193

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.

Expert Comment

by: systan on 2011-06-04 at 11:56:03ID: 27991

i Voted Yes

Expert Comment

by: prileyosborne on 2011-09-14 at 15:35:17ID: 31577

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);
            }
        }
    }
                                      
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:

Select allOpen 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!

Author Comment

by: Ray_Paseur on 2011-09-14 at 19:13:01ID: 31580

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());
                                      
1:
2:

Select allOpen 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?

Expert Comment

by: prileyosborne on 2011-09-15 at 11:30:56ID: 31605

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

Expert Comment

by: LezlyPrime on 2012-01-18 at 12:12:33ID: 34596

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.

Expert Comment

by: eyedropp on 2012-01-30 at 19:33:32ID: 34895

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?

Author Comment

by: Ray_Paseur on 2012-02-01 at 13:51:42ID: 41521

@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

Expert Comment

by: eyedropp on 2012-02-07 at 11:32:32ID: 42222

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...

Author Comment

by: Ray_Paseur on 2012-02-07 at 11:37:18ID: 42224

@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

Expert Comment

by: eyedropp on 2012-02-07 at 11:42:52ID: 42225

Will do. Thanks

Add your Comment

Please Sign up or Log in to comment on this article.

Loading Advertisement...

Top PHP Experts

  1. Ray_Paseur

    326,882

    Wizard

    4,070 points yesterday

    Profile
    Rank: Savant
  2. Roads_Roads

    77,834

    Master

    0 points yesterday

    Profile
    Rank: Genius
  3. maeltar

    71,332

    Master

    0 points yesterday

    Profile
    Rank: Guru
  4. StingRaY

    70,054

    Master

    0 points yesterday

    Profile
    Rank: Wizard
  5. DaveBaldwin

    64,155

    Master

    664 points yesterday

    Profile
    Rank: Genius
  6. jason1178

    37,050

    0 points yesterday

    Profile
    Rank: Genius
  7. COBOLdinosaur

    30,996

    664 points yesterday

    Profile
    Rank: Genius
  8. xterm

    28,850

    0 points yesterday

    Profile
    Rank: Sage
  9. eriksmtka

    27,641

    0 points yesterday

    Profile
    Rank: Master
  10. smadeira

    26,150

    0 points yesterday

    Profile
    Rank: Guru
  11. webmatrixpune

    23,436

    0 points yesterday

    Profile
    Rank: Guru
  12. logudotcom

    19,598

    10 points yesterday

    Profile
    Rank: Genius
  13. bportlock

    17,470

    0 points yesterday

    Profile
    Rank: Genius
  14. Derokorian

    17,368

    0 points yesterday

    Profile
    Rank: Guru
  15. maestropsm

    16,698

    0 points yesterday

    Profile
    Rank: Master
  16. leakim971

    16,600

    0 points yesterday

    Profile
    Rank: Genius
  17. alex_code

    16,402

    0 points yesterday

    Profile
    Rank: Guru
  18. mwvisa1

    14,400

    0 points yesterday

    Profile
    Rank: Genius
  19. hernst42

    14,332

    0 points yesterday

    Profile
    Rank: Genius
  20. pratima_mcs

    14,200

    0 points yesterday

    Profile
    Rank: Genius
  21. Slick812

    13,900

    0 points yesterday

    Profile
    Rank: Sage
  22. elvin66

    12,628

    0 points yesterday

    Profile
    Rank: Wizard
  23. zappafan2k2

    12,200

    0 points yesterday

    Profile
    Rank: Guru
  24. TerryAtOpus

    11,600

    0 points yesterday

    Profile
    Rank: Genius
  25. amar_bardoliwala

    11,500

    0 points yesterday

    Profile
    Rank: Master

Hall Of Fame