11.2 HTTP Authentication with PHP

Writing PHP scripts to manage the authentication process allows for flexible authorization logic. For example, an application might apply restrictions based on group membership: a user in the finance department gets to see the reports from the budget database, while others can't. In another application, a user of a subscription-based service might supply a correct username and password, but be denied access when a fee is 14 days overdue. Or, access might be denied on Thursday evenings during Australian Eastern Standard Time when system maintenance is performed.

PHP scripts give you more control over the authentication process than Apache files or configuration. In this section, we show you how PHP scripts can use authentication credentials, and how to develop simple, flexible authentication scripts that use HTTP.

11.2.1 Accessing User Credentials

When PHP processes a request that contains user credentials encoded in the Authorized header field, access is provided to those credentials through the superglobal variable $_SERVER. The element $_SERVER["PHP_AUTH_USER"] holds the username that's supplied by the user, and $_SERVER["PHP_AUTH_PW"] holds the password.

The script shown in Example 11-1 reads the authentication superglobal variables and displays them in the body of the response. In practice, you wouldn't display them back to the user because it's insecure?we've just done this to illustrate how they can be accessed. Instead, you'd use the credentials to authenticate the user, and allow or deny access to the application. We explain how to do this in the next section.

For the PHP code in Example 11-1 to display the authentication credentials, the script needs to be requested after a user has been challenged for a username and password. For example, the challenge can be triggered by placing the script file in a directory configured by Apache to require authentication as discussed in the previous section. The use of the superglobal variables doesn't trigger authentication, it just provides access to the values the user has provided.

Example 11-1. PHP access to authentication
<!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>Authentication</title>

</head>

<body>

<?php

  if (isset($_SERVER["PHP_AUTH_USER"]))

    print "<h2>Hi there {$_SERVER["PHP_AUTH_USER"]}</h2>";

  else

    print "You need to be authenticated for this to work!";



  if (isset($_SERVER["PHP_AUTH_PW"]))

    print "<p>Thank you for your password {$_SERVER["PHP_AUTH_PW"]}!";

?>

</body>

</html>

With access to the authentication header field information, simple applications that rely on identifying the user can be developed. For example, an application that charges on a per-page view basis might use the $_SERVER["PHP_AUTH_USER"] variable when recording an access to a particular page. In this way, Apache can provide the authentication, and the application records the users' behavior.

While this simple approach to developing an application removes the need to write any PHP code to implement authentication, users and passwords need to be maintained in an Apache password file. In the next section, we describe how to manage HTTP authentication from within a PHP script, thus relieving Apache of authentication responsibilities and allowing more complex logic to be applied to request authorization.

11.2.2 Managing HTTP Authentication with PHP

PHP scripts can manage the HTTP authentication challenges. To do this, you check if the variables $_SERVER["PHP_AUTH_USER"] and $_SERVER["PHP_AUTH_PW"] are set. If they're not, the user hasn't been authenticated and you send a response containing the WWW-Authenticate header to the browser. If the variables are set, the user has answered the challenge, and you check them against the credentials stored in the script using any logic that's required. If the user's credentials match those stored in the script, the user is allowed to use the script; if not, the challenge is sent again to the browser.

In Example 11-2, the user credentials are passed to the function authenticated( ) . This function uses the unsophisticated authentication scheme of checking that the password matches one that's hard-coded into the script and, if so, it allows the user to access the application. To test the script, you can use any username and the password kwAlIphIdE (the case is important). The template that's used with the example is shown in Example 11-3.

Example 11-2. A script that generates an unauthorized response
<?php

require_once "HTML/Template/ITX.php";

require "db.inc";



function authenticated($username, $password)

{

  // If either the username or the password are

  // not set, the user is not authenticated

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

    return false;



  // Is the password correct?

  // If so, the user is authenticated

  if ($password == "kwAlIphIdE")

    return true;

  else

    return false;

}



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

$template->loadTemplatefile("example.11-3.tpl", true, true);



$username = shellclean($_SERVER, "PHP_AUTH_USER", 20);

$password = shellclean($_SERVER, "PHP_AUTH_PW", 20);



if(!authenticated($username, $password))

{

  // No credentials found - send an unauthorized

  // challenge response

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

  header("HTTP/1.1 401 Unauthorized");



  // Set up the body of the response that is

  // displayed if the user cancels the challenge

  $template->touchBlock("challenge");

  $template->show( );

  exit;

}

else

{

  // Welcome the user now they're authenticated

  $template->touchBlock("authenticated");

  $template->show( );

}

?>

Example 11-3. The template that's used with Example 11-2
<!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>Web Database Applications</title>

</head>

<body>

<!-- BEGIN challenge -->

  <h2>You need a username and password to access this service</h2>

  <p>If you have lost or forgotten your password, tough!

