home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Web Database Applications with PHP \& MySQLWeb Database Applications with PHP \& MySQLSearch this book

9.4. Web Database Applications and Authentication

So far in this chapter we have presented techniques that control access to resources—in particular, PHP scripts—based around HTTP authentication. The simplest technique discussed so far is to configure Apache to perform the authentication and authorization. For greater flexibility, we have described how PHP can manage the authentication process, allowing scripts to apply whatever logic is required to meet the authorization needs.

In this section we discuss issues of building web database applications:

  • Examining why HTTP authentication works well with stateless applications

  • Showing how a stateful application might manage HTTP authentication and the issues that are faced when building session-based web database applications

  • Discussing some reasons why HTTP authentication may not be suitable for all applications

  • Developing an authentication framework that can be used in a web database application illustrating the techniques presented in this section and earlier in this chapter

9.4.2. Building Session-Based Applications

Building stateful web applications requires special care because of the stateless nature of HTTP. In Chapter 8 we presented session management as a technique for building stateful applications. Many web database applications—such as on-line banking—require both authentication and session management. We now look at some of the issues that arise when building session-based applications that require user authentication.

9.4.2.2. Authenticating without HTTP

HTTP authentication provides a simple mechanism for building applications that need to control user access. HTTP authentication supports stateless applications well and, with additional coding, can support stateful, session-based applications. However, HTTP authentication may not meet the requirements of some web database applications. Consider the following problems of HTTP authentication:

Browsers remember passwords
When a user enters his username and password into a browser authentication dialog box—such as that shown in Figure 9-1—the browser remembers the credentials until the browser program is terminated or a new set of credentials are collected. When the user finishes with a web application—even if the application includes a logout page—the browser remembers the user credentials and allows access back to the same pages without challenge. Users may think they have logged off from an application correctly, only to leave an unattended browser as a security risk. By typing in a URL or simply using the Back button, another user can access the application unchallenged. The only sure way to protect against this kind of access is to terminate the browser.

Applications can be written to minimize this risk. By writing scripts that deliberately respond as unauthorized to a request that contains authenticated credentials, an application can enforce the intention of a logout. However, the application has to remember that the user logged out—or timed out—and respond accordingly. Such schemes lead to clumsy interactions with the user

Limited to the browser authentication dialog
When an application uses HTTP authentication, the method for collecting user credentials is limited to the authentication dialog box provided by the browser. An online application might want to present the login page with some site advertising. For example, the login page of an online store, such as our winestore, can include new arrivals of stock as advertisements.

Another feature that isn't supported using the basic HTTP authentication is allowing users to authenticate themselves with credentials other than a username and a password. You can allow a user who has forgotten his password, to go to an alternate login page that asks for his date of birth, his mother's maiden name, or other personal details to authenticate. For this kind of application you should collect a new password and restrict the number of attempts to the alternate login screen; otherwise, there could be a security risk.

Some applications require multiple logins. For example, an application might be a corporate information system that requires all users to log in for basic access but then requires an additional username and password to access a restricted part of the site. HTTP doesn't allow for multiple Authorization header fields in the one request

Authentication can be built into session-based applications by collecting user credentials in a <form>. When the <form> is submitted, the username and password are authenticated, and the authenticated state is recorded as a session variable. The authentication and authorization techniques developed earlier in this chapter—for example the authenticateUser( ) function shown in Example 9-7—can easily be modified to work with <form> data rather than $PHP_AUTH_USER and $PHP_AUTH_PW.

Collecting user credentials in a <form> and storing the authenticated state in a session has disadvantages. First, the username and password aren't encoded—not even in a basic form—when passed from the browser to the web server. This problem is solved by using the Secure Sockets Layer protocol as discussed later in this chapter. Second, session hijacking may arise because the state of the session is used to control access to the application.

9.4.2.3. Session hijacking

By using the authenticated state stored as a session variable, a session-based application can be open to hijacking. When a request is sent to a session-based application, the browser includes the session identifier, usually as a cookie, to access the authenticated session. Rather than snoop for usernames and passwords, a hacker can use a session ID to hijack an existing session. Consider an online banking application in which a hacker waits for a real user to log in and then takes over the session, by including the session ID in a request, and transfers funds into his own account. If the session isn't encrypted, it is easy to read the session ID or, for that matter, the username and password. We recommend that any application that transmits usernames, passwords, cookies that identify sessions, or personal details should be protected using encryption.

