only for RuBoard - do not distribute or recompile Previous Section Next Section

Implementing User Authentication

There are four main elements to the user authentication module: user registration, login and logout, changing passwords, and resetting passwords. We will look at each of these in turn.

Registering

To register a user, we need to get his details via a form and enter him in the database.

When a user clicks on the "Not a member?" link on the login.php page, they will be taken to a registration form produced by register_form.php. This script is shown in Listing 24.5.

Listing 24.5 register_form.php—This Form Gives Users the Opportunity to Register with PHPBookmarks
<?
 require_once("bookmark_fns.php");
 do_html_header("User Registration");

 display_registration_form();

 do_html_footer();
?>

Again, you can see that this page is fairly simple and just calls functions from the output library in output_fns.php. The output of this script is shown in Figure 24.4.

Figure 24.4. The registration form retrieves the details we need for the database. We get users to type their passwords twice, in case they make a mistake.
graphics/24fig04.gif

The gray form on this page is output by the function display_registration_form(), contained in output_fns.php. When the user clicks on the Register button, he will be taken to the script register_new.php. This script is shown in Listing 24.6.

Listing 24.6 register_new.php—This Script Validates the New User's Data and Puts It in the Database
<?
   // include function files for this application
   require_once("bookmark_fns.php");


   // start session which may be needed later
   // start it now because it must go before headers
   session_start();


   // check forms filled in
   if (!filled_out($HTTP_POST_VARS))
   {
      do_html_header("Problem:");
      echo "You have not filled the form out correctly - please go back"
           ." and try again.";
      do_html_footer();
      exit;
   }

   // email address not valid
   if (!valid_email($email))
   {
      do_html_header("Problem:");
      echo "That is not a valid email address.  Please go back "
           ." and try again.";
      do_html_footer();
      exit;
   }

   // passwords not the same
   if ($passwd != $passwd2)
   {
      do_html_heading("Problem:");
      echo "The passwords you entered do not match - please go back"
           ." and try again.";
      do_html_footer();
      exit;
   }

   // check password length is ok
   // ok if username truncates, but passwords will get
   // munged if they are too long.
   if (strlen($passwd)<6 || strlen($passwd) >16)
   {
      do_html_header("Problem:");
      echo "Your password must be between 6 and 16 characters." 
           ."Please go back and try again.";
      do_html_footer();
      exit;
   }
   // attempt to register
   $reg_result = register($username, $email, $passwd); 
   if ($reg_result == "true")
   {
     // register session variable
     $valid_user = $username;
     session_register("valid_user");

     // provide link to members page
     do_html_header("Registration successful");
     echo "Your registration was successful.  Go to the members page "
          ."to start setting up your bookmarks!";
     do_HTML_URL("member.php", "Go to members page");
   }
   else
   {
     // otherwise provide link back, tell them to try again
     do_html_header("Problem:");
     echo $reg_result;
     do_html_footer();
     exit;
   }

   // end page
   do_html_footer();

?> 

This is the first script with any complexity to it that we have looked at in this application.

The script begins by including the application's function files and starting a session. (When the user is registered, we will create his username as a session variable as we did in Chapter 20, "Using Session Control in PHP." )

Next, we validate the input data from the user. There are a number of conditions we must test for. They are

The data validation functions we have used here, filled_out() and valid_email(), are shown in Listing 24.7 and Listing 24.8, respectively.

Listing 24.7 filled_out() Function from data_valid_fns.php—This Function Checks That the Form Has Been Filled Out
function filled_out($form_vars)
{
  // test that each variable has a value
  foreach ($form_vars as $key => $value)
  {
     if (!isset($key) || ($value == ""))
        return false;
  }
  return true;
}
Listing 24.8 valid_email() Function from data_valid_fns.php—This Function Checks Whether an Email Address Is Valid
function valid_email($address)
{
  // check an email address is possibly valid
  if (ereg("^[a-zA-Z0-9_]+@[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-\.]+$", $address))
    return true;
  else
    return false;
}

The function filled_out() expects to be passed an array of variables—in general, this will be the $HTTP_POST_VARS or $HTTP_GET_VARS arrays. It will check whether they are all filled out, and return true if they are and false if they are not.

