20.3 Authentication

The authentication module consists of the authentication.inc include file, the login and logout scripts, and the password change scripts. The code is closely based on that presented in Chapter 11 and we describe it only briefly here.

20.3.1 General-Purpose Functions

Example 20-4 shows the helper functions stored in the authentication.inc include file. The function newUser( ) creates a new row in the users table, and digests the password that's passed as a parameter using the md5( ) hash function. This is discussed in detail in Chapter 11.

The function authenticateUser( ) checks whether a row in the users table matches the supplied username and password (the supplied password is digested prior to comparison with those stored in the database). It returns true when there's a match and false otherwise.

The registerLogin( ) function saves the user's username as a session variable, and also stores the IP address from which they've accessed the winestore. The presence of the $_SESSION["loginUsername"] variable indicates the user has logged in successfully. The function unregisterLogin( ) deletes the same two session variables.

The function sessionAuthenticate( ) checks whether a user is logged in (by testing for the presence of $_SESSION["loginUsername"]) and that they're returning from the same IP address. If either test fails, the script calls unregisterLogin( ) and redirects to the script supplied as a parameter. This approach won't work for all situations?for example, if a user's ISP accesses the winestore through different web proxy servers, their IP address may change. It's up to you to decide whether this additional security step is needed in your applications. This is discussed in more detail in Chapter 11.

Example 20-4. The authentication.inc include file
<?php

// Add a new user to the users table

function newUser($loginUsername, $loginPassword, $cust_id, $connection)

{

   // Create the encrypted password

   $stored_password = md5(trim($loginPassword));



   // Insert a new user into the users table

   $query = "INSERT INTO users SET

             cust_id = {$cust_id},

             password = '{$stored_password}',

             user_name = '{$loginUsername}'";



   $result = $connection->query($query);



   if (DB::isError($result))

      trigger_error($result->getMessage( ), E_USER_ERROR);

}



// Check if a user has an account that matches the username and password

function authenticateUser($loginUsername, $loginPassword, $connection)

{

   // Create a digest of the password collected from the challenge

   $password_digest = md5(trim($loginPassword));



   // Formulate the SQL to find the user

   $query = "SELECT password FROM users

             WHERE user_name = '$loginUsername'

             AND password = '$password_digest'";



   $result = $connection->query($query);



   if (DB::isError($result))

      trigger_error($result->getMessage( ), E_USER_ERROR);



   // exactly one row? then we have found the user

   if ($result->numRows( ) != 1)

      return false;

   else

      return true;

}



// Register that user has logged in

function registerLogin($loginUsername)

{

   // Register the loginUsername to show the user is logged in

   $_SESSION["loginUsername"] = $loginUsername;



   // Register the IP address that started this session

   $_SESSION["loginIP"] = $_SERVER["REMOTE_ADDR"];

}



// Logout (unregister the login)

function unregisterLogin( )

{

   // Ensure login is not registered

   if (isset($_SESSION["loginUsername"]))

      unset($_SESSION["loginUsername"]);



   if (isset($_SESSION["loginIP"]))

      unset($_SESSION["loginIP"]);

}



// Connects to a session and checks that the user has

// authenticated and that the remote IP address matches

// the address used to create the session.

function sessionAuthenticate($destinationScript)

{

  // Check if the user hasn't logged in

  if (!isset($_SESSION["loginUsername"]))

  {

    // The request does not identify a session

    $_SESSION["message"] = "You are not authorized to access the URL

                            {$_SERVER["REQUEST_URI"]}";



    unregisterLogin( );

    header("Location: {$destinationScript}");

    exit;

  }



  // Check if the request is from a different IP address to previously

  if (isset($_SESSION["loginIP"]) &&

     ($_SESSION["loginIP"] != $_SERVER["REMOTE_ADDR"]))

  {

    // The request did not originate from the machine

    // that was used to create the session.

    // THIS IS POSSIBLY A SESSION HIJACK ATTEMPT



    $_SESSION["message"] = "You are not authorized to access the URL

                            {$_SERVER["REQUEST_URI"]} from the address

                            {$_SERVER["REMOTE_ADDR"]}";



    unregisterLogin( );

    header("Location: {$destinationScript}");

    exit;

  }

}

?>

20.3.2 Logging In and Out

The auth/login.php and auth/logincheck.php scripts are shown in Example 20-5 and Example 20-6 respectively. The auth/login.php script is a straightforward use of the winestoreFormTemplate class described in Chapter 16, and it simply collects the user's login name (their email address) and their password. The auth/logincheck.php script validates the username and password using functions from validate.inc discussed in Chapter 16, and then uses the helper functions discussed in the previous section to check the user's credentials and complete the login process.