Even if the connection is encrypted, the session ID may still be vulnerable. If the session ID is stored in a cookie on the client, it is possible to trick the browser into sending the cookie unencrypted. This can happen if the cookie was set up by the server without the secure parameter that prevents cookie transmission over an insecure connection. Cookie parameters and how to set up PHP session management to secure cookies are discussed in Chapter 8.

Hijack attempts can also be less sophisticated. A hacker can hijack a session by randomly trying session IDs in the hope that an existing session might be found. On a busy site many hundreds of sessions might exist at any one time, increasing the chance of the success of such an attack. One precaution is to reduce the number of idle sessions by setting short maximum lifetimes for dormant sessions, as discussed in Chapter 8.

Another precaution is to use session IDs that are hard to guess. The default PHP session management uses a random number—that can be configured with a random seed—passed through an MD5hashing algorithm, which generates a 32-character ID. The randomness and use of MD5 hashing in PHP session IDs make them much harder to guess than an ID based on other parameters, such as the system time, the client IP address, or the username.

9.4.3. Case Study: Customer Authentication

The case study example in this chapter is an authentication framework that doesn't rely on HTTP authentication to collect the username and password. The scripts developed in the case study illustrate how several techniques are applied and how the issues raised relating to session-based applications are solved. In this case study, we:

  • Develop a login <form> to collect user credentials

  • Authenticate the user credentials against encrypted passwords stored in the customer table

  • Use the IP address of the login request to deny access to requests from other machines

  • Develop a function that is included on each page to deny access without a successful login

  • Develop a logout function

9.4.3.1. Case study overview

Each customer of the winestore has an entry in the customer table that records confidential account details, including delivery address and credit-card details. Given such information, there is a good reason to restrict access to the application and protect confidential data.

We design the login page as a <form>, and the authentication is handled by the script that processes POST variables. The POST method is used rather than GET method to prevent the username and password from appearing in the URL. The authentication uses a query on the customer table to check the credentials; we use the approach described in Section 9.3.

We create a session to record the username that is authenticated and the IP address of the machine from which the login request originated. Each protected script then tests for the existence of the session variables that hold the authenticated name and the originating IP address and checks these against the originating IP address of the request for that script.

While the pages we have developed on the online winestore site are more attractive than the examples in this section, the structure of the code is the same.

9.4.3.2. Login page

The login page displays a <form> that collects a username and password and is used as the entry point for winestore customers. The login page is also used when a login attempt fails, as the destination page when a member logs out, and as a warning page when an unauthorized request is made to a script that requires a user to log in. Also, if a user that is already authorized requests the login page, we display a message to indicate that the user is already logged on. Figure 9-3 shows the rendered login <form> with a message showing a failed login attempt.

Figure 9-3

Figure 9-3. The login page shows a failed login attempt

Example 9-8 shows the login script with two helper functions that generate the HTML. The function login_page( ) generates the HTML <form> that collects two named <input> fields: formUsername and formPassword. The argument $loginMessage passes any error or warning messages the login page needs to display. If the $loginMessage is set, a formatted message is generated and included in the page. When the <form> is submitted, the formUsername and formPassword fields are encoded as POST variables and are processed by the script that performs the authentication.

The function logged_on_page( ) in Example 9-8 generates the HTML that is used when the script detects that a user has already logged in to the application. The main part of the script initializes a session and checks if the user has already been authorized. If the session variable authenticatedUser is registered, the user has already been authorized and the function logged_on_page( ) is called. If not, the entry <form> is displayed by calling the function login_page( ), and the session is destroyed.

Example 9-8. The PHP script that generates the login <form>

<?php
// Function that returns the HTML FORM that is 
// used to collect the username and password

function login_page($errorMessage)
{
  // Generate the Login-in page
  ?>
  
<!DOCTYPE HTML PUBLIC 
    "-//W3C//DTD HTML 4.0 Transitional//EN"
    "http://www.w3.org/TR/html4/loose.dtd" >
  <html>
    <head><title>Login</title></head>
    <body>
    <h2>Winestore Login Page</h2>
    <form method="POST" action="example.9-9.php">

  <? 
  // Include the formatted error message

  if (isset($errorMessage))
    echo 
      "<h3><font color=red>$errorMessage</font></h3>";

  // Generate the login <form> layout
  ?>
    <table>
      <tr><td>Enter your username:</td>
          <td><input type="text" size=10 
                   maxlength=10 
                   name="formUsername"></td></tr>     
      <tr><td>Enter your password:</td>
          <td><input type="password" size=10 
                   maxlength=10
                   name="formPassword"></td></tr>
    </table>
    <p><input type="submit" value="Log in">
    </form>
    </body>
  </html>
  <?
}

