11.3 Form-Based Authentication

So far in this chapter, we have presented authorization techniques based on HTTP. In this section, we describe how to build applications that don't rely on HTTP Authentication, but instead use HTML forms to collect user credentials and sessions to implement an authentication framework. We discuss why you might want to avoid HTTP authentication, and the types of applications that benefit from managing the authentication with forms.

11.3.1 Reasons to Use HTTP Authentication

Before you decide to build an application that manages its own authentication, you should consider the advantages of using HTTP Authentication:

  • It is easy to use. Protecting an application can be as simple as configuring your web server or creating a file.

  • The HTTP authentication process can be managed by PHP code when an application needs to take over the checking of user credentials. We described how to do this in Section 11.2.2 earlier in this chapter.

  • Support to collect and remember user credentials is built into browsers.

  • HTTP authentication works well with stateless applications.

11.3.2 Reasons to Avoid HTTP Authentication

Some applications, particularly session-based applications that track authenticated users, have requirements that are difficult to meet using HTTP authentication.


Browsers remember passwords

Usernames and passwords entered into a browser authentication dialog box (such as that shown in Figure 11-1) are remembered until the browser program is terminated or a new set of credentials is collected. You can force a browser to forget credentials by deliberately responding with an unauthorized code even when a request contains authenticated credentials. The following fragment does this:

// Force the browser to forget with an unauthorized

// challenge response ...

header("WWW-Authenticate: Basic realm=\"Flat Foot\"");

header("HTTP/1.1 401 Unauthorized");

However if a user forgets to log out?and the page that sends the WWW-Authenticate header field is not requested?then an unattended browser becomes a security risk. By typing in a URL or simply using the Back button, another user can access the application unchallenged.


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 in a style that's consistent with the application, perhaps by using a template, or in another language.


HTTP does not support multiple realms

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.

11.3.3 Authentication and Session-Based Applications

In Chapter 10, we presented session management as a technique for building stateful applications. For many applications that require authentication, a session is created when a user logs in, and tracks his interaction until he logs out or the session times out. We introduced this pattern in Chapter 10.

The basic pattern of session-based authentication is to authenticate a user's credentials once, and set up a session that records this authenticated status in session variables. Credentials are collected using a form and processed by the set-up script. Then, the authenticated status is recorded in the session; this contrasts with HTTP authentication, which sends the authenticated credentials with each request. If the session times out (or the user destroys the session), the authenticated status is destroyed; therefore, unlike authenticated HTTP credentials, the session ID cookie can't be used after the session has timed out and this makes the application more secure.

Collecting user credentials in a form and storing the authenticated state in a session has two disadvantages. First, the username and password aren't encrypted when passed from the browser to the web server. Therefore, in the PHP examples we present in the rest of this chapter, the username and password are transmitted as plain text; using the Secure Sockets Layer protocol, as discussed later in this chapter, solves this problem. Second, session hijacking is possible because the state of the session is used to control access to the application; session hijacking is discussed next.

11.3.3.1 Session hijacking

By using session variables to maintain authentication, an 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. The hacker then includes the session ID in a request, and transfers funds into his own account. If the session isn't encrypted, it's easy to read the session ID. 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. How to set up PHP session management to secure cookies is discussed in Chapter 10.

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 can be found. On a busy site, many thousands of sessions might exist at any one time, increasing the chance of success for such an attack. One precaution is to reduce the number of idle sessions by setting a short maximum lifetime for dormant sessions, as discussed in Chapter 10.

11.3.3.2 Recording IP addresses to detect session hijack attempts

Earlier in this chapter, we showed how to access the IP address of the browser when processing a request. The script shown in Example 11-4 checks the IP address set in the $_SERVER["REMOTE_ADDR"] variable against a hard-coded string that limits access to users whose machines are on a particular subnet.

The IP address of the client can also be used to help prevent session hijacking. If the IP address set in the $_SERVER["REMOTE_ADDR"] variable is recorded as a session variable when a user initially connects to an application, subsequent requests can be checked and allowed only if they are sent from the same IP address. We show you how to do this in the next section.

Using the IP address as recorded from the HTTP request has limitations. Network administrators often configure proxy servers to hide the originating IP address by replacing it with the address of the proxy server. All users who connect to an application via such a proxy server appear to be located on the one machine. Some large sites?such as that of a large university campus?might even have several proxy servers to balance load, so successive requests coming from a single user might appear to change address.


11.3.4 Session-Based Authentication Framework

The authentication framework developed in this section follows the pattern described in Chapter 10 and uses techniques developed earlier in the chapter. In this section we:

  • Develop a login script that uses a form to collect user credentials

  • Authenticate the user credentials against protected passwords stored in the users table

  • Show how session variables are set up to support session authentication and hijacking detection

  • Develop the sessionAuthenticate( ) function that protects each page that requires authentication

  • Develop a logout function that destroys a session

  • Develop scripts that allow a user to change his password

