- 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:
Furthermore, we can test for a client login (without actually requiring a login) with this:
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:
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/
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()
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.
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.
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-exchang
Like all our scripts, this one starts with the PHP command to load our "config" script, require_once('RAY_EE_confi
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:
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:
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/s
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:
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.
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.
by: jazzIIIlove on 2010-07-25 at 19:17:31ID: 17457
lovely but what if cookies are disabled in client side?