Authenticating Users in PHPThroughout this chapter, we will always create a sample application where a certain directory must be protected using a username and a password. There are three approaches to accomplishing this task:
The required files for these secured sections of the website will be put in directories protected1, protected2, and protected3, respectively. Why?Creating a simple user authentication is fairly easyjust let the user provide you with a username and password. If that matches the correct values, the "secret information" is unveiled, as shown in Listing 11.1: Listing 11.1. A Simple User Authentication Script<html> <head> <title>User Authentication</title> </head> <body> <?php if (isset($_POST["user"]) && isset($_POST["pass"]) && strtolower($_POST["user"]) == "shelley" && $_POST["pass"] == "deadline") { ?> Welcome! Here is the truth about the JFK assassination ... <?php } else { ?> Please log in! <form method="post"> User name: <input type="text" name="user" /><br /> Password: <input type="password" name="pass" /><br /> <input type="submit" name="Login" /> </form> <?php } ?> </body> </html> Most of the other authentication schemes work this way. But why is the preceding code not suitable for most websites? This way, you can protect only one page at a time, making the use for it rather limited. Most of the other protection mechanisms work for whole directories. Using HTTP Authentication with ApacheThe Apache Web server offers access control to the website using a file called .htaccess. In this file, you can provide information about who may access the website (or the current directory and its subdirectories, if you put the file in a subdirectory of the Web server), among other things. The file .htaccess is a text file where you can provide a number of configuration options. First, you have to provide a name for the restricted area: AuthName "PHP 5 Unleashed Protected Area" Also, the type of authentication must be provided; in this chapter, we chose Basic: AuthType Basic
Furthermore, you have to tell Apache where the file with user credentials (name, password) is: AuthUserFile /path/to/users We will cover this users file in a minute. Also, you need to tell Apache which users are allowed on your website. A good start is to allow all users that are defined in the users file. require valid-user This concludes the file .htaccess; Listing 11.2 is the complete code: Listing 11.2. An .htaccess FileAuthName "PHP 5 Unleashed Protected Area" AuthType Basic require valid-user AuthUserFile /path/to/users In the next step, you have to create a users file. This contains lines that look like these: christian:$apr1$xl......$QTjbmvK.a9Qj8kIQAu3Bf. john:$apr1$Om......$Myf3rygKopxZfP7gVlC9o/ shelley:$apr1$fm......$WN0gyiNlFrsKgqSJrwdr4. Each line contains a username and an associated, encrypted password, separated by a colon. But don't worry, you do not have to do this encryption by yourself. With Apache comes a useful tool called htpasswd that creates this password file (available even in Windows, in the bin subdirectory, whereas on Linux systems, it most often resides in /usr/local/apache/bin or wherever the Apache binaries are stored). The syntax for htpasswd is this: htpasswd [options] password_file username [password] More detailed information about this program is available when you call htpasswd without parameters. But for now, it is important to know that the switch c creates a new users file. If you omit this parameter, a new user is added to an existing file. The switch m uses the MD5 format (which is standard on the Windows platform, by the way). Here is a protocol for adding three users to a new user file: > htpasswd m c ../users.txt christian New password: ***** Re-type new password: ***** Adding password for user christian > htpasswd m ../users.txt john New password: ***** Re-type new password: ***** Adding password for user john > htpasswd m ../users.txt shelley New password: ***** Re-type new password: ***** Adding password for user shelley Now place all files in the protected1 directory, the users file to the directory you provided in your .htaccess file. Try to access a document within protected1; your Web browser will ask you for a username and a password. If not, you have to tell Apache to search and parse .htaccess files. Replace AllowOverride None with AllowOverride AuthConfig in httpd.conf and restart your Web server. Figure 11.1 shows the browser's prompt for the user credentials. Figure 11.1. The user is prompted for a name and a password.The .htaccess way works well, but has two major flaws:
Using HTTP AuthenticationThe title of this section is somewhat misleadingwe were using HTTP authentication in the previous section, as wellalthough with a little help from the .htaccess file. In this section, we will use a similar mechanism, but we won't rely on clumsy user files and .htaccess settings. This time, we will check the usernames and passwords within the PHP code. To do so, you have to send some special HTTP headers to the Web browser: header("WWW-Authenticate: Basic realm=\"PHP 5 Unleashed Protected Area\""); header("HTTP/1.0 401 Unauthorized"); The 401 HTTP status code stands for "Not Authorized"; most Web browsers then open up a modal window where the user can enter a name and password. Depending on the browser type, this can be done an infinite number of times (Netscape browsers) or three times until an error page is displayed (Internet Explorer). For the next examples to work, PHP must be run as a module, not in CGI mode. CGI mode will be covered later in this section. The $_SERVER array contains the values PHP_AUTH_USER and PHP_AUTH_PW, which contain the username and the password a user entered in the modal browser window. The following code snippet checks whether $_SERVER["PHP_AUTH_USER"] is set; if so, the username and password are printed out. If not, header entries are sent so that the user is prompted to provide a username and a password, as shown in Listing 11.3 (the page after a successful login is depicted in Figure 11.2): Listing 11.3. Username and Password Are Printed Out<?php if (isset($_SERVER["PHP_AUTH_USER"])) { echo("Username / password: "); echo(htmlspecialchars($_SERVER["PHP_AUTH_USER"]) . " / " . htmlspecialchars($_SERVER["PHP_AUTH_PW"])); } else { header("WWW-Authenticate: Basic realm=\"PHP 5 Unleashed Protected Area\""); header("HTTP/1.0 401 Unauthorized"); } ?> Figure 11.2. The user is now logged in.However, when you try to run this script in IIS, you will get into an endless loopyou are always prompted for your password; however, it is not shown in the Web browser. This is neglected by the majority of literature on PHP. Listing 11.4 shows a different version of the script. This time, a server variable HTTP_AUTHORIZATION is checked and printed, if available: Listing 11.4. This Script Is Tailored for Microsoft's IIS<?php if (isset($_SERVER["HTTP_AUTHORIZATION"]) && substr($_SERVER["HTTP_AUTHORIZATION"], 0, 6) == "Basic") { echo("HTTP_AUTHORIZATION: " . htmlspecialchars($_SERVER["HTTP_AUTHORIZATION"])); } else { header("WWW-Authenticate: Basic realm=\"PHP 5 Unleashed Protected Area\""); header("HTTP/1.0 401 Unauthorized"); } ?> You are again prompted for a username and password. After that, the content of the HTTP_AUTHORIZATION variable is displayed (see Figure 11.3). Figure 11.3. The value of HTTP_AUTHORIZATION (on IIS).
You see that the value of HTTP_AUTHORIZATION starts with Basic, then a blank, then some garbage. However, if you look closely, you see that the characters after Basic could be Base64-encoded text. Thus, change the previous listing to the code shown in Listing 11.5 (the result can be seen in Figure 11.5): Listing 11.5. Using base64_decode(), the User Data Is Readable<?php if (isset($_SERVER["HTTP_AUTHORIZATION"]) && substr($_SERVER["HTTP_AUTHORIZATION"], 0, 6) == "Basic") { echo("HTTP_AUTHORIZATION: Basic " . htmlspecialchars(base64_decode( substr($_SERVER["HTTP_AUTHORIZATION"], 6)))); } else { header("WWW-Authenticate: Basic realm=\"PHP 5 Unleashed Protected Area\""); header("HTTP/1.0 401 Unauthorized"); } ?> Figure 11.5. Now the username and the password are readable.As can be seen from Figure 11.5, the content of HTTP_AUTHORIZATION (after Basic) has the following structure, if Base64-decoded:
USERNAME:PASSWORD
Thus, to retrieve the username and password for both Apache and IIS, the following code (see Listing 11.6) comes in handy: Listing 11.6. The Username and Password Are Retrieved for Both Apache and IIS<?php if (isset($_SERVER["PHP_AUTH_USER"])) { $user = $_SERVER["PHP_AUTH_USER"]; $pass = $_SERVER["PHP_AUTH_PW"]; } elseif (isset($_SERVER["HTTP_AUTHORIZATION"])) { if (substr($_SERVER["HTTP_AUTHORIZATION"], 0, 5) == "Basic") { $userpass = split(":", base64_decode(substr($_SERVER["HTTP_AUTHORIZATION"], 6))); $user = $userpass[0]; $pass = $userpass[1]; } } if (isset($user)) { echo("Username / password: "); echo(htmlspecialchars($user) . " / " . htmlspecialchars($pass)); } else { header("WWW-Authenticate: Basic realm=\"PHP 5 Unleashed Protected Area\""); header("HTTP/1.0 401 Unauthorized"); } ?> Using Static Usernames and PasswordsUsing this code as a basis, HTTP authentication can be implemented rather easily. In Listing 11.7, the secret area is protected using one username/password combination: php5/iscool. Listing 11.7. Only One Username and Password Is Valid<?php if (isset($_SERVER["PHP_AUTH_USER"])) { $user = $_SERVER["PHP_AUTH_USER"]; $pass = $_SERVER["PHP_AUTH_PW"]; } elseif (isset($_SERVER["HTTP_AUTHORIZATION"])) { if (substr($_SERVER["HTTP_AUTHORIZATION"], 0, 5) == "Basic") { $userpass = split(":", base64_decode(substr($_SERVER["HTTP_AUTHORIZATION"], 6))); $user = $userpass[0]; $pass = $userpass[1]; } } if (!isset($user) || !isset($pass) || $user!="php5" || $pass!="iscool") { header("WWW-Authenticate: Basic realm=\"PHP 5 Unleashed Protected Area\""); header("HTTP/1.0 401 Unauthorized"); } else { echo("Welcome, $user!"); } ?> Only if the user enters the right credentials, the 401 Unauthorized HTTP header is not sent out to the client. To protect a site, just include the preceding file in all pages you want to secure.
Of course, this code can be easily extended. For instance, you could have a whole list of valid usernames and passwords. Imagine a file where usernames and (encrypted) passwords are stored in this format: username:encrypted_pw The encryption is done using PHP's crypt() function. As a first parameter, the password is submitted; as a second parameter, we use the string constant "pw". The following PHP script (see Listing 11.8 and Figure 11.6) lets the administrator enter a username and a password and writes the associated entry into a password file, using crypt(): Listing 11.8. Passwords Are Encrypted and Saved in a File<html> <head> <title>User Authentication</title> </head> <body> <?php if (isset($_POST["user"]) && isset($_POST["pass"])) { $pwfile = fopen("users.txt", "a"); fputs($pwfile, $_POST["user"] . ":" . crypt($_POST["pass"], "pw") . "\n"); fclose($pwfile); ?> user <?php echo htmlspecialchars($_POST["user"]) . ":" . crypt($_POST["pass"], "pw"); ?> added. <?php } ?> <form method="post"> User: <input type="text" name="user" /><br /> Password: <input type="password" name="pass" /><br /> <input type="submit" value="Encrypt!" /> </form> </body> </html> Figure 11.6. Users can be added to the users.txt file.As soon as some users are added, it is time to create a script that checks whether a given username and password exist in that filethat is, if the user is known to the system. To do so, the username and password provided using HTTP authentication is retrieved as shown earlier. After that, the user file is parsed for this username/password combo. If successful, the user is granted access. Listing 11.9 shows the complete code, which works on both Apache and IIS. Listing 11.9. Usernames and Passwords Are Checked Against Data in a File<?php if (isset($_SERVER["PHP_AUTH_USER"])) { $user = $_SERVER["PHP_AUTH_USER"]; $pass = $_SERVER["PHP_AUTH_PW"]; } elseif (isset($_SERVER["HTTP_AUTHORIZATION"])) { if (substr($_SERVER["HTTP_AUTHORIZATION"], 0, 5) == "Basic") { $userpass = split(":", base64_decode(substr($_SERVER["HTTP_AUTHORIZATION"], 6))); $user = $userpass[0]; $pass = $userpass[1]; } } $auth = false; $pwfile = fopen("users.txt", "r"); while (!feof($pwfile)) { $data = split(":", rtrim(fgets($pwfile, 1024))); if ($user == $data[0] && crypt($pass, "pw") == $data[1]) { $auth = true; break; } } fclose($pwfile); if (!$auth) { header("WWW-Authenticate: Basic realm=\"PHP 5 Unleashed Protected Area\""); header("HTTP/1.0 401 Unauthorized"); } else { echo("Welcome, $user!"); } ?>
Using Names and Passwords from a DatabaseThe more users you get, the less performability this file-based solution will have. After some time, you will want to use a database to save user information. Again, two scripts are generated. First, the PHP page in Listing 11.10 lets you add users to the database. The database is called auth; it contains a table users with at least two fields, user and pass, both VARCHAR(255). Listing 11.10. Passwords Are Encrypted and Saved in a Database<html> <head> <title>User Authentication</title> </head> <body> <?php if (isset($_POST["user"]) && isset($_POST["pass"])) { $pwdb = mysql_connect("localhost", "user", "pwd"); mysql_select_db("auth", $pwdb); mysql_query("INSERT INTO users (user, pass) VALUES ('" . $_POST["user"] . "', '" . crypt($_POST["pass"], "pw") . "')", $pwdb); ?> user <?php echo htmlspecialchars($_POST["user"]) . ":" . crypt($_POST["pass"], "pw"); ?> added. <?php } ?> <form method="post"> User: <input type="text" name="user" /><br /> Password: <input type="password" name="pass" /><br /> <input type="submit" name="Encrypt!" /> </form> </body> </html> The script to check submitted username/password combos is similar to the previous, file-based example; however, this time, the information is retrieved from the MySQL data source, as shown in Listing 11.11: Listing 11.11. Usernames and Passwords Are Checked Against Data in a Database<?php if (isset($_SERVER["PHP_AUTH_USER"])) { $user = $_SERVER["PHP_AUTH_USER"]; $pass = $_SERVER["PHP_AUTH_PW"]; } elseif (isset($_SERVER["HTTP_AUTHORIZATION"])) { if (substr($_SERVER["HTTP_AUTHORIZATION"], 0, 5) == "Basic") { $userpass = split(":", base64_decode(substr($_SERVER["HTTP_AUTHORIZATION"], 6))); $user = $userpass[0]; $pass = $userpass[1]; } } $auth = false; $pwdb = mysql_connect("localhost", "user", "pwd"); mysql_select_db("auth", $pwdb); $rows = mysql_query("SELECT user, pass FROM users", $pwdb); while ($row = mysql_fetch_array($rows)) { if ($user == $row["user"] && crypt($pass, "pw") == $row["pass"]) { $auth = true; break; } } if (!$auth) { header("WWW-Authenticate: Basic realm=\"PHP 5 Unleashed Protected Area\""); header("HTTP/1.0 401 Unauthorized"); } ?> The main advantage of this solution is that now you do not have to worry about things such as file locking and parallel access to the file users.txtthe database does this automatically for you. Lean back, relax, and let your users authenticate themselves.
Using PHP SessionsAll previously presented methods have two major flaws:
One thing that always works is the use of PHP sessions. The information about whether a user is authenticated is saved in a session variable. Thanks to PHP's session management, this information is then available on all pages of the Web application.
Before you start, check whether all session-related information in php.ini is set:
Again, we start using a simple example where only one username/password combination is valid. The session variable username will contain the username of the currently logged-in user. If this variable does not exist, the user is not logged in. On the other hand, if the variable does exist, the user has successfully logged in. If the session variable does not exist, a form is presented where the user can input a name and the associated password: <form method="post"> <input type="text" name="user" /><br /> <input type="password" name="pass" /><br /> <input type="submit" name="submit" value="Login" /> </form> After the user submits the HTML form, the name and password are checked. If the credentials are okay, the user is logged in. You must not forget that you have to set a session variable to save this "logged-in" status. Listing 11.12 contains the complete code for the login page. Listing 11.12. A Simple Login Page<?php session_start(); if (isset($_POST["submit"])) { if ($_POST["user"] == "php5" && $_POST["pass"] == "iscool") { $_SESSION["username"] = $_POST["user"]; } } ?> <html> <head> <title>User Authentication</title> </head> <body> <?php if (isset($_SESSION["username"])) { echo("You are logged in!"); } else { ?> <form method="post"> <input type="text" name="user" /><br /> <input type="password" name="pass" /><br /> <input type="submit" name="submit" value="Login" /> </form> <?php } ?> </body> </html> This works well; however, to modularize the whole login process, the user should be redirected after successfully logging inbut where? This is where another nifty trick comes in. When linking to the login form, we submit the following as part of the URL where the user came from: http://servername/login.php?url=/path/to/origin.php. If this value is not set, however, the user is redirected to a file index.php. Unfortunately, this simple approach creates some difficulties, depending on your PHP configuration. If you set PHP to not use cookies and/or if the user does not accept cookies for the sessions, you have to manually add the session information to the URL. You need two PHP functions for that:
This leads to three cases:
This leads to the following code: if (!isset($_COOKIE[session_name()])) { if (strstr($url, "?")) { header("Location: " . $url . "&" . session_name() . "=" . session_id()); } else { header("Location: " . $url . "?" . session_name() . "=" . session_id()); } } else { header("Location: " . $url); } The rest of the code is standard procedure: An HTML form accepts a username and password. Upon submitting this form, this information is checked against "php5"/"iscool". Upon success, the redirection URL is determined. Either, $_GET["src"] is set, or the standard value, "index.php", is used. Listing 11.13 is the complete code for the login page: Listing 11.13. A More Sophisticated Login Page<?php session_start(); if (isset($_POST["submit"])) { if ($_POST["user"] == "php5" && $_POST["pass"] == "iscool") { $_SESSION["username"] = $_POST["user"]; if (isset($_GET["url"])) { $url = $_GET["url"]; } else { $url = "index.php"; } if (!isset($_COOKIE[session_name()])) { if (strstr($url, "?")) { header("Location: " . $url . "&" . session_name() . "=" . session_id()); } else { header("Location: " . $url . "?" . session_name() . "=" . session_id()); } } else { header("Location: " . $url); } } } ?> <html> <head> <title>User Authentication</title> </head> <body> <form method="post"> <input type="text" name="user" /><br /> <input type="password" name="pass" /><br /> <input type="submit" name="submit" value="Login" /> </form> </body> </html> Finally, you need code that checks for the session information. If $_SESSION["username"] is set, no action is required. If, however, the user is not logged in, the user must be redirected to the login page. The name of the current script ($_SERVER["SCRIPT_NAME"]) is sent to this script as a GET URL variable. Listing 11.14 is the code. Figure 11.7 shows the result in a Web browser: Listing 11.14. If a User Is Not Logged in, the Login Form Is Loaded<?php session_start(); if (!isset($_SESSION["username"])) { header("Location: /protected3/login.php?url=" . urlencode($_SERVER["SCRIPT_NAME"])); } ?> Figure 11.7. The login pagethe referring page is seen as part of the URL.
To use it, you have two possibilities:
Using Static Usernames and PasswordsThis scheme can now be applied to the other two password management approaches. First, we use the text file where all usernames and associated (crypt()-encrypted) passwords are stored. The source code for adding users to this file does not change in comparison to the code from the previous section, because the file format doesn't change. What changes, however, is the code where this information is checked. This code logic will be included in the login.php file. If an associated entry is found in the user/password file, the session variable is set accordingly. Listing 11.15 is the complete code for this page: Listing 11.15. Login Information Is Read from a File<?php session_start(); if (isset($_POST["submit"])) { $user = $_POST["user"]; $pass = $_POST["pass"]; $auth = false; $pwfile = fopen("users.txt", "r"); while (!feof($pwfile)) { $data = split(":", rtrim(fgets($pwfile, 1024))); if ($user == $data[0] && crypt($pass, "pw") == $data[1]) { $auth = true; break; } } fclose($pwfile); if ($auth) { $_SESSION["username"] = $user; if (isset($_GET["url"])) { $url = $_GET["url"]; } else { $url = "index.php"; } if (!isset($_COOKIE[session_name()])) { if (strstr($url, "?")) { header("Location: " . $url . "&" . session_name() . "=" . session_id()); } else { header("Location: " . $url . "?" . session_name() . "=" . session_id()); } } else { header("Location: " . $url); } } } ?> <html> <head> <title>User Authentication</title> </head> <body> <form method="post"> <input type="text" name="user" /><br /> <input type="password" name="pass" /><br /> <input type="submit" name="submit" value="Login" /> </form> </body> </html> Using Usernames and Passwords from a DatabaseIf you have MySQL at hand, you can and you should store your users in the database. This makes handling users (including adding, modifying, and even deleting) much easier.
The database structure was explained earlierbasically, the columns user and pass contain the username and the associated password, the latter one encrypted using PHP's crypt() function. This code snippet reads out this information and compares it to the provided username and password: $user = $_POST["user"]; $pass = $_POST["pass"]; $auth = false; $pwdb = mysql_connect("localhost", "user", "pwd"); mysql_select_db("auth", $pwdb); $rows = mysql_query("SELECT user, pass FROM users", $pwdb); while ($row = mysql_fetch_array($rows)) { if ($user == $row["user"] && crypt($pass, "pw") == $row["pass"]) { $auth = true; break; } } The rest of the code is the same as before. The session variable is set; then the user is redirected. If necessary, the name and ID of the current PHP session are manually appended to the URL. Listing 11.16 is the complete code for the MySQL-driven login page: Listing 11.16. Login Information Is Loaded from a Database<?php session_start(); if (isset($_POST["submit"])) { $user = $_POST["user"]; $pass = $_POST["pass"]; $auth = false; $pwdb = mysql_connect("localhost", "user", "pwd"); mysql_select_db("auth", $pwdb); $rows = mysql_query("SELECT user, pass FROM users", $pwdb); while ($row = mysql_fetch_array($rows)) { if ($user == $row["user"] && crypt($pass, "pw") == $row["pass"]) { $auth = true; break; } } if ($auth) { $_SESSION["username"] = $user; if (isset($_GET["url"])) { $url = $_GET["url"]; } else { $url = "index.php"; } if (!isset($_COOKIE[session_name()])) { if (strstr($url, "?")) { header("Location: " . $url . "&" . session_name() . "=" . session_id()); } else { header("Location: " . $url . "?" . session_name() . "=" . session_id()); } } else { header("Location: " . $url); } } } ?> <html> <head> <title>User Authentication</title> </head> <body> <form method="post"> <input type="text" name="user" /><br /> <input type="password" name="pass" /><br /> <input type="submit" name="submit" value="Login" /> </form> </body> </html> |