//
// Function that returns HTML page showing that 
// the user with the $currentLoginName is logged on

function logged_on_page($currentLoginName)
{

  // Generate the page that shows the user 
  // is already authenticated and authorized
  ?>

<!DOCTYPE HTML PUBLIC 
      "-//W3C//DTD HTML 4.0 Transitional//EN"
      "http://www.w3.org/TR/html4/loose.dtd" >
  <html>
  <head><title>Login</title></head>
  <body>
    <h2>Winestore</h2>
    <h2>You are currently logged in as 
        <?=$currentLoginName ?></h2>
    <a href="example.9-10.php">Logout</a>
  </body>
  </html>
  <?
}

// Main
session_start( );

// Check if we have established a session
if (isset($HTTP_SESSION_VARS["authenticatedUser"]))
{
  // There is a user logged on
  logged_on_page(
          $HTTP_SESSION_VARS["authenticatedUser"]);
}
else
{
  // No session established, no POST variables 
  // display the login form + any error message
  login_page($HTTP_SESSION_VARS["loginMessage"]);

  session_destroy( );
}
?>

It is important that the script test the associative array holding the session variable $HTTP_SESSION_VARS["authenticatedUser"] rather than the global variable $authenticatedUser. Because of the default order in which PHP initializes global variables from GET, POST, and session variables, a user can override the value of $authenticatedUser simply by defining a GET or POST variable in the request. We discussed security problems with PHP variable initialization in Chapter 5.

9.4.3.3. Authentication script

When the login <form> is submitted, the POST variables are processed by the authentication script shown in Example 9-9. The authentication is performed by passing a handle to a connected MySQL server, the username, and the password to the function authenticateUser( ). The function executes a query to find the user row with the same username and encrypted password. As with the code in Example 9-7, we use the first two characters from the username as the salt string to the crypt( ) function.

The Boolean control variable $authenticated is set to the return value of the authenticateUser( ) function. If $authenticated is true, the username is registered as the $authenticatedUser session variable and the IP address of the client machine from which the request originated as the $loginIpAddress session variable.

If the authentication fails and $authenticated is set to false, the $loginMessage session variable is registered containing the appropriate message to display on the login <form> as shown in Figure 9-3. In Example 9-9 we always relocate back to the login page, keeping the code reasonably simple. An alternative would be to relocate back to a customer welcome page when authentication succeeds and relocate back to the login page only when authentication fails.

Example 9-9. Authentication script

<?php
include 'db.inc';
include 'error.inc';

function authenticateUser($connection, 
                          $username,
                          $password)
{
  // Test that the username and password 
  // are both set and return false if not
  if (!isset($username) || !isset($password))
    return false;

  // Get the two character salt from the username
  $salt = substr($username, 0, 2); 

  // Encrypt the password
  $crypted_password = crypt($password, $salt); 

  // Formulate the SQL query find the user
  $query = "SELECT password FROM users 
               WHERE user_name = '$username'
               AND password = '$crypted_password'";

  // Execute the query
  $result = @ mysql_query ($query,
                           $connection)
  or showerror( );

  // exactly one row? then we have found the user
  if (mysql_num_rows($result) != 1) 
    return false;
  else
    return true;

}


// Main ----------

  session_start( );

  $authenticated = false;

  // Clean the data collected from the user
  $appUsername = 
    clean($HTTP_POST_VARS["formUsername"], 10);
  $appPassword = 
    clean($HTTP_POST_VARS["formPassword"], 15);

  // Connect to the MySQL server
  $connection = @ mysql_connect($hostname,
                                $username,
                                $password) 
  or die("Cannot connect");
  if (!mysql_selectdb($databaseName,
                      $connection))
      showerror()

  $authenticated = authenticateUser($connection,
                                    $appUsername,
                                    $appPassword);

  if ($authenticated == true) 
  {
    // Register the customer id
    session_register("authenticatedUser");
    $authenticatedUser = $appUsername;

    // Register the remote IP address 
    session_register("loginIpAddress");
    $loginIpAddress = $REMOTE_ADDR;
  }
  else
  {
    // The authentication failed
    session_register("loginMessage");
    $loginMessage = 
      "Could not connect to the winestore " .
      "database as \"$appUsername\"";
  }

  // Relocate back to the login page
  header("Location: example.9-8.php");