The auth/logout.php script that logs the user out is shown in Example 20-7.

Example 20-5. The auth/login.php script that collects the user's credentials
<?php

// Show the login page



require_once "../includes/template.inc";

require_once "../includes/winestore.inc";

require_once "../includes/validate.inc";



set_error_handler("customHandler");



session_start( );



// Takes <form> heading, instructions, action, formVars name, and

// formErrors name as parameters

$template = new winestoreFormTemplate("Login",

                "Please enter your username and password.",

                S_LOGINCHECK, "loginFormVars", "loginErrors");





$template->mandatoryWidget("loginUsername", "Username/Email:", 50);

$template->passwordWidget("loginPassword", "Password:", 8);



// Add buttons and messages, and show the page

$template->showWinestore(NO_CART, B_HOME);

?>

Example 20-6. The auth/logincheck.php that validates and checks the user's credentials
<?php

// This script manages the login process.

// It should only be called when the user is not logged in.

// If the user is logged in, it will redirect back to the calling page.

// If the user is not logged in, it will show a login <form>



require_once "DB.php";

require_once "../includes/winestore.inc";

require_once "../includes/authenticate.inc";

require_once "../includes/validate.inc";



set_error_handler("customHandler");



function checkLogin($loginUsername, $loginPassword, $connection)

{



  if (authenticateUser($loginUsername, $loginPassword, $connection))

  {

     registerLogin($loginUsername);



     // Clear the formVars so a future <form> is blank

     unset($_SESSION["loginFormVars"]);

     unset($_SESSION["loginErrors"]);



     header("Location: " . S_MAIN);

     exit;

  }

  else

  {

     // Register an error message

     $_SESSION["message"] = "Username or password incorrect. " . 

                            "Login failed.";



     header("Location: " . S_LOGIN);

     exit;

  }

}



// ------



session_start( );



$connection = DB::connect($dsn, true);



if (DB::isError($connection))

  trigger_error($connection->getMessage( ), E_USER_ERROR);



// Check if the user is already logged in

if (isset($_SESSION["loginUsername"]))

{

     $_SESSION["message"] = "You are already logged in!";

     header("Location: " . S_HOME);

     exit;

}



// Register and clear an error array - just in case!

if (isset($_SESSION["loginErrors"]))

   unset($_SESSION["loginErrors"]);

$_SESSION["loginErrors"] = array( );



// Set up a formVars array for the POST variables

$_SESSION["loginFormVars"] = array( );



foreach($_POST as $varname => $value)

   $_SESSION["loginFormVars"]["{$varname}"] =

   pearclean($_POST, $varname, 50, $connection);



// Validate password -- has it been provided and is the length between 

// 6 and 8 characters?

if (checkMandatory("loginPassword", "password",

              "loginErrors", "loginFormVars"))

  checkMinAndMaxLength("loginPassword", 6, 8, "password",

                  "loginErrors", "loginFormVars");



// Validate email -- has it been provided and is it valid?

if (checkMandatory("loginUsername", "email/username",

              "loginErrors", "loginFormVars"))

  emailCheck("loginUsername", "email/username",

             "loginErrors", "loginFormVars");



// Check if this is a valid user and, if so, log them in

checkLogin($_SESSION["loginFormVars"]["loginUsername"],

           $_SESSION["loginFormVars"]["loginPassword"],

           $connection);

?>

Example 20-7. The auth/logout.php script that logs the user out
<?php

// This script logs a user out and redirects

// to the calling page.



require_once '../includes/winestore.inc';

require_once '../includes/authenticate.inc';



set_error_handler("customHandler");



// Restore the session

session_start( );



// Check they're logged in

sessionAuthenticate(S_LOGIN);



// Destroy the login and all associated data

session_destroy( );



// Redirect to the main page

header("Location: " . S_MAIN);

exit;

?>

20.3.3 Changing Passwords

The password change feature is implemented in the auth/password.php script shown in Example 20-8 and the auth/changepassword.php script in Example 20-9. The password change form that's output by auth/password.php is based on the winestoreFormTemplate class described in Chapter 16, and requires the user to enter their current password and two copies of their new password.

Example 20-8. The auth/password.php script that collects the user's old and new passwords
<?php

// This script shows the user a <form> to change their password

// The user must be logged in to view it.



require_once "../includes/template.inc";

require_once "../includes/winestore.inc";

require_once "../includes/authenticate.inc";



set_error_handler("customHandler");



session_start( );



// Check the user is properly logged in

