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.
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.
<?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; } } ?>
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.
<?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); ?>
<?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); ?>
<?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; ?>
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.
<?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 .
<?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); ?>