Developer

Building a dynamic Web site, part 7: User-driven Web sites

In the final installment of this series, Vincent Danen finishes the implementation of the user login and authentication system. He also takes a look at several ways of securing passwords for your user-driven Web site.

Welcome to part seven 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 explained how to install and configure these applications and built debug pages to help with site development. He showed how to design the database for Web site news, built the administrative page to add entries to the database via the Web site, and showed how to create the page that retrieves information from the database and displays it. In part six, he began to implement the user login and authentication system. If you’ve missed any of the previous parts to this series, check them out:


In this part, we will be continuing with the theme of part six—user logins, authentication, and building a Web site around your users.

We all know the popularity of the Web is not designer or programmer-driven. All of these neat technologies and languages were not written just because someone didn’t have anything else better to do. Designers demanded technology and languages that would help them build better and more interactive Web sites. Why? Because the average Joe wants to play all night on his favorite sites, check out new sites to add to his favorites, and interact with others online.

Because of this, more sites are being built around the concept of user configurability. This means a user logs in to a Web site and can change certain preferences, get some extra goodies by becoming a site member, and so on.

Let’s look at how to implement a user-driven Web site.

Create the database
In part six of this series, we looked at a rudimentary form of storing user information in a text file. This simply isn’t feasible for something on the scale of implementing a user-driven Web site. With the possibility of your site gaining thousands of members, using a text file for efficiency is beyond impractical—it’s impossible.

The first tool you need is a database in which to store this information. Using a database is ideal because it’s flexible and easily modified. You want your site to handle as much of this maintenance as possible. The last thing you want (or need) is to be manually sorting out thousands of user accounts.

As you have been using MySQL throughout this series, you’ll use it again here. The first thing you have to do is create the database. In part four of this series, you created a database called Website. Let’s continue with this database and make a new table called users. To do this, enter MySQL in command mode using:
mysql -u root -p

Once you are at the MySQL command prompt, change to the Website database by typing:
mysql> use Website;

Now that you are in the Website database, you want to create the new table called users. You’ll create a very rudimentary table for the time being. You’ll store the user’s alias, or nickname, real name, e-mail address, and password. You’ll also create another column entitled admin, which you can use to set an administrator flag. Finally, you’ll create a column called logins so you can keep track of the number of times the user has logged in to the Web site, and also keep track of the last date the user was on the system with the lastdate column. Use the following SQL command on the MySQL command line:
CREATE TABLE users (id INT (11) not null AUTO_INCREMENT,
username VARCHAR (15) not null ,
realname VARCHAR (40) not null ,
email VARCHAR (40) not null ,
password VARCHAR (15) not null ,
logins INT (10) not null DEFAULT ?0',
lastdate DATE,
admin SMALLINT (2) DEFAULT '0' not null ,
PRIMARY KEY (id));


What you’re basically doing here is creating the columns in your table and giving them a few names. Again, the id table is the primary table as it keeps all users unique from one another. The username is the nickname the user would enter, while everything else is relatively self-explanatory.

A random password generator in Perl
Now that you have your database created, you’re ready to populate it. The first thing you need to do is create a Perl program to generate a random password. I haven’t found an easy way to do this in PHP, so make use of Perl for this function. The idea is to let the user create a new account by asking him for three pieces of information: username, real name, and e-mail address. Once you have this information, you’ll generate a random password and e-mail it to the user. This accomplishes a few things. First, you generate a random password, which makes it difficult for others to guess. Whether the user decides to keep this password or not is entirely up to them; you will give them an option to change their password. However, this also ensures the user is using a valid e-mail address.

The Perl program to generate the random password is very small. Let’s call it randpw.pl and place it in the /admin directory:
#!/usr/bin/perl
@chars = (?a?..?z?, ?A?..?Z?, ?0'..?9');
for (1..8) {
    $password .= $chars[rand(62)];
}

print $password;

This generates a random password of eight characters that can either be upper or lowercase letters or numbers. Now, make this file executable by giving it 755 permissions. In order to use this in your PHP code, simply call it like this:
<?
    $temp = exec("./randpw.pl",$password);
?>


This little piece of PHP code will run the Perl program and take the output as the value for the array $password. In this instance, $temp is a dummy variable, and there is nothing to be done with it (thus the name). To use the generated password, you will have to subsequently use the variable $password[0], which is where the random password is stored.