The scripts presented in this section have been kept as simple as possible to illustrate the concepts. They use the authentication database and users table described earlier in this chapter, and the MySQL database connection is established with the user lucy and the password secret. A more complex authentication framework that's based on the scripts described here is presented with the online winestore in Chapter 16 through Chapter 20.

11.3.4.1 Code overview

The basic pattern of session-based authentication is to authenticate a user's credentials once, and set up a session that records this authenticated status as session variables. Credentials are collected with the login.html page shown in Example 11-9, and processed by the logincheck.php script shown in Example 11-10.

Applications scripts?such as the home.php script shown in Example 11-12?start by checking the status of the authentication session variables before running any other code. This check is performed by the sessionAuthenticate( ) function. If this check fails, the user is redirected to the logout.php script shown in Example 11-14 that explicitly destroys the session. The logout.php script can also be called directly, and it's typically included as a link on most application pages such as home.php.

The functions that are reused in the framework are implemented in a require file authentication.inc shown in Example 11-11. The file contains the authenticateUser( ) function that compares user-supplied credentials to those in the database (the function is shown in Example 11-7) and the sessionAuthenticate( ) function.

The password change module is shown in Example 11-16 and Example 11-18. Example 11-16 lists the password.php script that displays a password change form to collect the current password and a new password, and Example 11-18 is the script changepassword.php that validates the user data and, if that succeeds, changes the password. On success or failure, the changepassword.php script redirects to the password change page and displays a message to inform the user.

11.3.4.2 Login page

Example 11-9 shows the login.html page with a form that collects a username and password. The login page does not contain any PHP code.

Example 11-9. Login page
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"

                      "http://www.w3.org/TR/html401/loose.dtd">

<html>

<head>

  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">

  <title>Login</title>

</head>

<body>

<h1>Application Login Page</h1>

<form method="POST" action="logincheck.php">

<table>

  <tr>

    <td>Enter your username:</td>

    <td><input type="text" size="10" name="loginUsername"></td>

  </tr>

  <tr>

    <td>Enter your password:</td>

    <td><input type="password" size="10" name="loginPassword"></td>

  </tr>

</table>

<p><input type="submit" value="Log in">

</form>

</body>

</html>

Proxy servers, web gateways, and web servers often log the URLs that are requested, so the page submits the form input fields using the POST method, rather than using the GET method that encodes field values in the URL. This prevents user credentials from appearing in log files.

11.3.4.3 Setup script

The logincheck.php script shown in Example 11-10 authenticates the user by processing the POST variables collected in the login.html page, and sets up the session variables that record the authenticated status. This script does not generate any output except a Location header to relocate to the home page of the application or the logout page if authentication fails.

Example 11-10. Setup script
<?php

require 'authentication.inc';

require 'db.inc';



if (!$connection = @ mysql_connect("localhost", "lucy", "secret"))

  die("Cannot connect");



// Clean the data collected in the <form>

$loginUsername = mysqlclean($_POST, "loginUsername", 10, $connection);

$loginPassword = mysqlclean($_POST, "loginPassword", 10, $connection);



if (!mysql_selectdb("authentication", $connection))

  showerror( );



session_start( );



// Authenticate the user

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

{

  // Register the loginUsername

  $_SESSION["loginUsername"] = $loginUsername;



  // Register the IP address that started this session

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



  // Relocate back to the first page of the application

  header("Location: home.php");

  exit;

}

else

{

  // The authentication failed: setup a logout message

  $_SESSION["message"] = 

    "Could not connect to the application as '{$loginUsername}'";



  // Relocate to the logout page

  header("Location: logout.php");

  exit;

}

?>

The username and password are read from the $_POST superglobal array and untainted. Then, the username and password are passed to the authenticateUser( ) function. If the authenticateUser( ) function returns true, the user has successfully been authenticated and the script sets up the $_SESSION["loginUsername"] and $_SESSION["loginIP"] session variables, and the Location header field is sent to re-locate the browser to the home.php script. If the user credentials do not authenticate, the script sets up the message session variable and relocates to the logout.php script.

11.3.4.4 The authentication.inc require file

All pages that are protected by the authentication framework need to check the $_SESSION["loginUsername"] and $_SESSION["loginIP"] session variables to ensure that the user has successfully authenticated before running any other code. The sessionAuthenticate( ) function shown in Example 11-11 performs these checks and is included in the authentication.inc file.

Example 11-11. The sessionAuthenticate( ) and authenticateUser( ) functions
<?php



