home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Writing Apache Modules with Perl and C
By:   Lincoln Stein and Doug MacEachern
Published:   O'Reilly & Associates, Inc.  - March 1999

Copyright © 1999 by O'Reilly & Associates, Inc.


 


   Show Contents   Previous Page   Next Page

Chapter 6 - Authentication and Authorization
Authentication Handlers

In this section...

Introduction
A Simple Authentication Handler
An Anonymous Authentication Handler
Authenticating Against a Database

Introduction

   Show Contents   Go to Top   Previous Page   Next Page

Let's look at authentication handlers now. The authentication handler's job is to determine whether the user is who he or she claims to be, using whatever standards of proof your module chooses to apply. There are many exotic authentication technologies lurking in the wings, including smart cards, digital certificates, one-time passwords, and challenge/response authentication, but at the moment the types of authentication available to modules are limited at the browser side. Most browsers only know about the username and password system used by Basic authentication. You can design any authentication system you like, but it must ultimately rely on the user typing some information into the password dialog box. Fortunately there's a lot you can do within this restriction, as this section will show.

A Simple Authentication Handler

   Show Contents   Go to Top   Previous Page   Next Page

Example 6-5 implements Apache::AuthAny, a module that allows users to authenticate with any username and password at all. The purpose of this module is just to show the API for a Basic authentication handler.

Example 6-5. A Skeleton Authentication Handler

package Apache::AuthAny;
# file: Apache/AuthAny.pm
use strict;
use Apache::Constants qw(:common);
sub handler {
   my $r = shift;
    my($res, $sent_pw) = $r->get_basic_auth_pw;
   return $res if $res != OK;
    my $user = $r->connection->user;
   unless($user and $sent_pw) {
       $r->note_basic_auth_failure;
       $r->log_reason("Both a username and password must be provided", 
$r->filename); return AUTH_REQUIRED; }
    return OK;    
}
1;
__END__

The configuration file entry that goes with it might be:

<Location /protected>
 AuthName Test
 AuthType Basic
 PerlAuthenHandler Apache::AuthAny
 require valid-user
</Location>

For Basic authentication to work, protected locations must define a realm name with AuthName and specify an AuthType of Basic. In addition, in order to trigger Apache's authentication system, at least one require directive must be present. In this example, we specify a requirement of valid-user, which is usually used to indicate that any registered user is allowed access. Last but not least, the PerlAuthenHandler directive tells mod_perl which handler to call during the authentication phase, in this case Apache::AuthAny.

By the time the handler is called, Apache will have done most of the work in negotiating the HTTP Basic authentication protocol. It will have alerted the browser that authentication is required to access the page, and the browser will have prompted the user to enter his name and password. The handler needs only to recover these values and validate them.

It won't take long to walk through this short module:

package Apache::AuthAny;
# file: Apache/AuthAny.pm
use strict;
use Apache::Constants qw(:common);
sub handler {
   my $r = shift;
   my($res, $sent_pw) = $r->get_basic_auth_pw;

Apache::AuthAny starts off by importing the common result code constants. Upon entry its handler() subroutine immediately calls the Apache method get_basic_auth_pw(). This method returns two values: a result code and the password sent by the client. The result code will be one of the following:

OK

The browser agreed to authenticate using Basic authentication.

DECLINED

The requested URI is protected by a scheme other than Basic authentication, as defined by the AuthType configuration directive. In this case, the password field is invalid.

SERVER_ERROR

No realm is defined for the protected URI by the AuthName configuration directive.

AUTH_REQUIRED

The browser did not send any Authorization header at all, or the browser sent an Authorization header with a scheme other than Basic. In either of these cases, the get_basic_auth_ pw() method will also invoke the note_basic_auth_failure() method described later in this section.

The password returned by get_basic_auth_pw() is only valid when the result code is OK. Under all other circumstances you should ignore it. If the result code is anything other than OK the appropriate action is to exit, passing the result code back to Apache:

    return $res if $res != OK;

If get_basic_auth_pw() returns OK, we continue our work. Now we need to find the username to complement the password. Because the username may be needed by later handlers, such as the authorization and logging modules, it's stored in a stable location inside the request object's connection record. The username can be retrieved by calling the request object's connection() method to return the current Apache::Connection object and then calling the connection object's user() method:

    my $user = $r->connection->user;

The values we retrieve contain exactly what the user typed into the name and password fields of the dialog box. If the user has not yet authenticated, or pressed the submit button without filling out the dialog completely, one or both of these fields may be empty. In this case, we have to force the user to (re)authenticate:

    unless($user and $sent_pw) {
       $r->note_basic_auth_failure;
       $r->log_reason("Both a username and password must be provided",
                      $r->filename);
       return AUTH_REQUIRED;
   }

To do this, we call the request object's note_basic_auth_failure() method to add the www-Authenticate field to the outgoing HTTP headers. Without this call, the browser would know it had to authenticate but would not know what authentication method and realm to use. We then log a message to the server error log using the log_reason() method and return an AUTH_REQUIRED result code to Apache.

The resulting log entry will look something like this:

[Sun Jan 11 16:36:31 1998] [error] access to /protected/index.html
 failed for wallace.telebusiness.co.nz, reason: Both a username and
 password must be provided

If, on the other hand, both a username and password are present, then the user has authenticated properly. In this case we can return a result code of OK and end the handler:

    return OK;
}

