Last Updated:

Authentication with PHP and MySQL.

Fortunately for Web users, the information that the browser provides does not allow to identify the user. If you want to know the name of the visitor and other details, you should ask him about it.

PHP and MySQL

Having requested and received information about the visitor in response, it is necessary to somehow associate it with the visitor at his next entrances to the site. Assuming that only one user visits the site from a particular computer using a specific user name on that computer, and each user works on only one computer, then a cookie can be created on the user's computer to identify the user. This assumption is not true for most users. Often many people take turns using the same computer, or someone uses several computers. At the very least, sometimes you have to ask the user again who they are and ask the visitor to provide some proof that he is who he claims to be.

Asking a user to prove their identity is called authentication. A common method of authentication on the Web is to require visitors to provide a unique user name and password. Authentication is typically used to allow or deny access to specific pages or resources. Authentication may be optional or used for other purposes, such as personalization.

Implement access control.

 Simple access control is easy to implement. The code shown in Listing 7.10 outputs one of three possible pages. If this file is downloaded without parameters, an HTML form will be displayed that prompts you to enter your user name and password. If the settings are present at boot time, but they are incorrect, an error message is displayed. If the settings are present during the download and they are correct, the secret content is displayed to the visitor.

Code Listing 6.11. secret.php — PHP code to implement a simple authentication mechanism

<?

if(!isset($name)&&!isset($password))

{// Visitor must enter name and password

?>

<h1>Please Log In</h1>This page is secret.

<form method = post action = "secret.php">

<table border = 1>

<tr><th>Username</th><td><input type = text name = name></td></tr>

<tr><th>Password</th><td><input type = password name = password>

</td></tr><tr><td colspan =2 align = center>

<input type = submit value = "Log In"></td></tr>

</table></form>

<? }

else if($name=="user"&&$password=="pass")

<? The visitor's username and password combination is correct

echo "<h1>Here it is!</h1>";

echo "I bet you are glad you can see this secret page."; }

else

{ // Visitor's username and password combination is incorrect

echo "<hl>Go Away!</hl>";

echo "You are not authorized to view this resource."; }

?>

The code shown in Listing 6.11 implements a simple mechanism to allow authorized visitors to see the protected page, but the code contains several significant issues. This scenario:

  • Supports only one hard-coded username and password
  • Stores the password in plain text
  • Protects only one page
  • Sends the password in plain text

These problems can be solved with varying degrees of effort and success.

Storage of passwords. 

There are many more appropriate places to store passwords than the script code. Inside the script, it is very difficult to modify the data. You can write a script that will change yourself, but that's a bad idea. This will mean that there is a script running on the server that is writable and editable by other users. Storing passwords in a separate file on the server will allow you to easily write a program for adding and removing users, as well as for changing passwords.

Within a script or other data file, there is a limit to the number of users that can be served without seriously harming the overall performance of the script. If you plan to store a large number of elements in a file or search within a large number of elements, then, as discussed earlier, you should consider using a database instead of a two-dimensional file. The practical method of selection is that if you are going to store and search more than 100 items, you should give preference to the database.

Using a database to store visitor names and passwords won't complicate the scenario much, but it will allow you to quickly authenticate multiple users. It will also make it easier to create a script to add and remove users, as well as allow users to change their passwords.

The script for authenticating page visitors using the database is shown in Listing 6.12.

Code Listing 6.12secretdb.php is the use of MySQL for a simple authentication mechanism.

<?

if(!isset($name)&&!isset($password))

{ // Visitor must enter username and password

?>

<hl>Please Log In</hl>This page is secret.

<form method = post action = "secretdb.php"><table border = l>

<tr><th>Username</th><td><input type = text name = name></td></tr>

<tr><th>Password</th><td><input type = password name = password>

</td></tr><tr><td colspan =2 align = center>

<input type = submit value = "Log In"></td> </tr>

</table></form>

<? }

else

{ // Connect to MySQL

$mysql = mysql_connect( 'localhost', 'webauth', 'webauth' );

if(!$mysql)

{echo 'Cannot connect to database.'; exit;}

Select the appropriate database

$mysql = mysql_select_db('auth');

if(!$mysql)

{ echo 'Cannot elect database.'; exit; }

Query the database to verify

whether the corresponding entry exists

$query = "select count (*) from auth where name = '$name' and

pass = '$password'";

$result = mysql_query($query);

if (!$result)

{echo 'Cannot run query.'; exit; }

$count = mysql_result($result, 0, 0 );

if ( $count > 0 )

{// The visitor's username and password combination is correct

echo "<hl>Here it is!</hl>";

echo "I bet you are glad you can see this secret page."; }

else

<?

The visitor's username and password combination is incorrect

echo "<hl>Go Away!</hl>";

echo "You are not authorized to view this resource."; }

}