<!-- END challenge -->

<!-- BEGIN authenticated -->

  <h2>Welcome!</h2>

<!-- END authenticated -->

</body>

</html>

The authenticated( ) function returns false if either the $username or $password hasn't been set, or if the password isn't equal to the string kwAlIphIdE. If the user credentials fail the test, the script responds with the header field WWW-Authenticate, and sets the encoding scheme to Basic and the realm name to Flat Foot. It also includes the status code 401 Unauthorized. The PHP manual suggests sending the WWW-Authenticate response line before the HTTP/1.1 401 Unauthorized response line to avoid problems with some versions of the Internet Explorer browser.

The first time a browser requests this page, the script sends the challenge response containing the 401 Unauthorized header field. If the user cancels the authentication challenge, usually by clicking the Cancel button in a dialog box that collects the credentials, the HTML encoded in the challenge response is displayed. When they provide the correct credentials (a username and the password kwAlIphIdE), a welcome message is displayed. If they don't provide the correct credentials and don't press Cancel, the authentication dialog is redisplayed until they do.

11.2.3 Limiting Access by IP Address

Sometimes it's useful to limit access to an application, or part of an application, to users who are on a particular network or using a particular machine. For example, access to administrative functions in an application could be restricted to a single machine, or the latest version of your application could be limited to only those users in the testing department. In PHP, implementing this type of restriction is straightforward: you can check the IP address of the machine from which a request was sent by inspecting the variable $_SERVER["REMOTE_ADDR"]. You can do the same thing in Apache, but we don't discuss that here. (In addition, IP addresses can also be used to help prevent session hijacking, a problem discussed later in this chapter.)

The script shown in Example 11-4 allows access for users who have machines on a particular network subnet. The script limits access to the main content of the script to requests sent from clients with a range of IP addresses that begins with 141.190.17. Because that is just the start of an address, we test just the first 10 characters. The template used with the example is shown in Example 11-5.

Example 11-4. PHP script that forbids access from browsers outside an IP subnet
<?php

require_once "HTML/Template/ITX.php";



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

$template->loadTemplatefile("example.11-5.tpl", true, true);



if(strncmp("141.190.17", $_SERVER["REMOTE_ADDR"], 10) != 0)

{

  // Not allowed

  header("HTTP/1.1 403 Forbidden");

  $template->touchBlock("noaccess");

  $template->show( );

  exit;

}

else

{

  // Allowed

  $template->touchBlock("authenticated");

  $template->show( );

}

?>

Example 11-5. The template used with Example 11-4
<!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>Web Database Applications</title>

</head>

<body>

<!-- BEGIN noaccess -->

<h2>403 Forbidden</h2>

<p>You cannot access this page from outside the Marketing Department.

<!-- END noaccess -->

<!-- BEGIN authenticated -->

<h2>Marketing secrets!</h2>

<p>Need new development team - the old one says <i>No</i> far too often.

<!-- END authenticated -->

</body>

</html>

There are several HTTP status codes that are appropriate to use when denying access to a user. In the previous section, we used the response code of 401 Unauthorized to control HTTP authentication. However, the response status code of 403 Forbidden is more appropriate if an explanation as to why access has been denied is required and this is used in Example 11-4. The HTTP/1.1 standard describes 17 4xx status codes that have various meanings. The infamous 404 Not Found is returned by Apache if the requested resource doesn't exist, and a PHP script can return this code if the exact reason for the refusal needs to be hidden.

11.2.4 Authentication Using a Database

In this section, we show you how scripts can authenticate by querying a database table that contains usernames and passwords. Because users' credentials are sensitive information, we show how to protect passwords with encryption, and how the encrypted password is used in the authentication process.

11.2.4.1 Creating a database and table

To demonstrate the principles of using a database to manage authentication, we need a table that stores usernames and passwords, and we need a user who can access the database and the table. It's important to note that these are two different issues: the database table is used to store the usernames and passwords for the users of our application, while the MySQL database user is just used in our PHP scripts to read and write data to the database. We set up the database, table, and the MySQL account in this section.

In our examples in the remainder of the chapter, we use an authentication database that contains a users table. To create both, you need to log in as the MySQL root user and type the following into the MySQL command interpreter:

mysql> create database authentication;

Query OK, 1 row affected (0.05 sec)



mysql> use authentication;

Database changed

mysql> CREATE TABLE users (

    ->   user_name char(50) NOT NULL,

    ->   password char(32) NOT NULL,

    ->   PRIMARY KEY (user_name)

    -> ) type=MyISAM;

Query OK, 0 rows affected (0.02 sec)

The users table defines two attributes: user_name and password. The user_name must be unique and is defined as the primary key.

It's also necessary to have a MySQL user that has access to this database. You can create a user lucy with a password secret using the following statement, again entered into the MySQL command interpreter:

