Handling Users and Passwords
J. Scott Johnson on 2002 April 30
    

One of the very first real parts of an application that every PHP beginner hits is how to handle users and passwords. Think about it. Let's say that you want to write any of the following

:



    A FAQ engine
    An email tool
    A database viewer
    Etc.



Any of these applications are going to require one or more users along with their passwords. If you write it properly handling users and passwords can be built once and used over and over again in every application you build. This is what I've done and what I'm going to document here. I built one user / password system and I just move the code from application. No, I didn't use PHP's "objects" or "classes" -- it's just plain old PHP code and a few database tables.

NOTE: There is nothing wrong at all with PHP's objects or classes. They're just a little harder for beginners and not always necessary.

Overview

Before we hit PHP code or SQL code or table structures, etc, we should start with figuring out what we want to do. This always helps clarify any development. Here are our goals:

Allow users to sign up for our application over the web (if you are building an internal application then all you do is "pre-sign them up" and not display the sign up form)
Store the following information for every user:



    username
    password
    email address



Set a cookie to store the user's login information so they don't have to continually re-enter it.
Allow the user to login or log out as they want to

This is the essence of any web user registration and login module. Whether it's EBAY, Yahoo Mail or Amazon, it's all pretty much the same.


Why Cookies, Not Sessions?

You probably noticed above that I specified that we're going to use cookies, not sessions. There are a bunch of reasons:

1. Cookies are something that every single web developer needs to learn. This is as good a place as any to learn them.

2. Sessions aren't as easy to use as cookies. They are more powerful but not as simple.

3. This is a very standard login approach and it lets user logins persist across browser closings, etc. For applications that need some security but not a lot, this is very, very desirable.

NOTE: We're actually not going to store the password itself. This is covered below under "Why You Don't Want to Store Passwords in Clear Text". If this doesn't make sense right now, don't worry. We're going to make it clear.

NOTE: Due to the length of this article, we're only going to set the cookies but not get into the details of how cookies are used for security checks. We'll cover that in a future article.



User Database Structure

As with most PHP applications, we're going to use a database to handle our storage behind the scenes. And, once again, we're going to turn to our old friend MySQL. If we think about it, we need the following structure:

TABLE: users



    user_id
    username
    password



These are the only "required" fields. We need to store a user's username and their password. The user_id field adds the required "unique key" to our database. Still, are these the only fields that we need?