sessionAuthenticate(S_MAIN);



// Takes <form> heading, instructions, action, formVars name, 

// and formErrors name as parameters

$template = new winestoreFormTemplate("Change Password",

                "Please enter your existing and new passwords.",

                S_CHANGEPASSWORD, "pwdFormVars", "pwdErrors");



// Create the password change widgets

$template->passwordWidget("currentPassword", "Current Password:", 8);

$template->passwordWidget("newPassword1", "New Password:", 8);

$template->passwordWidget("newPassword2", "Re-enter New Password:", 8);



// Add buttons and messages, and show the page

$template->showWinestore(NO_CART, B_HOME);

?>

The auth/changepassword.php script checks that all three passwords are supplied and that each is between 6 and 8 characters in length. It then checks that the two copies of the new password are identical, that the new password is different to the old one, and that the old password matches the one stored in the users table. If all the checks pass, the new password is digested with the md5( ) hash function, and the users table updated with the new value. The script then redirects to the winestore home page, where a success message is displayed. If any check fails, the script redirects to auth/password.php .

Example 20-9. The auth/changepassword.php script that validates a password change and updates the database
<?php

require_once "DB.php";

require_once "../includes/winestore.inc";

require_once "../includes/authenticate.inc";

require_once "../includes/validate.inc";



set_error_handler("customHandler");



session_start( );



// Connect to a authenticated session

sessionAuthenticate(S_MAIN);



$connection = DB::connect($dsn, true);



if (DB::isError($connection))

   trigger_error($connection->getMessage( ), E_USER_ERROR);



// Register and clear an error array - just in case!

if (isset($_SESSION["pwdErrors"]))

   unset($_SESSION["pwdErrors"]);

$_SESSION["pwdErrors"] = array( );



// Set up a formVars array for the POST variables

$_SESSION["pwdFormVars"] = array( );



foreach($_POST as $varname => $value)

   $_SESSION["pwdFormVars"]["{$varname}"] =

     pearclean($_POST, $varname, 50, $connection);



// Validate passwords - between 6 and 8 characters

if (checkMandatory("currentPassword", "current password",

              "pwdErrors", "pwdFormVars"))

  checkMinAndMaxLength("loginPassword", 6, 8, "current password",

                  "pwdErrors", "pwdFormVars");



if (checkMandatory("newPassword1", "first new password",

              "pwdErrors", "pwdFormVars"))

  checkMinAndMaxLength("newPassword1", 6, 8, "first new password",

                  "pwdErrors", "pwdFormVars");



if (checkMandatory("newPassword2", "second new password",

              "pwdErrors", "pwdFormVars"))

  checkMinAndMaxLength("newPassword2", 6, 8, "second new password",

                  "pwdErrors", "pwdFormVars");



// Did we find no errors? Ok, check the new passwords are the

// same, and that the current password is different.

// Then, check the current password.

if (count($_SESSION["pwdErrors"]) == 0)

{

   if ($_SESSION["pwdFormVars"]["newPassword1"] !=

       $_SESSION["pwdFormVars"]["newPassword2"])

     $_SESSION["pwdErrors"]["newPassword1"] =

       "The new passwords must match.";



   elseif ($_SESSION["pwdFormVars"]["newPassword1"] ==

           $_SESSION["pwdFormVars"]["currentPassword"])

     $_SESSION["pwdErrors"]["newPassword1"] =

       "The password must change.";



   elseif (!authenticateUser($_SESSION["loginUsername"],

                             $_SESSION["pwdFormVars"]["currentPassword"],

                             $connection))

     $_SESSION["pwdErrors"]["currentPassword"] =

       "The current password is incorrect.";

}



// Now the script has finished the validation,

// check if there were any errors

if (count($_SESSION["pwdErrors"]) > 0)

{

    // There are errors.  Relocate back to the password form

    header("Location: " . S_PASSWORD);

    exit;

}



// Create the encrypted password

$stored_password = md5(trim($_SESSION["pwdFormVars"]["newPassword1"]));



// Update the user row

$query = "UPDATE users SET password = '$stored_password'

          WHERE user_name = '{$_SESSION["loginUsername"]}'";



$result = $connection->query($query);

if (DB::isError($result))

   trigger_error($result->getMessage( ), E_USER_ERROR);



// Clear the formVars so a future <form> is blank

unset($_SESSION["pwdFormVars"]);

unset($_SESSION["pwdErrors"]);



// Set a message that says

 that the page has changed

$_SESSION["message"] = "Your password has been successfully changed.";



// Relocate to the customer details page

header("Location: " . S_DETAILS);

?>