FeatureUSENIX

 

the webmaster

Password-Protecting Areas of Your Web Site

taylor_dave

by Dave Taylor
<[email protected]>

Dave Taylor is president of strategic consultancy Intuitive Systems, nestled in the Santa Cruz mountains above Silicon Valley. He has been involved with UNIX since 1980. His latest book, Teach Yourself UNIX in 24 Hours, can be previewed at <www.intuitive.com/tyu24/>..


Recently I was talking with a client about restructuring their Web site to make quite a bit of the material off-limits to the random public, areas with sensitive information that only their partners would be able to explore. The obvious solution was to employ a password-protection scheme, but there are a number of ways to approach this, and I thought it'd be interesting to share my thinking and results with the ;login: crowd.

Password Protection, Take One

The first and perhaps most obvious way to do this is to replace a page of information with a login FORM on a Web page, then feed what the user entered to a CGI script that compares the password against the official password for that area of the site.

The HTML for the form might look like:

<FORM ACTION=login.cgi METHOD=post>
Enter Site Password:
<INPUT TYPE=password NAME=pw >
<INPUT TYPE=submit VALUE="log in">
</FORM>

Then, if we're going to utilize a Perl solution and have something like the simple cgi-lib.pl library from Steven Brenner (you can get a copy for yourself from your local CPAN archive), the script underlying this is as simple as:

#!/usr/bin/perl
push (@INC, '/cgi-bin');
require ('cgi-lib.pl');
$official = "unix"; # official area password
&ReadParse;   # read and parse arguments
$pass="$in{'pw'}";
if ( $pass eq $official ) {
 print "Location: logged-in.html\n\n";
}
else {
 print "Content-type: text/html\n\n";
 print "Password Failed.";
}
exit 0;

Enter the correct password ("unix", as defined in the fourth line of the Perl script) and you're in: The next page you'll see is "logged-in.html".

This first version works, but it's pretty darn rudimentary and has some drawbacks, not the least of which is that everyone has the same password. Obviously, without account names to go with it, this is security only in the most minimal of senses.

There's another problem here too, one that's a bit more subtle: Once you've logged in, the URLs you're seeing in your Web browser are post-login URLs. If you were to send, say, the real-life URL of the page we're talking about here -- <http://www.intuitive.com/CGI/
password/logged-in.html> -- to your friends, they wouldn't even need to worry about the password because they'd have effectively skipped right past it.

You'd be surprised how many sites use this kind of mechanism for password protection. One password for everyone just isn't very good. Having a password facade can be good for some cases, but isn't very secure.

A Second Try

Before we leave this area, however, an example of where this kind of login might be useful is when you have individual accounts and want to display different information based on the type of account. In this case, the wrinkle is that you need to ask for both an account and password pair on the FORM (a trivial change), extract both in the CGI script, and then compare them against a file of defined account/password pairs. In this case, let's create a file "pwfile" that contains two lines

 taylor:unix:partner
 guest:guest:guest

that define the account name, password, and level of access granted.

The second version of the Perl login script is a bit more complex. First, the subroutine that does all the work, reading and parsing the account file:

sub Matches
{
 local ($given_name, $given_pass) = @_;
open(PASSWORDS,"pwfile") or die "can't open pwfile";
while ($line = <PASSWORDS>) {
 chomp($line);
($name, $pass, $accesslevel) = split(":", $line);
if ( $name eq $given_name) {
  if ( $pass eq $given_pass) {  #success!
   return $accesslevel;
  } else {
   return nil;  # wrong pw
  }
 }
}
 close(PASSWORDS);
 return nil;  # no match on acct
}

Here's where Perl is a winner: The split() routine automatically breaks up the line of information at the ':' separator, returning each of the three values into its own mnemonic variable. Then the conditional tests are easy to code.

The main program needs to be modified to take advantage of the level of access that we can now grant:

#!/usr/bin/perl
push (@INC, '/cgi-bin');
require ('cgi-lib.pl');
&ReadParse;   # read and parse arguments
$name="$in{'name'}";
$pass="$in{'pw'}";
if (($access = &Matches($name, $pass)) ne nil) {
 print "Location: $access.html\n\n";
}
else {
 print "Content-type: text/html\n\n";
 print "Account/Password pair failed.\n";
}
exit 0;

Do you see what's happening here? If user "taylor" logs in successfully, he'll be dropped onto the Web page "partner.html," whereas if the guest user logs in, she'll start out on the "guest.html" page.

This is a cool way to control access to a site and simultaneously offer multiple levels of access. It still suffers from some of the limitations of the earlier solution, of course. If you see "guest.html," you might well guess "partner.html" was another possible Web page, and poof, you're in!

