Integrating mod_perl with Apache 2.1 Authentication
by Geoffrey Young
|
Pages: 1, 2, 3
Taking a Step Back
Let's recap what we have accomplished so far. AuthenHook.pm
redefines AuthDigestProvider and AuthBasicProvider so
that any Perl providers listed in the configuration are automagically
registered and inserted into the authentication process. At request time, one
of the default Apache authentication handlers will call on the configured
providers to supply server-side credentials. All registered Perl providers
really point to the callbacks in AuthenHook.xs which have the
arduous task of proxying the request for server-side credentials to the proper
Perl provider. All in all, Apache::AuthenHook covers lots of ground, even if
the gory details of what happens over in XS land have been left out.
As we mentioned earlier, Apache::AuthenHook not only the ability to write
authentication providers in Perl, but it also follows the Apache API very
closely. While diving deep into the XS code that Apache::AuthenHook uses to
implement the check_password and get_realm_hash
callbacks is far beyond the scope of this article, you may find it interesting
that the callback signature for check_password
static authn_status check_password(request_rec *r, const char *user,
const char *password)
{
...
}
is practically identical to what Apache::AuthenHook passes on to Perl providers supporting the Basic authentication scheme.
sub handler {
my ($r, $user, $password) = @_;
...
}
If you recall, we started investigating the Apache 2.1 provider mechanism as
a way to combine the security of the Digest authentication scheme with the
strength of Perl. The signature for the Digest authentication callback,
get_realm_hash, is only slightly different than
check_password.
static authn_status get_realm_hash(request_rec *r, const char *user,
const char *realm, char **rethash)
{
...
}
How does this translate into a Perl API? It is surprisingly simple. As it
turns out, the name check_password is significant—for
Basic authentication, the provider is expected to take steps to see if the
incoming username and password match the username and password stored on the
server back-end. For Digest authentication, as the name
get_realm_hash might suggest, all a provider is responsible for is
retrieving the hash for a user at a given realm. mod_auth_digest
does all the heavy lifting.
Digest Authentication for the People
While we didn't take the time to explain how Basic authentication over HTTP actually works, briefly explaining Digest authentication is probably worth the time, if only to allow you to appreciate the elegance of the new provider mechanism.
When a request comes in for a resource protected by Digest authentication,
the server begins the process by returning a WWW-Authenticate
header that contains the authentication scheme, realm, a server generated
nonce, and various other bits of information. A fully RFC-compliant
WWW-Authenticate header might look like the following.
WWW-Authenticate: Digest realm="realm1",
nonce="Q9equ9C+AwA=195acc80cf91ce99828b8437707cafce78b11621",
algorithm=MD5, qop="auth"
On the client side, the username and password are entered by the end user
based on the authentication realm sent from the server. Unlike Basic
authentication, in which the client transmits the user's password practically
in the clear, Digest authentication never exposes the password over the wire.
Instead, both the client and server handle the user's credentials with care.
For the client, this means rolling up the user credentials, along with other
parts of the request such as the server-generated nonce and request URI, into a
single MD5 hash, which is then sent back to the server via the
Authorization header.
Authorization: Digest username="user1", realm="realm1",
qop="auth", algorithm="MD5", uri="/index.html",
nonce="Q9equ9C+AwA=195acc80cf91ce99828b8437707cafce78b11621",
nc=00000001, cnonce="3e4b161902b931710ae04262c31d9307",
response="49fac556a5b13f35a4c5f05c97723b32"
The server, of course, needs to have its own copy of the user credentials
around for comparison. Now, because the client and server have had (at various
points in time) access to the same dataset—the user-supplied username
and password, as well as the request URI, authentication realm, and other
information shared in the HTTP headers—both ought to be able to
generate the same MD5 hash. If the hash generated by the server does not match
the one sent by the client in the Authorization header, the
difference can be attributed to the one piece of information not mutually
agreed upon through the HTTP request: the password.
As you can see from the headers involved, there is quite a lot of
information to process and interpret with the Digest authentication scheme.
However, if you recall, one of the benefits of the new provider mechanism is
that mod_auth_digest takes care of all the intimate details of the
scheme internally, relieving you from the burden of understanding it at
all.
All a Digest provider is required to do is match the incoming user and realm
to a suitable digest, stored in the medium of its choosing, and return it. With
the hash in hand, mod_auth_digest will do all the subsequent
manipulations and decide whether the hash the provider supplied is indeed
sufficient to allow the user to continue on its journey to the resource it is
after.
With that background behind us, we can proceed with a sample Perl Digest provider.
package My::DigestProvider;
use Apache::Log;
use Apache::Const -compile => qw(OK DECLINED HTTP_UNAUTHORIZED);
use strict;
sub handler {
my ($r, $user, $realm, $hash) = @_;
# user1 at realm1 is found - pass to mod_auth_digest
if ($user eq 'user1' && $realm eq 'realm1') {
$$hash = 'eee52b97527306e9e8c4613b7fa800eb';
return Apache::OK;
}
# user2 is denied outright
if ($user eq 'user2' && $realm eq 'realm1') {
return Apache::HTTP_UNAUTHORIZED;
}
# all others are passed along to the next provider
return Apache::DECLINED;
}
1;
Note the only slight difference between the interface for Digest
authentication as compared to Basic authentication. Because the authentication
realm is an essential part of the Digest scheme, it is passed to our
handler() subroutine in addition to the request record,
$r, and username we received with the Basic scheme.
Knowing the username and authentication realm, our provider can choose
whatever method it desires to retrieve the MD5 hash associated with the user.
Returning the hash for comparison by mod_auth_digest is simply a
matter of populating the scalar referenced by $hash and returning
OK. While using references in this way may feel a bit strange, it
follows the same pattern as the official Apache C API, so I guess that makes it
ok.
If the user cannot be found, the provider can choose to return
HTTP_UNAUTHORIZED and deny access to the user, or return
DECLINED to pass authority for the user to the next provider.
Remember, unlike with the Perl handlers for all the other phases of the
request, you can intermix Perl providers with C providers, sandwiching the
default file provider with Perl providers of your own choosing.
The one question that remains is how to generate a suitable MD5 digest to
pass back to mod_auth_digest. For the default file provider, the
return digest is typically generated using the htdigest binary
that comes with the Apache installation. However, a Perl one-liner that can be
used to generate a suitable MD5 digest for Perl providers would look similar to
the following.
$ perl -MDigest::MD5 -e'print Digest::MD5::md5_hex("user:realm:password")'
That is all there is to it. No hash checking, no header manipulations, no back flips or somersaults. Simply dig out the user credentials and pass them along. At last, Digest authentication for the (Perl) people.
Don't Forget the Tests!
Of course, no module would be complete without a test suite, and the Apache-Test framework introduced last time gives us all the tools we need to write a complete set of tests.
For the most part, the tests for Apache::AuthenHook are not that different from those presented before. LWP supports Digest authentication natively, so all our test scripts really need to do is make a request to a protected URI and let LWP do all the work. Here is a snippet from one of the tests.
plan tests => 10, (have_lwp &&
have_module('mod_auth_digest'));
my $url = '/digest/index.html';
$response = GET $url, username => 'user1', password => 'digest1';
ok $response->code == 200;
When we plan the tests, we first check for the existence of
mod_auth_digest—both mod_auth_basic and
mod_auth_digest can be enabled or disabled for any given
installation, so we need to check for them where appropriate. Passing the
username and password credentials is pretty straightforward, using the
username and password keys after the URL when
formatting the request.
Actually, while the username and password keys
have special meaning, you can use the same technique to send any arbitrary
headers in the request.
# fetch the Last-Modified header from one response...
my $last_modified = $response->header('Last-Modified');
# and use it in the next request
$response = GET $uri, 'If-Modified-Since' => $last_modified;
ok ($response->code == HTTP_NOT_MODIFIED);
That's something to note just in case you need that functionality sometime later in your testing life.
One final note about our tests will apply to anyone writing a mod_perl XS
extension. Instead of using extra.conf.in to configure Apache, we
used extra.last.conf.in. The difference between the two is that
extra.last.conf.in is guaranteed to be loaded the last in the
configuration order—if our PerlLoadModule directive is
processed before mod_perl gets the chance to add the proper blib
entries, nothing will work, so ensuring our configuration is loaded after
everything else is in place is important.
Whew
mod_perl is truly exciting. With surprisingly little work, we have managed to open an entire new world within Apache 2.1 to the Perl masses. I know of no other blend of technologies that allow for such remarkable flexibility beyond what each individually brings to the table. Hopefully, this article has not only introduced you to new Apache and authentication concepts, but has also brought to light ways in which you can leverage mod_perl that you never thought of before.
More Information
I apologize if this article is a little on the heavy side, teasing you with only cursory introductions to cool concepts while leaving out the finer details. So, if you want to explore these concepts in more detail, I leave you with the following required reading.
A nice overall introduction to the new provider mechanism can be found in Safer Apache Driving with AAA. The mechanics of Basic authentication can be found in lots of places, but decent explanations of Digest authentication are harder to find. Both are covered to some level of detail in Chapter 13 of the mod_perl Developer's Cookbook, which is freely available online. Recipe 13.8 in particular includes the code that became the splinter in my mind and eventually this article.
A more detailed explanation of directive handlers in mod_perl 2.0 can be found in the mod_perl 2.0 documentation. Although covering only mod_perl 1.0 directive handlers, whose implementation is very different, Chapter 8 in Writing Apache Modules with Perl and C and Recipes 7.8 through 7.11 in the mod_perl Developer's Cookbook provide excellent explanations of concepts that are universal to both platforms, and are essential reading if you plan on using directive handlers yourself. If you are curious about the intricate details of directive merging, Chapter 21 in Apache: the Definitive Guide presents probably the most comprehensive explanation available.
Finally, if you are interested in the gory details of the XS that really drives Apache::AuthenHook, there is no better single point of reference than Extending and Embedding Perl, which was my best friend while writing this module and absolutely deserves a place on your bookshelf.
Thanks
Many thanks to Stas Bekman and Philippe Chiasson for their feedback and review of the several patches to mod_perl core that were required for the code in this article, as well as to Jörg Walter, who was kind enough to take the time to review this article and give valuable feedback.

