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 unauthorizedHowever if a user forgets to log outand the page that sends
// challenge response ...
header("WWW-Authenticate: Basic realm=\"Flat Foot\");
header("HTTP/1.1 401 Unauthorized");
the WWW-Authenticate header field is not
requestedthen 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.
|
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 credentialsAuthenticate the user credentials against protected passwords stored
in the users tableShow how session variables are set up to support session
authentication and hijacking detectionDevelop the sessionAuthenticate( ) function that
protects each page that requires authenticationDevelop a logout function that destroys a sessionDevelop 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 page shown in
Example 11-9, and processed by the
logincheck.php script shown in Example 11-10.Applications scriptssuch as the
script shown in Example 11-12start 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 script shown in
Example 11-14 that explicitly destroys the session.
The script can also be called
directly, and it's typically included as a link on
most application pages such as .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 script that displays a password
change form to collect the current password and a new password, and
Example 11-18 is the script
change that validates the user data
and, if that succeeds, changes the password. On success or failure,
the change 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
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"Proxy servers, web gateways, and web servers often log the URLs that
"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>
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
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
<?phpThe username and password are read from the $_POST
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: ");
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: ");
exit;
}
?>
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 script.
If the user credentials do not authenticate, the script sets up the
message session variable and relocates to the
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
<?phpThe sessionAuthenticate( ) function carries out
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: ");
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: ");
exit;
}
}
?>
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
script uses the
authentication.inc file and the
sessionAuthenticate( ) function. If the user
requests this page before logging in, they're
redirected to the page. If they have
logged in, the page is displayed.
Example 11-12. The home page of an application
<?phpThe script uses the
require "authentication.inc";
require_once "HTML/Template/ITX.php";
session_start( );
// Connect to an authenticated session or relocate to
session_authenticate( );
$template = new HTML_Template_ITX("./templates");
$template->loadTemplatefile("home.tpl", true, true);
$template->setVariable("USERNAME", $_SESSION["loginUsername"]);
$template->parseCurrentBlock( );
$template->show( );
?>
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=">Change Password</a>
<p><a href=">Logout</a>
</body>
</html>
11.3.4.6 Logout script
The
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
page shown in the previous section).
Example 11-14. Logout script
<?phpThe script doesn't
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( );
?>
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 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
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
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=">here</a> to log in.
</body>
</html>
11.3.4.7 Password management
The script in Example 11-16 and the
change 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 change form
<?phpThe script displays a form that
require "authentication.inc";
require_once "HTML/Template/ITX.php";
session_start( );
// Connect to a authenticated session or relocate to
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( );
?>
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
change. 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"The data that's entered into the password form is
"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="change">
<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</a>
<p><a href=">Logout</a>
</body>
</html>
processed by the change script in
Example 11-18.
Example 11-18. The change script
<?phpThe oldPassword, newPassword1,
require "authentication.inc";
require "db.inc";
session_start( );
// Connect to an authenticated session or relocate to
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: ");
?>
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 script.If the collected fields can't be verifiedthe
two new passwords don't match or the current
password isn't validthe
$_SESSION["passwordMessage"] variable is set to
indicate that the password couldn't be changed.The change script
doesn't display any output, but sets the
Location header field to relocate the browser to
the page.