Developer

Building a dynamic Web site, part 6: User logins and authentication

After rounding out the news subsystem of our Web site in part five of this Drill Down series, Vincent starts part six by implementing a user login and authentication system.

Welcome to part six of Vincent Danen’s Building a dynamic Web site series! Previously, Vincent decided to use Apache, MySQL, PHP4, and Perl as the building blocks of the project. He then explained how to install and configure these applications and built debug pages to help with site development. He also showed how to design the database for Web site news and built the administrative page to add entries to the database via the Web site. In part five, he explained how to create the page that retrieves information from the database and displays it for users to view. If you’ve missed any of the previous parts to this series, check them out:


Since we rounded out the news subsystem of our site in part five, I’ll begin this part by implementing a user login and authentication system. However, I’ll assume the same configuration and system information as our previous articles. This means you’ll continue working on your dynamic Web site using Apache, MySQL, PHP4, and Perl. By implementing the user login and authentication system, you can make less use of the less portable .htaccess method of securing pages on our site. This technique will also come in handy if you want to allow users to log in to the site and customize its appearance based upon their own preferences.

Creating the login system
The first thing you need to do is write your administration page. This is a simple index page that will prompt the user for a username and password. You can place this page anywhere you like, but I recommend using an /admin subdirectory for all administrative functions. By creating an index page (i.e. index.php), you ensure that anyone who selects http://www.yoursite.com/ won’t get a listing of all files in the directory. Obviously, this is preferred to using something obscure like http://www.yoursite.com/ as the login page because without an index.php, your files are open for the public to view. The only way you can work around this situation is to secure the directory with .htaccess from the beginning or if you configure Apache, to use login.php as an index page.

The simplest method is just using index.php. There’s nothing wrong with this if you properly set up your login interface. Place the following into your index.php file in your administrative directory:
<form method="post" action="menu.php">
<table width="100%" cellpadding="1" cellspacing="0" border="0">
<tr>
 <td width="50%">
 Please enter your user ID:
 </td>
 <td width="50%">
 <input type="text" name="userid" size="40" maxlength="40">
 </td>
</tr>
<tr>
 <td width="50%">
 Please enter your password:
 </td>
 <td width="50%">
 <input type="password" name="password" size="10" maxlength="10">
 <br /><br />
 </td>
</tr>
</table>
<input type="submit" value="Proceed">
</form>


This code creates a simple form that asks for a user ID and password and presents one button named Proceed. If the user types in his username and password, clicking the Proceed button will bring him to the file menu.php.

Now, before you create menu.php, create a file called .user.php. Notice the period before the filename. This character creates a hidden file that is not visible on the command line without explicitly viewing hidden files. Why make this file hidden? Well, it will contain essential user information, and there’s no need to make it any easier than necessary for someone to view it.

One quick note here: If you decide to implement user logins for all users to your site, this method is probably not the one you want to use. This method is useful only if you have a handful of administrators who’ll be accessing the back-end of your site. Placing a few thousand users into a text file is not an efficient use of resources. You’d get much better results using MySQL to store this kind of information. If this is the case, you may ask why we’d even put the administrators into a text file. There’s one simple reason, even if it isn’t a good one: It will teach you some different uses of PHP and will also illustrate a fast and easy way to get something done.

The .user.php file will look something like this:
<?
 switch ($userid) {
 case "vdanen":
  $name = "Vincent Danen";
  $id = "1";
  $pass = "secret";
  break;
 case "jim":
  $name = "Jim User";
  $id = "1";
  $pass = "passwd";
  break;
 default:
  // invalid user id
  $id = "0";
 }
?>


Basically, you’re writing a simple switch for the $userid variable that’s filled in by the login form in index.php. You use the switch() function to compare the value of $userid to a few different cases. Here, you’re comparing the value of $userid to two cases, vdanen and jim. If $userid is equal to vdanen, you set the value of $id to 1, which means valid user, and you also set vdanen’s password, in this case, secret. The same thing applies to user jim and any other cases you may wish to add. You also create the default case, which assumes the user ID is invalid and sets the value of $id to 0, or invalid user. You make this the default so every username is considered invalid unless it matches a specified username.

The only security flaw in this method is the passwords. The passwords are stored in clear text, which means anyone with permission to the file can read the user passwords. You can further secure this file by making it owned by user root and giving it a permission of 600, or owner read/write only. If you’re unable to make the file owned by root, you should give it a permission of 600 anyway. In that case, the only user on the system who can read the file is root.

Encrypted passwords
The other option is to encrypt the passwords. Personal Home Page (PHP) includes a function called crypt(), which will encrypt passwords using the standard UNIX DES encryption used in the Linux /etc/passwd file. Obviously, this method is preferred. The syntax for the crypt() command is:
$encrypted_string = crypt($string, $salt);

The $salt part of the crypt() command is optional. This allows you to provide a two-character salt to the encryption, which will further randomize the encryption possibilities. The problem (or advantage) of the crypt() function is it’s only a one-way encryption. You cannot decrypt the password by any means. This makes it very secure, but you’d best not forget your password!

The first thing you need to do is select a salt. Since DES supports only a two-character salt, you need to select a salt based on the following characters: a-zA-Z0-9./. For this example, I’ll use a salt value of 0A (the number zero and the letter A). Since there’s no command-line utility to give you the value of an encrypted password, you have to create a temporary PHP page that prints the encrypted password. Create a file called testpw.php that contains the following:
<?
 $string = crypt(?secret?, ?0A?);
 echo $string;
?>


