Apache Remote Authentication

Friday, January 29 2010 @ 12:17 PM CST

Contributed by: nat

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:

  • User visits Application Webserver
  • Application Webserver receives no authentication cookie, redirects user to Authentication Webserver with Application Webservers called URL as the query string.
  • The user visits the Authentication Webserver and authenticates by whatever means (NTLM, Kerberos, RSA, Basic Auth, other)
  • Authentication Webserver stores user credentials and the original URL on the Application Server that the user was trying to reach in a randomly named file, and directs Web Client back to the Application Webserver with information about how to get the random data file.
  • Web Client sends credentials data file info to the Application Webserver
  • Application Webserver fetches credentials data file from Authentication Webserver, after which the Authentication Webserver deletes credentials file. (Note that credentials are not entrusted to the client and pass from the authentication server to the application server directly.) Application Server generates cookie value associates userid with it, saving for 12 hours.
  • Application Webserver sends Web Client the authentication cookie lasting 12 hours along with a redirect to the original URL the client called.
  • Client visits original URL with authentication cookie, server uses the value to look up the userid and populates the REMOTE_USER environment variable, and the user is able to proceed.
  • After the cookie expires, whatever URL the Web Client hits next will trigger this whole process again

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

0 comments



/article.php/20100129121732331