The valid_email() function uses the regular expression we developed in Chapter 4, "String Manipulation and Regular Expressions," for validating email addresses. It returns true if an address appears valid, and false if it does not.

After we've validated the input data, we can actually try and register the user. If you look back at Listing 24.6, you'll see that we do this as follows:

						
$reg_result = register($username, $email, $passwd);
if ($reg_result == "true")
{
  // register session variable
  $valid_user = $username;
  session_register("valid_user");

  // provide link to members page
  do_html_header("Registration successful");
  echo "Your registration was successful.  Go to the members page "
       ."to start setting up your bookmarks!";
  do_HTML_URL("member.php", "Go to members page");
}

					

As you can see, we are calling the register() function with the username, email address, and password that were entered. If this succeeds, we register the username as a session variable and provide the user with a link to the main members page. This is the output shown in Figure 24.5.

Figure 24.5. Registration was successful—the user can now go to the members page.
graphics/24fig05.gif

The register() function is in the included library called user_auth_fns.php. This function is shown in Listing 24.9.

Listing 24.9 register() Function from user_auth_fns.php—This Function Attempts to Put the New User's Information in the Database
function register($username, $email, $password)
// register new person with db
// return true or error message
{
 // connect to db
  $conn = db_connect();
  if (!$conn)
    return "Could not connect to database server - please try later.";

  // check if username is unique
  $result = mysql_query("select * from user where username='$username'");
  if (!$result)
     return "Could not execute query";
  if (mysql_num_rows($result)>0)
     return "That username is taken - go back and choose another one.";

  // if ok, put in db
  $result = mysql_query("insert into user values
                         ('$username', password('$password'), '$email')");
  if (!$result)
    return "Could not register you  in database - please try again later.";

  return true;
}

There is nothing particularly new in this function—it connects to the database we set up earlier. If the username selected is taken, or the database cannot be updated, it will return false. Otherwise, it will update the database and return true.

One thing to note is that we are performing the actual database connection with a function we have written, called db_connect(). This function simply provides a single location that contains the username and password to connect to the database. That way, if we change the database password, we only need to change one file in our application. The function is shown in Listing 24.10.

Listing 24.10 db_connect() Function from db_fns.php—This Function Connects to the MySQL Database
function db_connect()
{
   $result = mysql_pconnect("localhost", "bm_user", "password");
   if (!$result)
      return false;
   if (!mysql_select_db("bookmarks"))
      return false;

   return $result;
}

When users are registered, they can log in and out using the regular login and logout pages. We'll build these next.

Logging In

If users type their details into the form at login.php (see Figure 24.3) and submit it, they will be taken to the script called member.php. This script will log them in if they have come from this form. It will also display any relevant bookmarks to users who are logged in. It is the center of the rest of the application. This script is shown in Listing 24.11.

Listing 24.11 member.php—This Script is the Main Hub of the Application
<?

// include function files for this application
require_once("bookmark_fns.php");
session_start();

if ($username && $passwd)
// they have just tried logging in
{
    if (login($username, $passwd))
    {
      // if they are in the database register the user id
      $valid_user = $username;
      session_register("valid_user");
    }
    else
    {
      // unsuccessful login
      do_html_header("Problem:");
      echo "You could not be logged in.
            You must be logged in to view this page.";
      do_html_url("login.php", "Login");
      do_html_footer();
      exit;
    }
}

do_html_header("Home");
check_valid_user();
// get the bookmarks this user has saved
if ($url_array = get_user_urls($valid_user));
  display_user_urls($url_array);

// give menu of options
display_user_menu();

do_html_footer();

?>

You might recognize the logic in this script: we are re-using some of the ideas from Chapter 20.

First, we check whether the user has come from the front page—that is, whether he has just filled in the login form—and try to log them in as follows:

						
if ($username && $passwd)
// they have just tried logging in
{
    if (login($username, $passwd))
    {
      // if they are in the database register the user id
      $valid_user = $username;
      session_register("valid_user");
    }

					

You can see that we are trying to log him in using a function called login(). We have defined this in the user_auth_fns.php library, and we'll look at the code for it in a minute.

If he is logged in successfully, we register his session as we did before, storing the username in the session variable $valid_user.

If all went well, we then show the user the members page:

						
do_html_header("Home");
check_valid_user();
// get the bookmarks this user has saved
if ($url_array = get_user_urls($valid_user));
  display_user_urls($url_array);

// give menu of options
display_user_menu();

do_html_footer();

					

This page is again formed using the output functions. You will notice that we are using several other new functions. These are check_valid_user(), from user_auth_fns.php; get_user_urls(), from url_fns.php; and display_user_urls() from output_fns.php. The check_valid_user() function checks that the current user has a registered session. This is aimed at users who have not just logged in, but are mid-session. The get_user_urls() function gets a user's bookmarks from the database, and display_user_urls() outputs the bookmarks to the browser in a table. We will look at check_valid_user() in a moment and at the other two in the section on bookmark storage and retrieval.

The member.php script ends the page by displaying a menu with the display_user_menu() function.

Some sample output as displayed by member.php is shown in Figure 24.6.

Figure 24.6. The member.php script checks that a user is logged in; retrieves and displays his bookmarks; and gives him a menu of options.
graphics/24fig06.gif

We will now look at the login() and check_valid_user() functions a little more closely. The login() function is shown in Listing 24.12.

Listing 24.12 The login() Function from user_auth_fns.php—This Function Checks a User's Details Against the Database
function login($username, $password)
// check username and password with db
// if yes, return true
// else return false
{
  // connect to db
  $conn = db_connect();
  if (!$conn)
    return 0;

  // check if username is unique
  $result = mysql_query("select * from user
                         where username='$username'
                         and passwd = password('$password')");
  if (!$result)
     return 0;

  if (mysql_num_rows($result)>0)
     return 1;
  else
     return 0;
}

As you can see, this function connects to the database and checks that there is a user with the username and password combination supplied. It will return true if there is, or false if there is not or if the user's credentials could not be checked.

The check_valid_user() function does not connect to the database again, but instead just checks that the user has a registered session, that is, that he has already logged in. This function is shown in Listing 24.13.

Listing 24.13 The check_valid_user() Function from user_auth_fns.php—This Function Checks That the User Has a Valid Session
function check_valid_user()
// see if somebody is logged in and notify them if not
{
  global $valid_user;
  if (session_is_registered("valid_user"))
  {
      echo "Logged in as $valid_user.";
      echo "<br>";
  }
  else
  {
     // they are not logged in
     do_html_heading("Problem:");
     echo "You are not logged in.<br>";
     do_html_url("login.php", "Login");
     do_html_footer();
     exit;
  }
}

If the user is not logged in, the function will tell him that he has to be logged in to see this page, and give him a link to the login page.

Logging Out

You might have noticed that there is a link marked "Logout" on the menu in Figure 24.6. This is a link to the logout.php script. The code for this script is shown in Listing 24.14.

Listing 24.14 logout.php—This Script Ends a User Session
<?
// include function files for this application
require_once("bookmark_fns.php");
session_start();
$old_user = $valid_user;  // store  to test if they *were* logged in
$result_unreg = session_unregister("valid_user");
$result_dest = session_destroy();

// start output html
do_html_header("Logging Out");

if (!empty($old_user))
{
  if ($result_unreg && $result_dest)
  {
    // if they were logged in and are now logged out
    echo "Logged out.<br>";
    do_html_url("login.php", "Login");
  }
  else
  {
   // they were logged in and could not be logged out
    echo "Could not log you out.<br>";
  }
}
else
{
  // if they weren't logged in but came to this page somehow
  echo "You were not logged in, and so have not been logged out.<br>";
  do_html_url("login.php", "Login");
}
do_html_footer();
?>

Again, you might find that this code looks familiar. That's because it is based on the code we wrote in Chapter 20.

Changing Passwords

If a user follows the "Change Password" menu option, he will be presented with the form shown in Figure 24.7.

Figure 24.7. The change_passwd_form.php script supplies a form where users can change their passwords.
graphics/24fig07.gif

This form is generated by the script change_passwd_form.php. This is a simple script that just uses the functions from the output library, so we have not included the source for it here.

When this form is submitted, it triggers the change_passwd.php script, which is shown in Listing 24.15.

Listing 24.15 change_passwd.php—This Script Attempts to Change a User Password
<?
 require_once("bookmark_fns.php");
 session_start();
 do_html_header("Changing password");
 check_valid_user();
 if (!filled_out($HTTP_POST_VARS))
 {
   echo "You have not filled out the form completely.
         Please try again.";
   display_user_menu();
   do_html_footer();
   exit;
 }
 else
 {
    if ($new_passwd!=$new_passwd2)
       echo "Passwords entered were not the same.  Not changed.";
    else if (strlen($new_passwd)>16 || strlen($new_passwd)<6)
       echo "New password must be between 6 and 16 characters.  Try again.";
    else
    {
        // attempt update
        if (change_password($valid_user, $old_passwd, $new_passwd))
           echo "Password changed.";
        else
           echo "Password could not be changed.";
    }


 }
   display_user_menu();
   do_html_footer();
?>

This script checks that the user is logged in (using check_valid_user()), that they've filled out the password form (using filled_out()), and that the new passwords are the same and the right length. None of this is new. If all that goes well, it will call the change_password() function as follows:

						
if (change_password($valid_user, $old_passwd, $new_passwd))
  echo "Password changed.";
else
  echo "Password could not be changed.";

					

This function is from our user_auth_fns.php library, and the code for it is shown in Listing 24.16.

Listing 24.16 change_password() Function from user_auth_fns.php—This Function Attempts to Update a User Password in the Database
function change_password($username, $old_password, $new_password)
// change password for username/old_password to new_password
// return true or false
{
  // if the old password is right
  // change their password to new_password and return true
  // else return false
  if (login($username, $old_password))
  {
    if (!($conn = db_connect()))
      return false;
    $result = mysql_query( "update user
                            set passwd = password('$new_password')
                            where username = '$username'");
    if (!$result)
      return false;  // not changed
    else
      return true;  // changed successfully
  }
  else
    return false; // old password was wrong
}

This function checks that the old password supplied was correct, using the login() function that we have already looked at. If it's correct, then the function connects to the database and updates the password to the new value.

Resetting Forgotten Passwords

In addition to changing passwords, we need to deal with the common situation in which a user has forgotten her password. Notice that on the front page, login.php, we provide a link for users in this situation, marked, "Forgotten your password?" This link will take users to the script called forgot_form.php, which uses the output functions to display a form as shown in Figure 24.8.

Figure 24.8. The forgot_form.php script supplies a form in which users can ask to have their passwords reset and sent to them.
graphics/24fig08.gif

This script is very simple, just using the output functions, so we will not go through it here. When the form is submitted, it calls the forgot_passwd.php script, which is more interesting. This script is shown in Listing 24.17.

Listing 24.17 forgot_passwd.php—This Script Resets a User's Password to a Random Value and Emails Her the New One
<?
 require_once("bookmark_fns.php");
 do_html_header("Resetting password");

 if ($password=reset_password($username))
 {
    if (notify_password($username, $password))
      echo "Your new password has been sent to your email address.";
    else
      echo "Your password could not be mailed to you."
           ." Try pressing refresh.";
 }
 else
   echo "Your password could not be reset - please try again later.";

  do_html_url("login.php", "Login");

 do_html_footer();
?>

As you can see, this script uses two main functions to do its job: reset_password() and notify_password(). Let's look at each of these in turn.

The reset_password() function generates a random password for the user and puts it into the database. The code for this function is shown in Listing 24.18.

Listing 24.18 The reset_password() Function from user_auth_fns.php—This Script Resets a User's Password to a Random Value and Emails Her the New One
function reset_password($username)
// set password for username to a random value
// return the new password or false on failure
{
  // get a random dictionary word b/w 6 and 13 chars in length
  $new_password = get_random_word(6, 13);

  // add a number  between 0 and 999 to it
  // to make it a slightly better password
  srand ((double) microtime() * 1000000);
  $rand_number = rand(0, 999);
  $new_password .= $rand_number;

  // set user's password to this in database or return false
  if (!($conn = db_connect()))
      return false;
  $result = mysql_query( "update user
                          set passwd = password('$new_password')
                          where username = '$username'");
  if (!$result)
    return false;  // not changed
  else
    return $new_password;  // changed successfully
}

This function generates its random password by getting a random word from a dictionary, using the get_random_word() function and suffixing it with a random number between 0 and 999. The get_random_word() function is also in the user_auth_fns.php library. This function is shown in Listing 24.19.

Listing 24.19 The get_random_word() Function from user_auth_fns.php—This Function Gets a Random Word from the Dictionary for Use in Generating Passwords
function get_random_word($min_length, $max_length)
// grab a random word from dictionary between the two lengths
// and return it
{
   // generate a random word
  $word = "";
  $dictionary = "/usr/dict/words";  // the ispell dictionary
  $fp = fopen($dictionary, "r");
  $size = filesize($dictionary);

  // go to a random location in dictionary
  srand ((double) microtime() * 1000000);
  $rand_location = rand(0, $size);
  fseek($fp, $rand_location);

  // get the next whole word of the right length in the file
  while (strlen($word)< $min_length || strlen($word)>$max_length)
  {
     if (feof($fp))
        fseek($fp, 0);        // if at end, go to start
     $word = fgets($fp, 80);  // skip first word as it could be partial
     $word = fgets($fp, 80);  // the potential password
  } ;
  $word=trim($word); // trim the trailing \n from fgets
  return $word;
}

To work, this function needs a dictionary. If you are using a UNIX system, the built-in spell checker ispell comes with a dictionary of words, typically located at /usr/dict/words, as it is here. If you are using some other system or do not want to install ispell, don't worry! You can download the word list used by ispell from

http://ficus-www.cs.ucla.edu/ficus-members/geoff/ispell-dictionaries.html

This site also has dictionaries in many other languages, so if you would like a random, say, Norwegian or Esperanto word, you can download one of those dictionaries instead. These files are formatted with each word on a separate line, separated by newlines.

To get a random word from this file, we pick a random location between 0 and the filesize, and read from the file there. If we read from the random location to the next newline, we will most likely only get a partial word, so we skip the line we open the file to, and take the next word as our word by calling fgets() twice.

The function has two clever bits. The first is that, if we reach the end of the file while looking for a word, we go back to the beginning:

						
if (feof($fp))
        fseek($fp, 0);        // if at end, go to start

					

The second is that we can seek for a word of a particular length—we check each word that we pull from the dictionary, and, if it is not between $min_length and $max_length, we keep searching.

Back in reset_password(), after we have generated a new password, we update the database to reflect this, and return the new password back to the main script. This will then be passed on to notify_password(), which will email it to the user.

Let's have a look at the notify_password() function, shown in Listing 24.20.

Listing 24.20 The notify_password() Function from user_auth_fns.php—This Function Emails a Reset Password to a User
function notify_password($username, $password)
// notify the user that their password has been changed
{
    if (!($conn = db_connect()))
      return false;
    $result = mysql_query("select email from user
                            where username='$username'");
    if (!$result)
      return false;  // not changed
    else if (mysql_num_rows($result)==0)
      return false; // username not in db
    else
    {
      $email = mysql_result($result, 0, "email");
      $from = "From: support@phpbookmark \r\n";
      $mesg = "Your PHPBookmark password has been changed to $password \r\n"
              ."Please change it next time you log in. \r\n";
      if (mail($email, "PHPBookmark login information", $mesg, $from))
        return true;
      else
        return false;
    }
}

In this function, given a username and new password, we simply look up the email address for that user in the database, and use PHP's mail() function to send it to her.

It would be more secure to give users a truly random password—made from any combination of upper and lowercase letters, numbers, and punctuation—rather than our random word and number. However, a password like 'zigzag487'will be easier for our user to read and type than a truly random one. It is often confusing for users to work out whether a character in a random string is 0 or O (zero or capital O), or 1 or l (one or a lowercase L).

On our system, the dictionary file contains about 45,000 words. If a cracker knew how we were creating passwords, and knew a user's name, he would still have to try 22,500,000 passwords on average to guess one. This level of security seems adequate for this type of application even if our users disregard the emailed advice to change it.

only for RuBoard - do not distribute or recompile Previous Section Next Section