Create the user account
You now have the components to build your first user entry. You obtain the user’s nickname, real name, and e-mail address from a form. You then generate a random password. After you have done this, two steps remain to setup the user account. First, you want to e-mail the password to the user. Second, you want to add a record for this user to your database. Let’s take a look at each of these steps separately. First, you’ll make the form for the user to create the user account. In the directory in which you store your HTML pages (I’ll assume /home/httpd/html), create a new page called newuser.php that contains:
<form method="post" action="newdone.php">
<table width="100%" cellpadding="1" cellspacing="0" border="0">
<tr>
    <td width="50%">
    Please enter the user name you wish to use:
    </td>
    <td width="50%">
    <input type="text" name="username" size="15" maxlength="15">
    </td>
</tr>
<tr>
    <td width="50%">
    Please enter your real name:
    </td>
    <td width="50%">
    <input type="text" name="realname" size="40" maxlength="40">
    </td>
</tr>
<tr>
    <td width="50%">
    Please enter your email address:
    </td>
    <td width="50%">
    <input type="text" name="email" size="40" maxlength="40">
    <br /><br />
    </td>
</tr>
</table>
<input type="submit" value="Proceed">
</form>


This page will ask the user for the username by which he wishes to be known, his real name, and e-mail address. When he clicks on the Proceed button, he will be taken to a new page called newdone.php. That page will look something like this:
<?
    // encryption functions (MySQL): we must define these first
    function encode($encode_str, $pass_str) {
      $data = mysql_query("select encode('$encode_str', '$pass_str')");
      $row = mysql_fetch_row($data);
      return $row[0];
    }

    function decode($decode_str, $pass_str) {
      $data = mysql_query("select decode('$decode_str', '$pass_str')");
      $row = mysql_fetch_row($data);
      return $row[0];
    }

    function password($string) {
      $data = mysql_query("select password('$string')");
      $row = mysql_fetch_row($data);
      return $row[0];
    }

    if ( $username && $email && realname ) {
      $date = date("Y-m-d");
      // generate random password
      $temp = exec("./randpw.pl",$password);
      $pemail = urlencode($email);
      $result = passthru("./newemail.pl $pemail $password[0]");
      mysql_pconnect("localhost","root","Daemon8")
       or die("Unable to connect to SQL server");
      // create special password based on username
      $pw = password($username);
      $encpw = encode($password[0], $pw);
      // perform the query
      $query = "INSERT INTO users ";
      $query .= "(id, username, realname, email, password, ";
      $query .= "logins, lastdate, admin) ";
     $query .= "values(00000000,'$username','$realname','$email','$encpw',";
      $query .= "0, '$date', 0)";
    mysql_select_db("freezer") or die("Unable to select database");
      $result = mysql_query($query);
?>
<p>An email message has been sent to <? echo $email; ?> with your password.
As soon as you receive that message,
you can then <a href="login.php">login</a>
with the supplied password.</p>
<?
    } else {
      // a field is missing
    echo "All required fields were not completed. Please go back.";
      exit;
    }
?>


This definitely needs a little explaining. The first thing you are doing in this page is defining a few encryption functions. Because the PHP crypt() function is only one way, you don’t want to make use of it here. The reason is if a user forgets his password, you want to be able to e-mail his password to the defined e-mail address. E-mailing an encrypted password will not do the user any good. This makes the crypt() function not very useful in this situation.

However, MySQL has some useful functions, namely the encode() and decode() functions. These will encrypt the provided string, which in your case will be the password. One note about using MySQL to encrypt and decrypt passwords: A connection must be made prior to using these SQL commands. If a connection is not open to the database, running the SQL queries to encode or decode anything will not work.

It might be simpler to put the functions you define at the beginning of the page into another file (perhaps one called encrypt-func.php), and simply include it in your page. This makes the functions a little more portable across pages.

The next thing you do is ensure that the user filled out the form entirely. If he has not included all three pieces of information, you give him a short note telling him to go back and fill out the form completely.

Get today’s date and store it in the variable $date. This gives you your last login date field. Then, generate a random password using the PHP exec() function. Because the exec() function only stores output into an array, tell it to store it in the $password array. The $temp variable is used simply to run the exec() function and contains nothing of interest. Then you have to create a new variable called $pemail which contains the user’s e-mail address in the standard URL encoding format, which you create with the urlencode() function. This function basically takes the @ symbol in the e-mail address and turns it into the %40 character sequence so that you can pass the e-mail address to the Perl script.