We actually need a few more fields if we want this database structure into the future. If you've ever seen those features "Hint Question" and "Hint Answer" on other websites that lets user's answer a question and be given a password automatically when they forget it, we're going to need that as well. We won't wrote all the code for that now (that's for a future article if there is interest), but we should build the table fields now so that we have the structure for when we get around to it.

Are hint question and hint answer the only additional fields we need? Unfortunately, we also need some other fields. Here's how I'd expand the table structure above:

TABLE: users



    user_id
    username
    password
    hintquestion
    hintanswer
    email
    ipaddress
    date
    type




As you can see, I've colored the additional fields in blue. Let's talk about them a bit:

email -- pretty understandable here. You may not use or want it immediately but pretty soon you will. It is smart to capture it right from the start but, of course, not required.

ipaddress -- A very, very useful tool for you is the user's ip address. What this lets you do is watch for hacker type type activity. It's particularly useful if you are running any kind of discussion group since you can disallow posts by ip address. Another benefit to ipaddress is it lets you identify all the activity that you do on the application. I always make between 10 and 20 test accounts when I build an application and its nice to be able to delete all of them with a single sql statement (i.e. "delete from users where ipaddress ='192.168.1.56')

date -- I always like to store the date and time that a user logged in since it lets me see who is active.

type -- I often include a "type" field in my users table just to allow for fairly arbitrary extensions that I won't think of before hand. For example, I could have a "best user" award and implement it by setting the type field to a value.

Now that we know what fields we need, what "data types" are we going to store them as? The easy way is to look at the table creation code for our users table:



    create table users (
        user_id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
        username CHAR(30) NOT NULL UNIQUE,
        type INT(2),
        password CHAR(40) NOT NULL,
        email char(75),
        hintquestion CHAR(50),
        hintanswer CHAR(50),
        ipaddress CHAR(20),
        date DATETIME
    )





Do I Really Need to Set Everything Up in Advance?

You're probably wondering why I am setting things up in advance. Is this really needed? The answer is, of course, both yes and no. With databases other than MySQL, changing the structure of your database table is fairly difficult so you do want to get it right. Still, MySQL has the most excellent "alter table" command which lets you change tables on the fly so you don't have to get it right. However, what I am trying to build here is a single user "module" that you can use in multiple applications. That's the reason that I am doing it right -- so when your application grows, you have the features that you need built right in.

The only feature that I left out of this user table is handling different levels of user rights (i.e. Fritz can add to the database while Franz can edit existing records and add to the database). This is a relatively simple change which I'll cover in a follow up article if there is interest.

With our database structure defined, we can now move on to the mysterious "MD5" function.

Understanding MD5

Before we can move onto the guts of the application, we need to tackle something new: MD5. Unless you've already built password stuff in PHP, you probably haven't used this at all (and if you have, you don't need this article). Anyway, MD5 is a PHP function that, from the PHP manual, does the following:

Calculates the MD5 hash of str using the RSA Data Security, Inc. MD5 Message-Digest Algorithm, and returns that hash. The hash is a 32-character hexadecimal number.

Ok then. What's that mean? I could go into quite a bit of boring detail but I won't. All that this means from your perspective is that you give it your password, it does arcane black magic on it and then gives you back a value that you can store in your database which you use to verify the password. Here's how its used:


<?php
    $password 
md5($password);

?>


Isn't that simple? All you do is store this new "password" in the database. Then, when you try and verify or "authenticate" a user, all you do is first call the md5 function again as shown above and then compare the two values. Here's the beauty of this: md5 is a "one way" function. In other words, if your user database is hacked, the hacker can't figure out the password from the stored md5 value. Now, as to why, you should care about this, we move onto the next section.

Why You Don't Want to Store Passwords in Clear Text

Given that we have to go to the effort of using MD5, why don't we just store passwords directly in the database? This is, to put it mildly, just a bad idea. There are lots of reasons for this including:

1. Databases get hacked all the time. Do you want to be the one to email every user you have saying "Ah... You need to change your password ..."

2. People increasingly use the same password across different web sites. If you're hacked then you really inconvenience them

3. Do you want the temptation? I don't know many development folk that wouldn't be tempted by knowing a user's password.

Trust me, you just plain don't want to do this. Don't get tempted, just don't do it. If you do, you will ultimately end up regretting it.



User Registration Module

Now that we understand the basics of what we're trying to do, we can get to the guts of our application. The first step of any login system is registration. This is where the user "registers" to use your application. This is also often called "signup". Here's what we need to do:

Create an html form where user's register
Write a PHP script that adds the user to the database if they pass certain criteria such as not already existing in the database

Here's the html form that we're going to use:



    <form name=registration action="signup.php" method="get">
        Username:
        <input name="signupusername" type="text" width="10"><BR>
        Password:
        <input name="signuppassword" type="password" width="10"><BR>
        Verify your password:
        <input name="signuppasswordverify" type="password" width="10"><BR>
        Email Address:
        <input name="emailaddress" type="text" width="10"><BR>
        <INPUT TYPE=SUBMIT VALUE="Sign Up">
    </form>




Notes:

The "type=password" reference above tells the HTML form that this is a "password" field, not a text field. What this means is that what the user types is shown as "*" not plain text. Again, this is a security measure.

While you would probably want to put this in a table for the best possible look and feel, we don't need to do that now.

I added the email address field above, even though we said it wasn't required, because a really nice, professional touch with user systems is to email the information to the user after they entered it. It's actually pretty easy and it saves a lot of silly technical support i.e. "ah... what's my password".



The Code

Now that we've captured our username and password, we can move onto the PHP script show below. First I'm going to list the script and then we'll discuss it. Don't worry about the length -- since this script has to capture data, validate data and store it to the database, it is a bit long. I've added copious comments and I do think its pretty clear. Here's the quick summary:



    Capture the user info
    Validate it for simple errors such as passwords not matching
    Validate it against the database for obvious errors (duplicate username)
    Store it to the database
    Send the user an email with their information
    Display confirmation page





<?php
    
// User Sign Up PHP Script
    //
    // This script validates information, saves it to the database
    // displays it to them
    // and sends it to them via email
    //
    // vars:
    // $signupusername
    // $signuppassword
    // $signuppasswordverify
    // $signupemailaddress
    //
    // Note have to use signup* vars initially to make it
    // clear to ourselves that
    // we're dealing with signupotherwise

    // Include common variables and message text
    // include 'zcommon.php';
    // Normally these would come from an include file. Since this is 
    // an example, we'll just put them here: 
    // db login parameters

    
$dbhost "YOURHOSTHERE";
    
$dbuser "YOURUSERHERE";
    
$dbpassword "YOURPASSHERE";
    
$db "YOURDBHERE";
    
$sysadminemail "sjohnson@fuzzygroup.com"


    
// null out cookies if new user
    // null out cookies at start of sign in routine
    // all cookies begin with "ck_" to indicate that they are a cookie
    // helps troubleshoot mysterious cookie errors
    // note on using cookies.
    // MUST BE SET before ANY http output.
    // They TRAVEL in the http HEADER so have to go first.
    
setcookie ("ck_user""");
    
setcookie ("ck_password""");
    
setcookie ("ck_user_id""");

    
// 0th check that passwords match
    
if($signuppassword!=$signuppasswordverify) {
        
create_error_page_passwordsnotmatch();
    }
    else {
        
// their passwords match so enter next validation stage
        // first test that their username isn't already in use

        // Connecting, selecting database
        
$link mysql_connect("$dbhost""$dbuser""$dbpassword")
            or die(
"Could not connect");
        
// select the database
        
mysql_select_db("$db")
            or die(
"Could not select database");
        
// try and select the username that the user entered to see if
        // it is already in the db
        
$query "SELECT username "
        
" FROM users "
        
" WHERE username='$signupusername'";

        
$result mysql_query($query)
            or die(
"Query failed at username unique testing stage.");

        
// logic -- if the num_rows is 1 then the username is already
        // in use and they have to choose another
        
$num_rows mysql_num_rows($result);
        
// don't need to get it -- its the same as what we already have

        // num_rows can't ever be >= to 1 since unique constraint
        // on the column of data
        
if ($num_rows == 1) {
            
create_error_page_usernameinuse();
        }
        else {
            
// NOTE -- Depending on how you want to define
            // a valid password (5 chars, 6 chars
            // plus a number, etc), that would go here

            //Capture the ipaddress and call MD5
            
$ipaddress getenv ("REMOTE_ADDR");
            
$encryptedpassword md5($signuppassword);

            
//try and add them to the database
            
$query "INSERT INTO users "
            
" ( username, password, date, ipaddress ) "
            
" VALUES ('$signupusername','$encryptedpassword', "
            
" NOW(), '$ipaddress')";

            
//execute the query
            
$result mysql_query($query)
            or die(
"Query failed at user insertion stage.");

            
//now query the db back for the user_id variable
            
$query "SELECT user_id "
            
" FROM users "
            
" WHERE username='$signupusername'";

            
// get the result
            
$result mysql_query($query)
            or die(
"Query failed at userid retrieval stage.");

            
//get the user_id from the result
            
$num_rows mysql_num_rows($result);
            
$row mysql_fetch_array($result);
            
$user_id $row[0];

            
// send the cookies now. MUST BE FIRST THING OUTPUT
            
setcookie ("ck_username""$signupusername");
            
setcookie ("ck_password""$signuppassword");
            
setcookie ("ck_user_id""$user_id");

            
//Define the $title variable for the page title
            
$title "Thanks for Signing Up!";

            
//Set up the page header
            
PrintPageHeader("$title");

            
//Print out the body of the page
            // Note that some basic html formatting is used
            // here to make it look better

            
print "<TABLE WIDTH=728 BGCOLOR=WHITE><TR><TD>";
            print 
"<H1>Thank You...</H1>";
            print 
"<HR>";
            print 
"<CENTER>Thanks for playing! ";
            print 
"Seriously, we appreciate your signing";
            print 
"up.</CENTER><BR><BR>";
            print 
"Here is the information that you entered:";
            print 
"<UL>";
            print 
"<LI>Username: $signupusername</LI>";
            print 
"<LI>Password: $signuppassword</LI>";
            print 
"<LI>Email Address: $emailaddress</LI>";
            print 
"</UL>";
            print 
"<BR>We have also emailed this to the email ";
            print 
"address you gave us.<BR>";
            print 
"</TD>";
            print 
"</TR>";
            print 
"<TABLE>";

            
//Usually you want to add a link to your
            // application's home page here.
            // Left as an exercise for the reader.

            //handle sending out the email if we got an email address!
            
if ($emailaddress != "") {
                
// compose the email
                
$to $emailaddress;
                
$subject "Your IMSaver Account";
                
$message "Hi there, "
                
"Your account has been created and is ready for use."
                
""
                
""
                
"Your username is: $signupusername"
                
"Your password is: $signuppassword"
                
""
                
"Thanks for signing up for YOURNAMEHERE."
                
"http://YOURURLHERE/"
                
""
                
"YOURNAMEHERE";
                
// send the email
                
mail($to$subject$message"From: YOU@YOU.COM""YOU@YOU.COM");
            }

            
//Set up the footer of the page
            
PrintPageFooter("");
        }
    }

    
// start of functions

    
function PrintPageHeader ($title) {
        print 
"<HTML>";
        print 
"<HEAD>";
        print 
"<TITLE>";
        print 
"$title";
        print 
"</TITLE>";
        print 
"</HEAD>";
        print 
"<BODY>";
    }

    function 
PrintPageFooter ($title) {
        print 
"</BODY>";
        print 
"</HTML>";
    }


    function 
create_error_page_passwordsnotmatch() {
        
//Define the $title variable for the page title
        
$title "We're Sorry But You're Passwords Don't Match!";

        
//Set up the page header
        
PrintPageHeader("$title");

        print 
"<TABLE WIDTH=728 BGCOLOR=WHITE><TR><TD>";
        print 
"<H1>We're Sorry...</H1>";
        print 
"<HR>";
        print 
"<BR>";
        print 
"We're sorry but You didn't enter matching passwords. ";
        print 
"&nbsp;Please make sure that you enter your password ";
        print 
"in the Password field and the Verify Password fields ";
        print 
"and that both are the same.<BR><BR>";
        print 
"Please press the back button and make sure that the ";
        print 
"passwords match.";

        
// close container table
        
print "</TD></TR></TABLE>";

        
//Set up the footer of the page
        
PrintPageFooter("");
    }


    function 
create_error_page_usernameinuse() {
        
//Define the $title variable for the page title
        
$title "We're Sorry But that Username is Already in Use...";

        
//Set up the page header
        
PrintPageHeader("$title");

        print 
"<TABLE WIDTH=728 BGCOLOR=WHITE><TR><TD>";

        print 
"<H1>We're Sorry...</H1>";
        print 
"<HR>";
        print 
"<BR>You entered a Username that another person is using";
        print 
"<BR>Press Back to try again.";
        
// close container table
        
print "</TD></TR></TABLE>";

        
//Set up the footer of the page
        
PrintPageFooter("");
    }


?>


That wasn't so hard, was it? Seriously, this script is actually pretty simple and if you look at the comments, I think you will find it makes sense. If not, scott@fuzzygroup.com and you know what to do.



A Plea to People Building Login Routines

You'll note that I didn't do much password or username validation above. There actually is a very good reason for this. Users increasingly need to or want to log in at different websites. Users have trouble with this since they have to have different usernames and different passwords when sites validate too stringently. As an example, lots of sites require a minimum 6 character password, alphanumeric in format i.e. catcat5. Lots of other sites require a minimum 8 character password. This is fine except when the first group of sites don't allow an 8 character password. I could name at least one major financial institution that has this level of astonishing foolishness. Here's my recommendation:


Username: any string at all as long as it is unique.

Example: scott, fuzzygroup, scott@fuzzygroup.com are all good usernames.

Why: Sure, scott@fuzzygroup.com might also represent an email address. Why do you care if there's an @ sign. The database certainly doesn't. Be flexible here, your users will appreciate it.


Password: Unless you are building an application that is financial in nature, you can accept anything that is at least 5 characters with one number.

Why: For most applications, this is simply strong enough. As long as money in ANY FORM isn't involved, you just don't have to worry. When money is involved then please contact me for consulting. Seriously, there are lots of approaches and I could write a book here. Take it seriously when money (and credit card numbers are money) is involved.

User Login Module

Now that we've written our user registration code, the next step is user login. This code is much easier and significantly shorter.

The Login Form

We'll start, as before, with our login form.



    <form name=registration action="logme.php" method="get">
    Username:
    <input name="username" type="text" width="10"><BR>
    Password:
    <input name="password" type="password" width="10"><BR>
    <INPUT TYPE=SUBMIT VALUE="Log In">
    </form>




As you can see, the form is much the same without the password verification and email address. If you notice, I call the script logme.php, not login.php. I like to centralize all my similar routines in one script for easy maintenance. This lets me put login and logout routines in one place. This is a personal thing but I find that it makes it easier. I didn't use "loginroutines.php" since that's just too long.



The Code

Once again, its time for the code. Here's the logme.php script in its entirety. Once again, I have added lots of comments to make it easy for you to understand.


<?php

    
// Login Routine and Logout Routine
    //
    // VARS:
    // $user
    // $password
    // zcommon.php for std set of vars and message text

    // Include common variables and message text
    // include 'zcommon.php';
    // Normally these would come from an include file. Since this is 
    // an example, we'll just put them here: 
    // db login parameters
    
$dbhost "YOURHOSTHERE";
    
$dbuser "YOURUSERHERE";
    
$dbpassword "YOURPASSHERE";
    
$db "YOURDBHERE";
    
$sysadminemail "sjohnson@fuzzygroup.com";

    switch (
$action) {
        case 
login:
            
process_login();
            die();
        case 
logout:
            
//null out cookies at start of login routine
            // note on using cookies.
            // MUST BE SET before ANY http output.
            // They TRAVEL in the http HEADER so have to go first.
            
setcookie ("ck_username""");
            
setcookie("ck_password""");
            
setcookie("ck_user_id""");
            die();
    }

    function 
process_login() {
        global 
$dbhost;
        global 
$dbuser;
        global 
$dbpassword;
        global 
$db;

        
// define homepage and text variables
        
global $homepage;
        global 
$homedir;
        global 
$sysadminemail;
        global 
$userstable;

        
//form vars
        
global $username;
        global 
$password;

        
// Connecting, selecting database
        
$link mysql_connect("$dbhost""$dbuser""$dbpassword")
            or die(
"Could not connect");

        
mysql_select_db("$db")
            or die(
"Could not select database");

        
//Check that the user exists in the db and if not, create an
        // error page
        
$query "SELECT user_id FROM imsaver_users WHERE "
        
"username='$username'";
        
$result mysql_query($query)
            or die(
"Query failed at userid retrieval stage.");

        
//Logic concept: if the user_id doesn't exist, an empty string
        // or "" will be returned with the $user_id call below.
        // We can test this to see if the user has entered the username
        // correctly
        
$num_rows mysql_num_rows($result);
        
$row mysql_fetch_array($result);
        
$user_id $row[0];

        
//very important for user friendliness -- tell them
        // what the login error was -- incorrect
        // username or incorrect password
        // first test -- did the username exist
        
if ($user_id == "") {
            print 
"<HTML>";
            print 
"<HEAD>";
            print 
"<TITLE>";
            print 
"Incorrect username";
            print 
"</TITLE>";
            print 
"<BODY>";
            print 
"<CENTER>";
            print 
"<B><CENTER>We're sorry but the username that you";
            print 
"entered doesn't seem to exist in our database.<BR>";
            print 
"Perhaps you entered it in error. Press the back button ";
            print 
"to try again.";
        }
        else {
            
//this means that there was 1 result from the query so that
            // username exists in the database

            //now have to verify password. Basically same code.

            
$query "SELECT password "
            
" FROM imsaver_users "
            
" WHERE username='$username'";

            
$result mysql_query($query)
                or die(
"Query failed at userid retrieval stage.");

            
//Encrypt the password the user entered since our
            // database stores it in encrypted fashion and we need to
            // compare it this way
             
$encryptedpassword md5($password);

             
$row mysql_fetch_array($result);

            
//grab the password from the row array, 0th element
            // since only 1 column selected
            // have to use a variable $passwordfromdb so we don't
            // overwrite our $password variable from the form var
            
$passwordfromdb $row[0];

            if (
$encryptedpassword == $passwordfromdb) {
                
//set our cookies for our future security checks
                 
setcookie ("ck_username"$username);
                 
setcookie("ck_password"$password);
                 
setcookie("ck_user_id"$user_id);

                
// Create our results page showing them they are logged in
                 
print "<HTML>";
                 print 
"<HEAD>";
                 print 
"<TITLE>";
                 print 
"You're Logged In!";
                 print 
"</TITLE>";
                 print 
"<BODY>";
                 print 
"You're Logged In";
                
//This needs to have a link added of course
                //If you wanted to automatically take them to the main screen
                // then use the header function to redirect them
                 
print "Click Here to Continue";
                 print 
"</BODY>";
                 print 
"</HTML>";

                
//close the database
                // Closing connection
                
mysql_close($link);
            }
            else {
                
//passwords didn't match so make an error page
                
print "<HTML>";
                print 
"<HEAD>";
                print 
"<TITLE>";
                print 
"Incorrect password";
                print 
"</TITLE>";
                print 
"<BODY>";
                print 
"<CENTER>";
                print 
"<B><CENTER>We're sorry but the password that you entered";
                print 
"doesn't match with the one in our database.<BR>";
                print 
"Press the back button to try again.";
                print 
"</CENTER>";
                print 
"</BODY>";
                print 
"</HTML>";

                
// Closing connection
                 
mysql_close($link);
            }
        }
    }


?>




Things that May Not Be Apparent

There are a couple of things that may not be apparent from the code above:

Why didn't I use the printpageheader() function in the logme script? I didn't use it because it isn't defined in this script file -- its in our signup file. In a real world application, you would put this in the include file that also defines your database variables. I didn't do this because I wanted readers to see what the functions did.

The global call at the beginning of the login function makes variables outside the function available within the function. Without this, your function has no access to variables outside the function.

Why isn't there a process_logout() function? Given the shortness of the logout code, it just wasn't needed. We only need to delete our cookies so its all right there.

How do I Call Logme.php?

Given that our logme.php script does two things, handle user logins and handle user logouts, you may be wondering how we call the script from our html page. Surely we have to "tell" the script what to do, don't we? That's exactly correct. Here is the html syntax for links to the logme.php script for both cases:



    <a href="logme.php?action=login">login</a>
    <a href="logme.php?action=logout">login</a>




The ?action=login is passed to the script in the $action variable which our "switch" statement processes and calls the right routine.

Extending This

Its not very hard to extend this. Adding the user hints, setting cookies to automatically log the user out after a set time period, all of this can be done. Drop emails to me at scott@fuzzygroup.com if you want me to follow up on this.

Conclusion and Disclaimer

That's pretty much all you need to do for a basic user login and registration system. There is more you can do but this is all that you have to do.

Example

If you want to see a working example of this code then you can check out http:// www.fuzzygroup.com/imsaver/, a free web service for saving your Instant Message sessions. All written in PHP!

About the Author

Scott Johnson is a high tech veteran having founded his first software company at 19 (in 1987, long before the "dot com"). That company, NTERGAID, made and shipped hypertext tools before the web was even conceived. After running NTERGAID successfully, Scott sold the company to Dataware where Scott led Enterprise Knowledge Management products. After Dataware, Scott moved to Mascot Network in charge of Product Management, Product Marketing. He's now available for consulting work.
Tentatively planning to Open Soon! (no dates ...) // Doing heavy development now...