?>

You can create the database used in the sample by connecting to MySQL as root and running the script shown in Listing 6.13.

Code Listing 6.13. createauthdb.sql - Create a database, a table, and two users.

create database auth;

use auth;

create table auth (

name varchar(10) not null,

pass varchar(30) not null,

primary key (name)

);

insert into auth values ('user', 'pass');

insert into auth values

( 'testuser', password('test123') );

grant select, insert, update, delete

on auth.*

to webauth@localhost

identified by 'webauth';

Encrypt passwords. Whether passwords are stored in a database or in a file, storing passwords in plain text carries unwarranted risks. A unidirectional hashing algorithm will provide additional protection at little additional cost.

The PHP function crypt() is a unidirectional cryptographic hash function. The prototype of this function is as follows:

string crypt (string str[, string salt])

With the string str as input, this function returns a pseudo-random string. For example, if you pass the string "pass" to a function and a salt argument of "xx", crypt() will return the string "xxkTlmYjlikoII". This string cannot be deciphered and turned back into "pass" even by its creator, so at first glance the string may not seem so useful. What makes the crypt() function useful is that the result of that function is deterministic. Each time you call with the same str and salt parameters, this function will return the same result.

Instead of PHP code, such as

if( $username == "user" && password == "pass" )

{ // The password is the same

}

you can use this code

if($username= 'user' && crypt($password,'xx') == 'xxkTlmYjlikoII')

{ // The password is the same

}

We don't need to know what the string "xxkTlmYjlikoH" looked like before using the crypt() function. It is only necessary to know whether the entered password coincides with the password for which the crypt() function was used.

As mentioned, hard-coding the correct visitor names and passwords is a bad idea. To do this, you should organize a separate file or database. If you are using a MySQL database to store authentication data, you can use the crypt() PHP function or the MYSQL PASSWORD() function. the result of these functions is not the same, but they have the same purpose. Both the crypt() and PASSWORD() functions receive the string as an argument and apply an invertible hashing algorithm to the daily string.

To use the PASSWORD() function in Listing 14.2, the SQL query should be rewritten as follows:

select count (*) from auth where

name = '$name' and

pass = password('$password')

This query will count the number of rows in the auth table where the value of the name field matches the contents of the $name variable and the pass field matches the result of the PASSWORD() function applied to the value of the $password variable. If we force visitors to choose unique names, the query result can be 0 or 1.

Protect multiple pages. 

Protecting more than one page with scenarios like this is a bit more complicated. Because there is no state mechanism in the HTTP protocol, there is no automatic communication or association between successive requests from the same visitor. This complicates the transfer between pages of user input, such as authentication data.

To create this functionality yourself, you'll need to include parts of Listing 6.11 in each page you want to protect. Using the auto_prepend_file directives and auto_append_file, the desired file can be automatically inserted at the beginning (prepend) or end (append) of each file in the specified directories.

If we use this approach, what happens when a user opens several pages on the site? You cannot request a password separately for each page that the user wishes to view.

You can include user input in each hyperlink on the page. Because users can use spaces or other characters that are not allowed in the URL, you should use the urlencode() function to securely package such characters.

There are still some problems with this approach. Because the authentication data will be present in the Web page sent to the visitor, the secure pages that the user has visited can be viewed by anyone working on the same computer. To do this, just click on the "Back" button in the browser window and view cached copies of pages or look into the history of page visits. The password is sent to and from the browser with each requested or provided page, which happens more often than necessary.

The problem can be solved with the help of two mechanisms - basic HTTP authentication and session support. Basic authentication solves the caching problem, but the server still sends the password to the Web browser in every request. Session management solves both problems. Let's look at basic HTTP authentication first, and session management is covered later.

Basic authentication. 

Fortunately, user authentication is a fairly common task and there are authentication capabilities built into the HTTP protocol. Scripts and Web servers can request authentication from a Web browser. After that, the Web browser should display a dialog box or something similar and ask the user for the necessary information.

