This started out as a project to take Apache NTLM authentication and offload it to cookie authentication, because the NTLM auth was flaky and would sometimes croak on random pages, given that NTLM authentication was being done for each keepalive. Upon completion of this project, it is determined that ANY authentication process, be it NTLM, Kerberos, RSA SecurID, or other method that can be performed against apache can be used, and offload credentials to cookie authentication. Without further ado, let's look at how it works:
![]() |
|
![]() |
|
![]() |
|
![]() |
|
Below are apache configuration and scripts: Apache Application Webserver, needing authentication:
RewriteEngine on
RewriteLock logs/rewrite_map_lock
NameVirtualhost *
<VirtualHost *>
ServerName default
### BELOW IS THE AUTHENTICATION PART ###
RewriteEngine on
# If cookie "authen_ntlm_cookie" does not exist, send to auth via transparent proxy
RewriteCond % !authen_ntlm_cookie
RewriteCond % !^/auth
RewriteCond % !^/500
RewriteCond % .
RewriteRule (/.*$) http://apache-auth.company.com/auth?http://%$1?% [L,R]
RewriteCond % !authen_ntlm_cookie
RewriteCond % !^/auth
RewriteCond % !^/500
RewriteRule (/.*$) http://apache-auth.company.om/auth?http://%$1 [L,R]
# If trying to get the protected site, use the cookie to get the authenticated user
RewriteMap auth_user prg:/export/home/www/ntlmauth/auth_user
RewriteCond % authen_ntlm_cookie
RewriteCond % !^/auth
RewriteRule ^.*$ - [E=REMOTE_USER:$}]
ScriptAlias /auth /export/home/www/ntlmauth/auth
<Directory "/export/home/www">
Order allow,deny
Allow from all
</Directory>
</VirtualHost>
And here is the webserver config on the authentication server, assuming NTLM authentication with the Apache2::AuthenNTLM mod_perl module.
LoadModule perl_module modules/mod_perl.so
<VirtualHost *>
ServerName apache-auth.company.com
ScriptAlias /auth /export/home/www/ntlmauth/auth
<Location /auth>
# NTLM auth
PerlAuthenHandler Apache2::AuthenNTLM
AuthType ntlm,basic
AuthName "Authentication Required"
# "domain pdc bdc"
PerlAddVar ntdomain "MYDOMAIN mydc01 mydc02"
PerlSetVar defaultdomain MYDOMAIN
PerlSetVar splitdomainprefix 1
#PerlSetVar ntlmdebug on
require valid-user
</Location>
ScriptAlias /auth_fetch /export/home/www/ntlmauth/auth_fetch
DocumentRoot /export/home/www/null/www
<Directory "/export/home/www">
Order allow,deny
Allow from all
</Directory>
</VirtualHost>
Now for the perl code that does the manipulation. It's in /export/home/www/ntlmauth as configured above. First we have "auth" which runs on BOTH the client and server, doing different things on each.
#!/usr/bin/perl
# This file is dual-purposed:
# 1] Users are sent here as https://apache-auth/auth if they don't
# have an auth cookie. By apache authentication rules, we should have
# ENV set by NTLM by now, so create a nonce, save
# information in the nonce file, and return the user to
# http://callingserver/auth?nonce=<noncevalue>
# 2] Users are sent back to
# http://callingserver/auth?nonce=<noncevalue>. Take this value and
# call https://apache-auth/auth_fetch?nonce=<noncevalue> (server to server
# communication to keep sensitive data from going to client) with
# nonce value, receive nonce file information. Make map file for the
# cookie with userid, and then redirect user to original caller.
# Note that auth_fetch should be password protected or otherwise
# secured so that only servers can hit it.
use CGI qw(cookie);
use FindBin;
use LWP::Simple qw(get);
$cookie_name="authen_ntlm_cookie";
$cookie_expiration="+12h";
$redirect_server="http://apache-auth.company.com";
$nonce_dir=$FindBin::Bin."/nonce"; # only used on apache-auth
$map_dir=$FindBin::Bin."/map"; # only used on callingserver, shared with auth_user
$DEBUG=0;
if ($ENV =~ /^http/i) {
# Scenario 1 above due to query string being URI. We are on apache-auth server
# This script should be protected by authentication, so we should not have
# a case where there is not a REMOTE_USER set
## Create cookie package
# nonce value
my $nonce=int(rand(1000000000000000));
# Calling location - original URL plus query string
my $location = $ENV;
$location =~ s/%3f/?/g;
## Write out nonce file
open(W, ">$nonce_dir/$nonce");
print W $ENV."n".$location."n";
close(W);
## Redirect user to local auth
my $calling_server = $location;
$calling_server =~ s/^([^/]+//[^/]*)/.*/$1/;
if ($DEBUG) {
print "Content-type: text/plainnn";
}
print "Location: $calling_server/auth?nonce=$noncenn";
if ($DEBUG) {
print `env`;
}
# Let's clean up the nonce dir while we are here
&cleanup($nonce_dir);
} elsif ($ENV =~ /^nonce=/) {
# Scenario 2 above
my $query = new CGI;
# Get nonce info
my $url = $redirect_server."/auth_fetch?nonce=".$query->param("nonce");
my $content = get($url);
if (! $content) {die "Did not receive nonce information for $url"}
my ($userid, $referrer) = split(/n/, $content);
# cookie value
my $cookievalue=int(rand(1000000000000000));
# Write out map file
open(W, ">$map_dir/$cookievalue");
print W "$userid";
close(W);
# Give back cookie and relocate
print "Set-Cookie: ".cookie(-name=>$cookie_name, -value=>$cookievalue, -expires=>$cookie_expiration, -path=>'/')."n";
print "Location: $referrernn";
# Let's clean up the map dir while we are here
# This is problematic - if users have cookies set still to these and server restarts, they will lose authentication.
# Can set original cookie to 12 hours and clean up here after 2 days... hence the 12h expiration above
&cleanup($map_dir);
} else {
die "Unsupported functionality";
}
exit;
##############################################################################
# cleanup
# DESCRIPTION: Clean up files more than 2 days old
# ARGUMENTS: file dir
# RESULTS: n/a
##############################################################################
sub cleanup {
my ($dir) = @_;
my $now = time();
my $threshold = 86400 * 2;
opendir(R, $dir);
while (my $file = readdir(R)) {
next if ($file !~ /^d+$/); # Only clear out numeric filenames
my $fqfile = "$dir/$file";
my @stats = stat($fqfile);
if ($stats[9] + $threshold < $now) {
unlink($fqfile);
}
}
closedir(R);
}
Next is auth_fetch:
#!/usr/bin/perl
use CGI;
use FindBin;
my $query = new CGI;
$nonce_dir = $FindBin::Bin."/nonce";
$nonce_file = $nonce_dir."/".$query->param("nonce");
print "Content-type: text/plainnn";
if (-r $nonce_file) {
open(R,$nonce_file);
while (<R>)
close(R);
unlink($nonce_file);
}
And finally, auth_user:
#!/usr/bin/perl
# Users should have a cookie set that we can use to get their login
# information. Find out their userid and print it so that apache can
# set it as ENV
use FindBin;
$cookie_name="authen_ntlm_cookie";
$map_dir=$FindBin::Bin."/map";
$|=1;
# Data structure to keep auth data in memory rather than disk access
# each time
my %AUTH=();
while ($input = <STDIN>) {
chomp($input);
$input =~ /$cookie_name=(d+)/;
my $cookie_id = $1;
eval {
if (! ($AUTH)) {
open(R, "$map_dir/$cookie_id") || die "No auth data for cookie $cookie_id";
$AUTH = <R>;
chomp($AUTH);
close(R);
}
};
if ($@) {
warn $@;
}
print $AUTH."n";
}
On the authentication server, you should have a directory, /export/home/www/ntlmauth/nonce, and on the application webserver, you should have directory /export/home/www/ntlmauth/map - both writeable by your webserver user (apache) and not accessible by others.
Note that you will probably want to change your logfile format to use the REMOTE_USER variable, similar to:
LogFormat "%h %l %e %t "%r" %>s %b "%i" "%i"" combined