mysql> GRANT SELECT, INSERT, UPDATE, DELETE ON authentication.users TO

    -> lucy@127.0.0.1 IDENTIFIED BY 'secret';

Query OK, 0 rows affected (0.00 sec)

The syntax of this statement is discussed in Chapter 15. We use the user lucy in our scripts in the remainder of the chapter.

11.2.4.2 Protecting passwords

Storing user passwords as plain text represents a security risk because insiders, external hackers, and others may gain access to a database. Therefore, a common practice is to encrypt the password using a non-reversible, one-way encryption algorithm and store the encrypted version in the database. The encrypted version is then used in the authentication process. (One-way or asymmetric encryption is discussed later in this chapter.)

The process of protecting a password works as follows. First, a new username and password are collected from the user. Then, the password is encrypted and a new row is inserted into the users table that contains the plain text username and the encrypted password. Later, when the user returns and wants to log in to the application, they provide their username and password. The password provided by the user is encrypted, the row is retrieved from the users table that matches the provided username, and the encrypted version of the password supplied by the user is compared to the encrypted version stored in the table. If the username and encrypted passwords match, the credentials are correct and the user passes the authentication.

PHP provides two functions that can be used for one-way encryption of passwords. We define the functions next, and then show you examples that explain their behavior in more detail.


string crypt(string message [, string salt])

On most platforms, this function returns an encrypted string that's calculated with a popular (if somewhat old) encryption algorithm known as DES. The plain text message to be encrypted is supplied as the first argument, with an optional second argument used to salt the DES encryption algorithm. By default, only the first eight characters of the message are encrypted, and the salt is a two-character string used by DES to make the encrypted string harder to crack. PHP generates a random salt if one isn't provided. The first two characters of the returned value is the salt used in the encryption process.

As we show later, a salt is used to help prevent two passwords that are identical being encrypted to the same string. The salt and the password are both inputs to the encryption function and, therefore, when two passwords are the same but have different salts, the output is different. To encrypt another string to test if it's the same as the encrypted string, you need to know what salt was used so that you can re-use it. For this reason, the salt is returned as the first two characters of the encrypted string.

This function is one-way: the returned value can't be decrypted back into the original string.

Several PHP constants control the encryption process, and the default behavior is assumed in the description we've provided. However, on some platforms, the internals of the function actually use the MD5 approach discussed next or the salt can be longer. You should consult the PHP manual for more details.


string md5(string message)