?>

9.4.3.4. Logout script

A separate script is called when a user logs out of the application. Example 9-10 shows the script that unregisters the $authenticatedUser session variable, registers the $loginMessage variable containing the appropriate message, and relocates back to the login script. The login script checks if the $loginMessage session variable is registered and displays the message that the user has logged out.

Example 9-10. Logout script

<?php
  session_start( );

  $appUsername =
     $HTTP_SESSION_VARS["authenticatedUser"];

  $loginMessage = 
    "User \"$appUsername\" has logged out";
  
  session_register("loginMessage");

  session_unregister("authenticatedUser");

  // Relocate back to the login page
  header("Location: example.9-8.php");
?>

9.4.3.5. Authorizing other requests

The scripts shown in Example 9-8, Example 9-9, and Example 9-10 form a framework that manages the login and logout functions and sets up the authentication control session variables. Scripts that require authorization need to check the session variables before they generate any output.

The authorization code that checks the authentication control session variables, shown in Example 9-11, can be written to a separate file and included with each protected page using the include directive. This saves having to rewrite the code for each page that requires authorization.

Example 9-11 begins by initializing the session and calculating two Boolean flags. The first flag $notAuthenticated is set to true if the session variable $authenticatedUser isn't set. The second flag $notLoginIp is set to true only if the session variable $loginIpAddress is set and has the same value as the IP address of the client that sent this request. The IP address of the client that sent the request is available to scripts in the server environment variable $REMOTE_ADDR. Unlike with environment variables, PHP doesn't overwrite $REMOTE_ADDR by a GET or POST variable with the same name.

Both the $notAuthenticated flag and the $notLoginIp flag are tested, and if either is true, an appropriate $loginMessage is set and registered with the session, and then the Location: header field is sent with the HTTP response to relocate the browser back to the login script. The two cases are separated, because the script might be enhanced to record more information about the possible hijack attempt and even to destroy the session.

Example 9-11. Code that checks the authenticated state from the session variables

<?php
  session_start( );

  $loginScript = "example.9-8.php";

  // Set a boolean flag to check if 
  // a user has authenticated
  $notAuthenticated = 
    !isset($HTTP_SESSION_VARS["authenticatedUser"]);

  // Set a boolean flag to true if this request
  // originated from the same IP address
  // as the one that created this session
  $notLoginIp = 
    isset($HTTP_SESSION_VARS["loginIpAddress"]) 
    && ($HTTP_SESSION_VARS["loginIpAddress"] !=
        $REMOTE_ADDR);

  // Check that the two flags are false
  if($notAuthenticated)
  {
    // The request does not identify a session
    session_register("loginMessage");
    $loginMessage = 
      "You have not been authorized to access the " .
      "URL $REQUEST_URI";

    // Re-locate back to the Login page
    header("Location: " . $loginScript);
    exit;
  }
  else if($notLoginIp)
  {
    // The request did not originate from the machine
    // that was used to create the session. 
    // THIS IS POSSIBLY A SESSION HIJACK ATTEMPT

    session_register("loginMessage");
    $loginMessage = 
      "You have not been authorized to access the " .
      "URL $REQUEST_URI from the address $REMOTE_ADDR";

    // Re-locate back to the Login page
    header("Location: " . $loginScript);
    exit;
  }

?>

To use the code developed in Example 9-11 to protect a page, we only need to include the file containing the code. If we saved Example 9-11 to auth.inc, protecting a page is easy:

<?php include("auth.inc"); ?>
<!DOCTYPE HTML PUBLIC 
    "-//W3C//DTD HTML 4.0 Transitional//EN"
    "http://www.w3.org/TR/html4/loose.dtd" >
<html>
    ...
    <h2>Your Credit Card details</h2>
    ...
    <p><a href="example.9-10.php">Logout</a>
    ...
</html>

As discussed in Chapter 4, including files with the .inc extension presents a security problem. If the user requests the include file, the source of the include file is shown in the browser.

There are three ways to address this problem:

  • Store the include files outside the document tree of the Apache web server installation. For example, store the include files in the directory /usr/local/include/php and use the complete path in the include directive.

  • Use the extension .php instead of .inc. In this case, the include file is interpreted by the PHP script engine and produces no output because it contains no main body.

  • Configure Apache so that files with the extension .inc can't be retrieved.



Library Navigation Links

Copyright © 2003 O'Reilly & Associates. All rights reserved.