Although the Web server requests new authentication details in each user request, the Web browser does not need to request this information for each page. In general, the browser stores authentication details while the browser window is open and automatically sends them without user intervention.

The described feature of the HTTP protocol is called basic authentication. Baov authentication can be enabled by PHP or by using a Web server, Apache, and IIS. Next, methods involving the use of PHP are considered.

Basic authentication transmits the username and password in plain text and is therefore not particularly secure. The HTTP 1.1 protocol has a more secure method called digest authentication. This method uses a hashing algorithm (typically MD5) to mask the details of the transaction. Digest authentication is supported on many Web servers, but is not supported in a significant number of browsers. Digest authentication has been supported in Microsoft Internet Explorer since version 5.0. Digest authentication support is included in Netscape Navigator version 6.0.

In addition to the very weak support in the set of available browsers, digest authentication is also not very secure. Both basic and digest authentication provide a low level of security. None of these methods guarantees to the user that he is working with the exact computer to which he planned to get access. Both methods allow the attacker to repeat the same request to the server. Because basic authentication transmits a user's password in the clear, any attacker capable of intercepting packets can simulate a user's request.

Basic authentication provides a low level of security, similar to that provided when connecting via Telnet or FTP. These methods also transmit passwords in plain text. Digest authentication is somewhat more secure and encrypts passwords before transmission. The use of ssl and digital certificates allows you to reliably protect all parts of transactions on the Web.

However, in many situations, a fast and relatively insecure method, such as basic authentication, would be most appropriate.

Basic authentication helps protect named areas and requires users to enter the correct user name and password. Scopes are named, so there can be multiple scopes on a single server. Different files and directories on the same server can belong to different scopes, each protected by its own set of user names and passwords. Named scopes also allow you to group multiple directories on a single physical or virtual site into a single scope and protect the entire scope with a single password.

Use basic authentication in PHP. PHP scenarios can mostly be called cross-platform, but the use of basic authentication is based on environment variables set by the server. The HTTP authentication script should determine the type of server and behave accordingly depending on whether it is running as an Apache module on an Apache server or as an ISAPI module on an IIS server. The script shown in Listing 6.14 will run on both servers.

Code Listing 6.14. http.php — enable basic HTTP authentication by MEANS of PHP.

<?

if you are using an iis server you will need to set variables

$PHP_AUTH_USER and $PHP_AUTH_PW environments

if (substr($SERVER_SOFTWARE, 0, 9) == "Microsoft" &&

!isset($PHP_AUTH_USER) && !isset($PHP_AUTH_PW) &&

substr($HTTP_AUTHORIZATION, 0, 6) == "Basic" )

{ list($PHP_AUTH_USER, $PHP_AUTH_PW) =

explode(":", base64_decode(substr($HTTP_AUTHORIZATION, 6))); }

replace this if statement with a database query

if ($PHP_AUTH_USER != "user" || $PHP_AUTH_PW != "pass")

{

The visitor has not yet submitted the details or the name and password are incorrect

header('WWW-Authenticate: Basic realm="Realm-Name"');

if (substr($SERVER_SOFTWARE, 0, 9) == "Microsoft")

header("Status: 401 Unauthorized");

else header("HTTP/1.0 401 Unauthorized");

echo "<hl>Go Away!</hl>";

echo "You are not authorized to view this resource."; }

else {

the visitor provided the correct information

echo "<h1>Here it is!</hl>";

echo "<p>I bet you are glad you can see this secret page.";

}

?>

This code works the same way as the code in the previous listing. If the user has not submitted authentication data, he is issued an authentication request. If the user has provided incorrect information, an access-denied message is displayed for the user. If the information is correct, the user will see the contents of the page.

The interface of this example is different from that of the previous examples. We do not create an HTML form to enter a user name and password. The authentication dialog box will display the user's browser. Some see this as an improvement, others prefer to have full control over the visual aspects of the interface.

Since the built-in capabilities of browsers are used for authentication, the latter demonstrate some caution in handling failed authentication attempts. Internet Explorer gives the user three authentication attempts, and if all of them fail, it displays an access denied message. Netscape Navigator provides an unlimited number of attempts, but displays a retry dialog box requesting "Authentication Failed. Retry?". Netscape displays an access denied message only when the user clicks the Cancel button.

Like the code in Listings 6.11 and 6.12, you can insert this example code at the beginning of each file that you want to protect. You can do this manually or automatically for each file in the directory.