Now, view this file with your Web browser. You’ll see a garbled string on the page. This is the value of the password. Then, rewrite .user.php with the encrypted passwords, shown by testpw.php, like this:
<?
 switch ($userid) {
 case "vdanen":
  $name = "Vincent Danen";
  $id = "1";
  $pass = "0AYvFSLYrLYug";
  break;
 case "jim":
  $name = "Jim User";
  $id = "1";
  $pass = "0AarrCB/p6VQg";
  break;
 default:
  // invalid user id
  $id = "0";
 }
?>


You now have a file that contains passwords that are indecipherable. Notice that because you chose the salt of 0A, the first two letters of each password contain those characters. This is true regardless of which salt you choose. You’ll need to use the testpw.php file to obtain the encrypted password for each one you use.

Creating the menu
Now that you have a more useable .user.php file, you can continue with your login system. Since you’ve already created the login form in index.php, you must now create the menu.php file, which will validate the users and present them a list of options for various administrative tasks. Your menu.php file will look something like this:
<?
 include "/home/httpd/html/admin/.user.php3";
 if ($id == 1) {
  $cpass = crypt($password, ?0A?);
  if ($cpass == $pass) {
 // display menu
?>
 <p>Welcome, <? echo $name; ?>.</p>
 <ul>
  <li> <a href="listnews.php?userid=<? echo $userid; ?>">
  List News Item</a></li>
  <li> <a href="dist.php?userid=<? echo $userid; ?>">
  Edit Distribution Information</a></li>
 </ul>
<?
  } else { ?>
 <p>You have entered an invalid password.</p>
<?
  }
 } else { ?>
 <p>You have entered an invalid user ID.</p>
<? } ?>


Let’s take a closer look at this code. The first thing you do is include the .user.php file you created. This will parse the value of $userid as passed from index.php and will assign a few values, notably the $password and $id variables.

Next, check to make sure $id is equal to 1. If it is, test the password entered. If it is not, a message stating that an invalid user ID was entered will be presented.

If the user ID is okay, check for the password. Since you’re using encrypted passwords, you must encrypt the password that was presented in the login and compare it to the value of the $pass variable as defined in .user.php. This is what the crypt() function does here. You encrypt $password (as entered in index.php) with the same salt you’ve been using, which is 0A in this case. You then assign the results to the variable $cpass and compare $cpass to $pass. If they’re identical, the user has entered a valid password, so the menu is displayed. If not, a message stating that an invalid password was entered is presented.

Finally, you have a list of links the user, upon successful authentication, can select from. The first item is to list news items. This list, as you saw in part five, will then present a list of news items on the system and provide links to each item. The second item on the list goes to another section of the site, in this case allowing the user to edit distribution information.

You’ll notice the links are written in a specific way. They both have userid= appended to the end of the link. This was done deliberately so you can track the user ID across pages. For example, when a user chooses to list the news items, the listnews.php page will receive the value of $userid as a parameter. This means you can reference the user ID in any page onto which you choose to pass that information. This can be used for logging, if you wish, or to make things easier for the user. For example, take all of the news items that contain an Author field. Previously, the author would have to type in his name when creating a new item. By passing $userid in this manner, you can modify the form so the user doesn’t need to enter his username in the Author field of the form. You can simply make reference to .user.php to obtain the user’s name and automatically insert it into the Author field in the database if he chooses to add a news item. This technique prevents users from entering a name other than their own and allows you to track individuals. By taking away a user’s ability to post an item as someone other than himself, you reduce the risk of him entering a news item he should not be entering.

Final Steps
The next step is to look at using MySQL to store user information. Not only will this be more efficient but you can also store more information about a user and allow that user to change it. With the text file method we illustrated, the administrator must change the password manually each time. By using MySQL and creating a form-based user management system, you can allow your users and/or administrators to change passwords and other items on their own, and you can store this information in a database. By using the crypt() function, you can still provide strong password security, but the advantage of MySQL is anyone who is in the system will need adequate permissions to even look in the MySQL database in the first place. This puts up one more stumbling block in front of anyone who may have malicious intentions for your site.

Also, by breaking your PHP files into smaller files and using the include() function to make them a part of the page, you offer another level of security: You can store important files in directories that are inaccessible to anyone but the user Apache runs (typically nobody). By using a judicious combination of filesystem permissions and ownership, as well as encryption, you can create a very secure system.

Conclusion
In this Daily Drill Down, I once again illustrated the power of PHP by implementing a user-based login and authentication system. What I’ve displayed here is relatively simple and is not ideal for public user-login systems employed across an entire Web site. Our technique is appropriate for an administrative system with a few members. Storing passwords in a plain-text file, whether they are encrypted or not, is not the best method, but it is useful. By keeping an eye on permissions, making the file hidden, and utilizing the crypt() function in PHP, we have made it pretty secure.

In the last Daily Drill Down of this series, we’ll look at using MySQL to provide authentication instead of the text file I used in this illustration. We’ll also examine an easier way of saving session information across your Web site than re-writing all URLs to pass the required information.

Vincent Danen, a native Canadian from Edmonton, Alberta, is an avid Linux "revolutionary" and a firm believer in the Open Source philosophy. He attempts to contribute to the Linux cause in as many ways as possible, from his Freezer Burn Web site to local advocacy in his hometown. Owner of a Linux consulting firm, Vincent is also the security updates manager for MandrakeSoft, creators of the Linux-Mandrake operating system. Vincent is a certified Linux Administrator by Tekmetrics.com.

The authors and editors have taken care in preparation of the content contained herein, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for any damages. Always have a verified backup before making any changes.

About Vincent Danen

Vincent Danen works on the Red Hat Security Response Team and lives in Canada. He has been writing about and developing on Linux for over 10 years and is a veteran Mac user.

Editor's Picks

Free Newsletters, In your Inbox