function authenticateUser($connection, $username, $password)

{

  // Test the username and password parameters

  if (!isset($username) || !isset($password))

    return false;



  // Create a digest of the password collected from

  // the challenge

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



  // Formulate the SQL find the user

  $query = "SELECT password FROM users WHERE user_name = '{$username}'

            AND password = '{$password_digest}'";



  // Execute the query

  if (!$result = @ mysql_query ($query, $connection))

    showerror( );



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

  if (mysql_num_rows($result) != 1)

    return false;

  else

    return true;

}



// 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( )

{

  // 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"]}";



    header("Location: logout.php");

    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"]}";



    header("Location: logout.php");

    exit;

  }

}



?>

The sessionAuthenticate( ) function carries out two tests: first, if the session variable $_SESSION["loginUsername"] isn't set, the user isn't logged in; and, second, if session variable $_SESSION["loginIP"] isn't set or it doesn't have the same value as the IP address of the client that sent the current request, a possible hijack attempt has occurred. If either test fails, a $_SESSION["message"] variable is set with an appropriate message and the Location header field is used to relocate the browser to the logout script.

Example 11-11 also includes the authenticateUser( ) function that's reproduced from Example 11-7.

11.3.4.5 Application scripts and pages

Example 11-12 shows how the home.php script uses the authentication.inc file and the sessionAuthenticate( ) function. If the user requests this page before logging in, they're redirected to the logout.php page. If they have logged in, the home.php page is displayed.

Example 11-12. The home page of an application
<?php

require "authentication.inc"; 

require_once "HTML/Template/ITX.php";



session_start( );



// Connect to an authenticated session or relocate to logout.php

session_authenticate( );



$template = new HTML_Template_ITX("./templates");

$template->loadTemplatefile("home.tpl", true, true);



$template->setVariable("USERNAME", $_SESSION["loginUsername"]);

$template->parseCurrentBlock( );

$template->show( );

?>

The script uses the home.tpl template shown in Example 11-13 to display the $_SESSION["loginUsername"] variable that shows who is logged on. This script also provides links to log out and to change the user's password.

Example 11-13. The home.tpl template that's used with Example 11-12
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"

                      "http://www.w3.org/TR/html401/loose.dtd">

<html>

<head>

  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">

  <title>Home</title>

</head>

<body>

  <h1>Welcome to the application</h1>

  You are logged on as {USERNAME}

  <p><a href="password.php">Change Password</a>

  <p><a href="logout.php">Logout</a>

</body>

</html>

11.3.4.6 Logout script

The logout.php script is shown in Example 11-14. It's either requested by another script (such as logincheck.php) when the user fails the authentication process, or a user can explicitly end a session by requesting it (for example, from the home.php page shown in the previous section).

Example 11-14. Logout script
<?php

  require_once "HTML/Template/ITX.php";

  session_start( );



  $message = "";



  // An authenticated user has logged out -- be polite and thank them for

  // using your application.

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

    $message .= "Thanks {$_SESSION["loginUsername"]} for

                 using the Application.";



  // Some script, possibly the setup script, may have set up a 

  // logout message

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

  {

    $message .= $_SESSION["message"];

    unset($_SESSION["message"]);

  }



  // Destroy the session.

  session_destroy( );



  // Display the page (including the message)

  $template = new HTML_Template_ITX("./templates");

  $template->loadTemplatefile("logout.tpl", true, true);

  $template->setVariable("MESSAGE", $message);

  $template->parseCurrentBlock( );

  $template->show( );

?>

The logout.php script doesn't call the sessionAuthenticate( ) function to check that a user is authenticated, and so we don't need to include the authentication.inc file. Instead, the logout.php function calls session_start( ) and then tests if either of the session variables $_SESSION["loginUsername"] and $_SESSION["message"] are set. If either is set, they are used to create a message to show the user:

  • The $_SESSION["message"] variable is created in the logincheck.php or authentication.inc scripts when user credentials fail to authenticate and it's used to explain why the process failed.

  • The $_SESSION["loginUsername"] variable is used in logout.php to thank the user for using the application.

With the message complete, the script destroys the session by calling the session_destroy( ) function. The logout page prints the $message variable using the template logout.tpl shown in Example 11-15, and this page provides a link back to the login.html page.

Example 11-15. The logout.tpl template file that's used with Example 11-14
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"

                      "http://www.w3.org/TR/html401/loose.dtd">

<html>

<head>

  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">

  <title>Logout</title>

</head>

<body>

  <h1>Application Logout Page</h1>

  {MESSAGE}

  <p>Click <a href="login.html">here</a> to log in.

</body>

</html>

11.3.4.7 Password management