The username will now be available to other handlers and CGI scripts. In particular, the username will be available to any authorization handler further down the handler chain. Other handlers can simply retrieve the username from the connection object just as we did.

Notice that the Apache::AuthAny module never actually checks what is inside the username and password. Most authentication modules will compare the username and password to a pair looked up in a database of some sort. However, the Apache::AuthAny module is handy for developing and testing applications that require user authentication before the real authentication module has been implemented.

An Anonymous Authentication Handler

   Show Contents   Go to Top   Previous Page   Next Page

Now we'll look at a slightly more sophisticated authentication module, Apache::AuthAnon. This module takes the basics of Apache::AuthAny and adds logic to perform some consistency checks on the username and password. This module implements anonymous authentication according to FTP conventions. The username must be "anonymous" or "anybody," and the password must look like a valid email address.

Example 6-6 gives the source code for the module. Here is a typical configuration file entry:

<Location /protected>
  AuthName Anonymous
  AuthType Basic
  PerlAuthenHandler Apache::AuthAnon
  require valid-user
  PerlSetVar Anonymous anonymous|anybody
</Location>

Notice that the <Location> section has been changed to make Apache::AuthAnon the PerlAuthenHandler for the /protected subdirectory and that the realm name has been changed to Anonymous. The AuthType and require directives have not changed. Even though we're not performing real username checking, the require directive still needs to be there in order to trigger Apache's authentication handling. A new PerlSetVar directive sets the configuration directive Anonymous to a case-insensitive pattern match to perform on the provided username. In this case, we're accepting either of the usernames anonymous or anybody.

Turning to the code listing, you'll see that we use the same basic outline of Apache::AuthAny. We fetch the provided password by calling the request object's get_basic_auth_pw() method and the username by calling the connection object's user() method. We now perform our consistency checks on the return values. First, we check for the presence of a pattern match string in the Anonymous configuration variable. If not present, we use a hardcoded default of anonymous. Next, we attempt to match the password against an email address pattern. While not RFC-compliant, the $email_pat pattern given here will work in most cases. If either of these tests fails, we log the reason why and reissue a Basic authentication challenge by calling note_basic_auth_failure(). If we succeed, we store the provided email password in the request notes table for use by modules further down the request chain.

While this example is not much more complicated than Apache::AuthAny and certainly no more secure, it does pretty much everything that a real authentication module will do.

A useful enhancement to this module would be to check that the email address provided by the user corresponds to a real Internet host. One way to do this is by making a call to the Perl Net::DNS module to look up the host's IP address and its mail exchanger (an MX record). If neither one nor the other is found, then it is unlikely that the email address is correct.

Example 6-6. Anonymous Authentication

package Apache::AuthAnon;
# file: Apathe/AuthAnon.pm
use strict;
use Apache::Constants qw(:common);
my $email_pat = '[.\w-]+\@\w+\.[.\w]*[^.]';
my $anon_id  = "anonymous";
sub handler {
   my $r = shift;
    my($res, $sent_pwd) = $r->get_basic_auth_pw;
   return $res if $res != OK;
    my $user = lc $r->connection->user;
   my $reason = "";
    my $check_id = $r->dir_config("Anonymous") || $anon_id;
    $reason = "user did not enter a valid anonymous username "
      unless $user =~ /^$check_id$/i;
    $reason .= "user did not enter an email address password " 
unless $sent_pwd =~ /^$email_pat$/o;
    if($reason) {
      $r->note_basic_auth_failure;
      $r->log_reason($reason,$r->filename);
      return AUTH_REQUIRED;
   }
   $r->notes(AuthAnonPassword => $sent_pwd);
    return OK;
}
1;
__END__

   Show Contents   Go to Top   Previous Page   Next Page
Copyright © 1999 by O'Reilly & Associates, Inc.