A Third Solution: Let the Server Do the Work

Yet another solution, one that offers more security, is to let the Apache Web Server do the work through the .htpasswd facility. In essence, you create a simple password file containing names and encrypted passwords, then enable your Web server to look for the file. Once set up, any access to any files within the protected folder must automatically be validated by forcing the user to enter a login/password pair.

To create the password file, I use a simple Perl script that I cobbled together called makepasswd:

#!/usr/bin/perl
print "\nMake htpasswd Account Entry...\n\n";
print "User name : ";
chomp($user = <STDIN>);
print "Password : ";
chomp($passwd = <STDIN>);
srand($$|time);
@saltchars=(a..z,A..Z,0..9,'.','/');
$salt=$saltchars[int(rand($#saltchars+1))];
$salt.=$saltchars[int(rand($#saltchars+1))];
$passwdcrypt = crypt($passwd,$salt);
print "\nAdd the following to the htpasswd file:\n\n";
print "\t$user:$passwdcrypt\n\n";
exit 0;

As you can see, it does the work of encrypting the password and then displays exactly the information you'll need to add to the new .htpasswd file. A file duplicating the two accounts shown above would look like this:

taylor:z6K5hUINhVrIA
guest:SXUISuZuiD0Os

With the passwords encrypted, it's a lot harder for hackers to reverse-engineer and sneak in if they manage to snag a copy of this information!

The only other step is to change the httpd.conf file so that the server knows to look for the password file in the directory. This is done by adding the boldface lines below to the file:

<VirtualHost www.intuitive.com>
ServerAdmin [email protected]
DocumentRoot /web
ServerName www.intuitive.com
<Location /CGI/password/private>
AuthName /web/CGI/password/private
AuthType basic
AuthUserFile /web/CGI/password/private/.htpasswd
Require valid-user
Allow From All
</Location>
</VirtualHost>

Now we're rocking! Any access to any of the information in the specified folder (AuthName) requires the user to log in to the server correctly, with the dialog box (as shown in Figure 1) popped up and the account/password information compared against the contents of the AuthUserFile as shown above.

Figure 1


Figure 1

This is very cool and professional looking, being able to hide your information behind this kind of password protection. The downside is that you don't get much control over the presentation and appearance of the box, whereas in the previous approaches you could build a login page that looked very consistent with the rest of the site's appearance.

Merging These Together

Unfortunately, there's no easy way to include additional fields in the htpasswd file data (which would be ideal), so instead you're stuck having either to give out different password-protected URLs for different classes of users (for example, yourhost.com/partners/ and yourhost.com/guest/) or try a hybrid solution.

Let's talk about the latter for a sec. It turns out that once you've logged in to a Web server with the .htpasswd-prompted solution, for the duration of that session you now have an additional environment variable that you're carrying around with you: REMOTE_USER. It'll contain the name half of the name/password information required to log in.

With that in your toolbox, you could then have an index.cgi script, for example, that looks up the user in a second access-level file (keyed on the REMOTE_USER information), then presents a page based on that information. It's a simple subset of what we've already seen; we don't even need to worry about any CGI argument parsing.

The first part is the replacement for the match routine:

sub AccessLevel
{
 local ($given_name) = @_;
 open(PASSWORDS,"../pwfile") or die "can't open pwfile";
 while ($line = <PASSWORDS>) {
  chomp($line);
  ($name, $pass, $accesslevel) = split(":", $line);
  if ( $name eq $given_name) {
   return $accesslevel;
  }
 }
 close(PASSWORDS);
 return "guest"; # default access
}

If there isn't a match in the file, it returns "guest" as the access level. Notice that I'm using the same file from the previous examples; it's living one level up on the filesystem (../pwfile) but otherwise it's as you've already seen.

Finally, here's the simple snippet that's the heart of the switch CGI script:

$name=$ENV{"REMOTE_USER"};
$access=&AccessLevel($name);
print "Location: $access.html\n\n";

The variable REMOTE_USER contains the login name of the person who successfully signed in to the restricted area. If I just signed in as "taylor" (with the password "unix") then REMOTE_USER would be set to "taylor" automatically.

Summary

There's no perfect, graceful solution to password-protecting an area of a Web site with complete control, but this does give you a good idea of the different types of solutions and their trade-offs.

You can try out all these different solutions online and experience them for yourself: <http://www.intuitive.com/CGI/password/>.

 

?Need help? Use our Contacts page.
First posted: 14 Apr. 1999 jr
Last changed: 14 Apr. 1999 jr
Issue index
;login: index
USENIX home