Then, you run a Perl script called newemail.pl in your /cgi-bin/ directory. This Perl script will send an e-mail message to the user that contains their password. The parameters we give it are the URL-encoded e-mail address and the first variable in the $password array (thus writing $password[0]). This variable contains the randomly generated password. The Perl script will look something like this:
#!/usr/bin/perl
use Mail::Sendmail;

$email = $ARGV[0];
$password = $ARGV[1];
# define sender's email address:
$from = "robot\@mydomain.com";

@email = split(/\%40/,$email);
$email = join("@",$email[0],$email[1]);

$Mail::Sendmail::mailcfg{mime} = 0;
%mail = (
    smtp  => "localhost",
    To   => $email,
    From  => $from,
);
$mail{"Subject: "} = "Welcome to Mydomain.com!";
$mail{"Message: "} .= "Welcome to Mydomain.com. Recently you chose\n";
$mail{"Message: "} .= "to register on our site. This is your\n";
$mail{"Message: "} .= "password: " . $password . "\n";
sendmail(%mail) or die $Mail::Sendmail::error;


You may want to make the message a little more informative or creative. For this to work, you must have the Mail::Sendmail Perl module installed. You can download and install it from CPAN. Basically, this Perl script will take the e-mail address and password for the user as arguments and will send an e-mail message to the user’s e-mail address, which contains the random (unencrypted) password. The e-mail will appear to come from www.cpan.org, in this case. Make the sender’s e-mail address either your own e-mail address or a forwarding e-mail address that points to an account you check often. The reason for this is if the e-mail bounces due to a user entering a bogus e-mail address, you want to know about it so you can delete their account. In this case, you would have mailto:robot@mydomain.com forward to your regular e-mail address.

Now, let’s get back to the newdone.php page. Now that you have sent the e-mail message, you will encrypt the password using the encode() function you created. At this point, you’ll save the newly encrypted password to the variable $encpw, which you will insert into the database instead of the unencrypted password. However, the first thing you need to do is create a seed for the password, which you’ll do using the password() function you created. Basically, you take the username the user supplied and transform it into a special password you use as the key to encrypt and decrypt the password. Without this key, you cannot encrypt or decrypt the password properly.

There is one thing to make note of here: Because the key is dependent upon the username, you cannot allow the user to change their username without re-generating the encrypted password. The reason for this is simple: If the username is vdanen and you run your password() function on it, you get a specific password returned, based on that username. The returned password is in turn the key you use to encrypt and decrypt the password. If the username were changed to something like linuxguru, your password() function now generates a different key based on the new username. That generated password is no longer the proper key to retrieve the password for that user. Instead of returning a properly decrypted password, it would return something entirely different and any subsequent logins will fail. What you, as an administrator, need to remember is if you allow the user to change his username, you must retrieve the encrypted password from the database and decrypt it so you have the unencrypted password available before saving the username. Run the password() function against the new username and then create a new encrypted password based on the return value of the password() function. At this point, you can insert the new username and the new encrypted password to the database at the same time.

After all of this is done, you print a brief message to the user telling him an e-mail is on the way. You’ll also supply a link to a page called login.php which is the page users will use to log in to the Web site.

Conclusion
We’ve done some heavy-duty programming today. Implementing a secure user authentication system is not easy. It is a difficult process that must be handled with some precision. Even the smallest error in the code can render your site inoperable as far as user logins are concerned. If there is one thing I will stress right now, it’s that you test and re-test your site many times under various conditions before making it public. There is nothing worse than trying to fix a live site that has gone wrong. Get all the bugs and issues worked out before you let your users at it. Inevitably, this will save everyone some grief.

With all the ways of securing passwords that we’ve looked at, there is one issue we haven’t mentioned which should be pretty obvious. All of the encryption and decryption is done server-side, which means it is all done on the server. This is helpful to prevent local attacks or people snooping about your system. This does not take into account that the password being entered from the user’s side is sent over the Internet in clear-text. The clear-text password sent from the user to the site is encrypted at the site’s end and compared to an encrypted password. This does you and your users no good if someone is packet sniffing and is able to obtain a clear-text password. You may also want to look into implementing SSL on your site to ensure that passwords and other traffic are encrypted by the user’s browser and decrypted by Apache. Please take a look at the article titled “E-commerce on the cheap: Apache and OpenSSL” for more information on how to use SSL to further secure your site.

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 siteto 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