The password.php script in Example 11-16 and the changepassword.php script in Example 11-18 allow a user to change their password. Both scripts start by requiring the authentication.inc file and calling the sessionAuthenticate( ) function, allowing access only when a user has successfully authenticated.

Example 11-16. The password.php password change form
<?php

require "authentication.inc";

require_once "HTML/Template/ITX.php";



session_start( );



// Connect to a authenticated session or relocate to logout.php

sessionAuthenticate( );



$message = "";



// Check if there is a password error message

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

{

  $message = $_SESSION["passwordMessage"];

  unset($_SESSION["passwordMessage"]);

}



// Display the page (including the message)

$template = new HTML_Template_ITX("./templates");

$template->loadTemplatefile("password.tpl", true, true);

$template->setVariable("USERNAME", $_SESSION["loginUsername"]);

$template->setVariable("MESSAGE", $message);

$template->parseCurrentBlock( );

$template->show( );

?>

The password.php script displays a form that collects the original password and the new password twice; the new password is collected twice to minimize the chances of a typing error rendering the new password unusable. The script uses the password.tpl template shown in Example 11-17. There are two template placeholders: USERNAME is used to display the name of the logged-in user, and MESSAGE is used to display a message that is stored in a session variable that is set by changepassword.php. Once a message has been recorded for display, it's unset in the session store so that it doesn't appear again.

Example 11-17. The password.tpl template used with Example 11-16
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"

                      "http://www.w3.org/TR/html401/loose.dtd">

<html>

<head>

  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">

  <title>Password Change</title>

</head>

<body>

  <h1>Change Password for {USERNAME}</h1>

  {MESSAGE}

  <form method="POST" action="changepassword.php">

  <table>

    <tr>

      <td>Enter your existing password:</td>

      <td><input type="password" size="10" name="oldPassword"></td>

    </tr>

    <tr>

      <td>Enter your new password:</td>

      <td><input type="password" size="10" name="newPassword1"></td>

    </tr>

    <tr>

      <td>Re-enter your new password:</td>

      <td><input type="password" size="10" name="newPassword2"></td>

    </tr>

  </table>

  <p><input type="submit" value="Update Password">

  </form>

  <p><a href="home.php">Home</a>

  <p><a href="logout.php">Logout</a>

</body>

</html>

The data that's entered into the password form is processed by the changepassword.php script in Example 11-18.

Example 11-18. The changepassword.php script
<?php

require "authentication.inc";

require "db.inc";



session_start( );



// Connect to an authenticated session or relocate to logout.php

sessionAuthenticate( );



if (!$connection = @ mysql_connect("localhost", "lucy", "secret"))

  die("Cannot connect");



// Clean the data collected from the user

$oldPassword = mysqlclean($_POST, "oldPassword", 10, $connection);

$newPassword1 = mysqlclean($_POST, "newPassword1", 10, $connection);

$newPassword2 = mysqlclean($_POST, "newPassword2", 10, $connection);



if (!mysql_selectdb("authentication", $connection))

  showerror( );



if (strcmp($newPassword1, $newPassword2) == 0 &&

  authenticateUser($connection, $_SESSION["loginUsername"], $oldPassword))

{

  // OK to update the user password



  // Create the digest of the password

  $digest = md5(trim($newPassword1));



  // Update the user row

  $update_query = "UPDATE users SET password = '{$digest}'

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



  if (!$result = @ mysql_query ($update_query, $connection))

    showerror( );



  $_SESSION["passwordMessage"] =

    "Password changed for '{$_SESSION["loginUsername"]}'";

}

else

{

  $_SESSION["passwordMessage"] =

    "Could not change password for '{$_SESSION["loginUsername"]}'";

}



// Relocate to the password form

header("Location: password.php");

?>

The oldPassword, newPassword1, and newPassword2 fields are read from the $_POST superglobal array, and made safe with the mysqlclean( ) function. Then, if both the new password fields are identical, and the current password is valid for the currently logged in user, the update code runs. As discussed previously, collecting the new password twice helps prevent the introduction of typing errors, and calling the authenticateUser( ) function ensures that only the user herself can change the password.

Once the collected fields have been verified, the password can be updated in the database. The user's row is updated with the MD5 digest of the new password, and the $_SESSION["passwordMessage"] variable is set to indicate that the password has been changed. The message is displayed by the password.php script.

If the collected fields can't be verified?the two new passwords don't match or the current password isn't valid?the $_SESSION["passwordMessage"] variable is set to indicate that the password couldn't be changed.

The changepassword.php script doesn't display any output, but sets the Location header field to relocate the browser to the password.php page.



     
    ASPTreeView.com
     
    Evaluation has ЙДН№ёЧФ¶»expired.
    Info...