Returns a 32-character message digest calculated from the source message using the RSA Data Security, Inc. MD5 Message Digest Algorithm (http://www.faqs.org/rfcs/rfc1321.html.). A digest is a 32-character fingerprint or signature of a message, and is not an encrypted representation of the message itself. The MD5 message digest is calculated by examining the whole message, and messages that differ by a single character produce very different digest results. Like the crypt( ) function, md5( ) is one-way.

It is impossible to generate the original message from a digest. The digest of the message is always 32 characters, and it's not an encrypted representation of the message. Instead, it's a string that's calculated from the message that is almost guaranteed to be unique to that message.

This function is widely supported on most platforms, and should be used in preference to crypt( ) for code that needs to be portable. Note that MD5 message digests and Apache's Digest authentication are unrelated concepts.

Example 11-6 shows how crypt( ) and md5( ) are used. The script generates the following output:

md5(aardvark7) = 94198c7f71931fdeb0a7f4b75a603586

crypt(aardvark7, 'aa') = aaE/1j3.0Ky/Y

crypt(aardvark7, 'bb') = bbptug8K4z6vA



md5(aardvark8) = 4a68f92613baa5202d523134e768db13

crypt(aardvark8, 'aa') = aaE/1j3.0Ky/Y

crypt(aardvark8, 'bb') = bbptug8K4z6vA

Example 11-6. Using crypt( ) and md5( )
<!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>Passwords</title>

</head>

<body>

<?php

$passwords = array( );

$passwords[] = "aardvark7";

$passwords[] = "aardvark8";



foreach($passwords as $password)

{

  print "\n<p> md5({$password}) = " . md5($password);

  print "\n<br> crypt({$password}, 'aa') = " . crypt($password, "aa");

  print "\n<br> crypt({$password}, 'bb') = " . crypt($password, "bb");

}

?>

</body>

</html>

Both functions have advantages and disadvantages:

  • md5( ) works with strings of any length. It returns a fixed-length string of 32 characters that's different if the input strings are different. It differentiates between aardvark7 and aardvark8 in Example 11-6 as one would expect.

  • crypt( ) uses only the first eight characters of a password and a salt to calculate the encrypted string and so, if the first eight characters and the salt are the same, the encrypted strings are the same. In Example 11-6, it does not differentiate between aardvark7 and aardvark8 when the salt is the same.

  • The salt in crypt( ) adds a useful extra feature that isn't automatically supported by md5( ): when the string is encrypted with a different salt string, it produces a different encrypted text even when two users have chosen the same password. In Example 11-6, the result of encrypting aardvark7 with the salts aa and bb is a very different string.

A common strategy is to use the first two characters of the username as the salt to crypt( ). In general, this results in different encrypted strings even if the users choose the same password, because it's unlikely they'd also have the same first two characters in their username. If you want to salt the md5( ) input, you could pass both the username (or part of the username) and the password to the md5( ) function by concatenating the strings.

The users table has been defined to store the 32-character result of the md5( ) function. The following fragment of code shows how the password is protected using the md5( ) function and a new user is inserted into the users table.

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

{

  // Create the digest of the password

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



  // Insert the user row

  $query = "INSERT INTO users SET password = '$stored_password',

                                  user_name = '$username'";



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

    showerror( );

}

The function expects three parameters: a MySQL database connection that has the authentication database as the selected database, a plain text username, and a plain text password. In the next section, we show you how to authenticate a user by comparing a password that's provided by the user to the stored password. Later in this chapter, we show you how passwords are updated in the users table as part of a complete authentication framework.

Because both crypt( ) and md5( ) are one-way, after a password is stored, there is no way to read back the original value. This prevents desirable features such as reminding a user of his forgotten password. However, importantly, it prevents all but the most determined attempts to get access to the passwords.

11.2.4.3 Authenticating

When a script needs to authenticate a username and password collected from an authentication challenge, it needs to check the credentials against the database. To do this, the user-supplied password is encrypted, and then a query is executed to find a row in the users table that has a matching username and encrypted password. If a row is found, the user is valid.

Example 11-7 shows the authenticateUser( ) function that validates credentials. The function is called by passing in a handle to a connected MySQL server that has the authentication database selected and the username and password collected from the authentication challenge. The script begins by testing $username and $password, and if either variable is not set, the function returns false. The script then constructs a SELECT query to search the users table using $username and the digest of $password created using the md5( ) function. The query is executed and if a row is found, the $username and $password have been authenticated, and the function returns true.

Example 11-7. Authenticating a user against an encrypted password in the users table
<?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}'";



  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;

}

?>

The authenticateUser( ) function is likely to be used in many scripts, so it's useful to store it in a require file. For example, if the code is stored in the file authentication.inc, we could rewrite Example 11-4 to use the database authentication function by requiring the file. The rewritten version is shown in Example 11-8.

Example 11-8. A rewritten version of Example 11-4 that uses database authentication
<?php

require "authentication.inc";

require "db.inc";

require_once "HTML/Template/ITX.php";



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

$template->loadTemplatefile("example.11-3.tpl", true, true);



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

    die("Could not connect to database");



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

   showerror( );



$username = mysqlclean($_SERVER, "PHP_AUTH_USER", 50, $connection);

$password = mysqlclean($_SERVER, "PHP_AUTH_PW", 32, $connection);



if (!authenticateUser($connection, $username, $password))

{

  // No credentials found - send an unauthorized

  // challenge response

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

  header("HTTP/1.1 401 Unauthorized");



  // Set up the body of the response that is

  // displayed if the user cancels the challenge

  $template->touchBlock("challenge");

  $template->show( );

  exit;

}

else

{

  // Welcome the user now they're authenticated

  $template->touchBlock("authenticated");

  $template->show( );

}

?>

11.2.4.4 Encrypting other data in a database

The PHP crypt( ) and md5( ) functions can be used only to store passwords, personal identification numbers (PINs), and so on. These functions are one-way: after the original password is encrypted and stored, you can't get it back (in fact, as discussed previously, an md5( ) return value is a signature or fingerprint and not an encrypted copy of the message). Therefore, these functions can't be used to store sensitive information that an application needs to retrieve. For example, you can't use them to store and retrieve credit card details or to encrypt a sensitive document.

To store sensitive information, you need two-way functions that use a secret key to encrypt and decrypt the data. One significant problem when using a key to encrypt and decrypt data is the need to securely manage the key. The issue of key management is beyond the scope of this book, however we discuss encryption briefly in Section 11.4.

If you need to store data using two-way encryption, a good set of tools are in the mcrypt encryption library. PHP provides a set of functions that access it but, to use them, you must install the libmcrypt library and then compile PHP with the --with-mcrypt parameter; ready-to-use Microsoft Windows software is also available from the PHP web site. We don't discuss the mcrypt library in this book, but you can find more information at http://www.php.net/manual/en/ref.mcrypt.php and at http://mcrypt.sourceforge.net/.

MySQL also offers the reversible encode( ) and decode( ) functions described in Chapter 15.