Apache-AppSamurai

 view release on metacpan or  search on metacpan

FAQ  view on Meta::CPAN

Q: Why is this FAQ so sparse, useless, condescending, confusing, revoltingly
   formatted, poorly written?  This is pretty much the worst FAQ ever made.
A: Thanks!  Someone is actually reading the FAQ!

Q: Really, though, couldn't you think of any questions?
A: Sounds like you are pretty sharp.  Why don't you ask some....


General
-------
Q: I am using multiple authentication methods, and the credentials passed into
   the login form seem to be going to the wrong places... help!
A: The order of credentials (and how they are checked) is defined by their
   order inside the AuthMethods setting for your auth name.  The first
   authentication method in the list will be the first to be checked, and
   will get the value sent to the credential_1 value in the login form,
   login.html.

   In most cases, you will want your strongest (or any dynamic/token based)
   authentication checked first.  Set that to credential_1 and put it first
   in the AuthMethods list.  You want your weakest, (easiest guessed or most
   static), authentication method checked last, so put it in the last
   credential_ value in your login.html form and last in the AuthMethods list.

examples/conf/appsamurai-owa.conf  view on Meta::CPAN

PerlSetVar OwaSatisfy All

# Set the "secure" flag on the authentication cookie (Note - If you are not
# using SSL, well, USE SSL!!!)
PerlSetVar OwaSecure 1

# Set the silly Microsoft http-only cookie flag
PerlSetVar OwaHttpOnly 1

# Custom mapping of xxxxxx;yyyyyy Basic authentication password input
# to specific and separate individual credentials.
# Example: If the user logs into the basic auth popup with the password:
#		myRockinPassword;1234123456
# The map below will set credential_1 as "1234123456" and credential_2
# as "myRockinPassword", then proceed as if the same were entered into
# a form login.  (Default: undef)
#PerlSetVar OwaBasicAuthMap "2,1=(.+);([^;]+)"

# List the authentication methods (modules) you will be using, in order of
# credential number on the login form.  (credential_1, credential_2, etc)
PerlSetVar OwaAuthMethods "AuthBasic"

examples/htdocs/login.pl  view on Meta::CPAN


my $r = shift;
($r) or die "FATAL: NO REQUEST SENT TO SCRIPT!\n";

# if there are args, append that to the uri after checking for and removing
# any ASERRCODE code.
$params{URI} = $r->prev->uri || '';

my $args = $r->prev->args || '';

if (($args) && ($args =~ s/&?ASERRCODE\=(bad_credentials|no_cookie|bad_cookie|expired_cookie)//)) {
    $params{REASON} = $1;
}

if ($args) { 
    $params{URI} .= '?' . $args;
}

# These messages have HTML in them with CSS. (Update as needed, or add a
# JavaScript snippet to check a hidden value and display the corresponding
# message, then just set a variable.)

# Default message
$params{MESSAGE} = "<span class=\"infonormal\">Please log in</span>";

if ($params{REASON} eq 'bad_credentials') {
    # Login failure	
    $params{MESSAGE} = "<span class=\"infored\">Access Denied - The credentials supplied were invalid. Please try again.</span>";
} elsif ($params{REASON} eq 'expired_cookie') {
    # Expired session
    $params{MESSAGE} = "<span class=\"infored\">Access Denied - Your session has expired. Please log in.</span>";
}

# Build nonce and HMAC (using server key) fro CSRF protection.  (Note - this
# only protects the login form.... once logged in, the app must protect itself.
# Yet another place where having bidirectional filtering would be useful)
# Required for CSRF protection

lib/Apache/AppSamurai.pm  view on Meta::CPAN

    my ($self, $r) = @_;
    my ($auth_type, $auth_name) = ($r->auth_type, $r->auth_name);
    
    # Use the magic of Apache::Request to ditch POST handling code
    # and cut to the args.
    my $ar = ($MP eq 1) ?
	Apache::Request->instance($r) :
	Apache2::Request->new($r);

    my ($ses_key, $tc, $destination, $nonce, $sig, $serverkey);
    my @credentials = ();

    # Get the hard set destination, or setup to just reload
    if ($r->dir_config("${auth_name}LoginDestination")) {
	$destination = $r->dir_config("${auth_name}LoginDestination");
    } elsif ($ar->param("destination")) {
	$destination = $ar->param("destination");
    } else {
	# Someday something slick could hold the URL, then cut through
	# to it.  Someday.  Today we die.
        $self->Log($r, ('warn', "No key 'destination' found in form data"));

lib/Apache/AppSamurai.pm  view on Meta::CPAN

        return $auth_type->login_form($r);
    }  

    # Check form nonce and signature
    if (defined($ar->param("nonce")) and defined($ar->param("sig"))) {
	unless (($nonce = CheckSidFormat($ar->param("nonce"))) and
		($sig = CheckSidFormat($ar->param("sig")))) {
	    
	    $self->Log($r, ('warn', "Missing/invalid form nonce or sig"));
	    $r->subprocess_env('AuthCookieReason', 'no_cookie');
	    $r->err_headers_out->{'Location'} = $self->URLErrorCode($destination, 'bad_credentials');
	    $r->status(REDIRECT);
	    return REDIRECT;
	}
	$serverkey = $self->GetServerKey($r) or die("FATAL: Could not fetch valid server key\n");

	# Now check!
	unless ($sig eq ComputeSessionId($nonce, $serverkey)) {
	    # Failed!
	    $self->Log($r, ('warn', "Bad signature on posted form (Possible scripted attack)"));
	    $r->subprocess_env('AuthCookieReason', 'no_cookie');
	    $r->err_headers_out->{'Location'} = $self->URLErrorCode($destination, 'bad_credentials');
	    $r->status(REDIRECT);
	    return REDIRECT;
	}
    } else {
	# Failed!
	$self->Log($r, ('warn', "Missing NONCE and/or SIG in posted form (Possible scripted attack)"));
	$r->subprocess_env('AuthCookieReason', 'no_cookie');
	$r->err_headers_out->{'Location'} = $self->URLErrorCode($destination, 'bad_credentials');
	$r->status(REDIRECT);
	return REDIRECT;
    }

    # Get the credentials from the data posted by the client
    while ($tc = $ar->param("credential_" . scalar(@credentials))) {
	push(@credentials, $tc);
	
	($tc) ? ($tc =~ s/^(.).*$/$1/s) : ($tc = ''); # Only pull first char
 	                                              # for logging
	$self->Log($r, ('debug', "login(); Received credential_" . (scalar(@credentials) - 1) . ": $tc (hint)"));
    }

    # Convert all args into a GET and clear the credential_X args
    $self->_convert_to_get($r) if $r->method eq 'POST';
    
    # Check against credential cache if UniqueCredentials is set
    if ($r->dir_config("${auth_name}AuthUnique")) {
	unless ($self->CheckTracker($r, 'AuthUnique', @credentials)) {
	    # Tried to send the same credentials twice (or tracker system
	    # failure. Delete the credentials to fall through
	    @credentials = ();
	    $self->Log($r, ('warn', "login(): AuthUnique check failed: Tracker failure, or same credentials have been sent before"));
	}
    }

    if (@credentials) {
	# Exchange the credentials for a session key.
	$ses_key = $self->authen_cred($r, @credentials);
	if ($ses_key) {
	    # Set session cookie with expiration included if SessionExpire
	    # is set. (Extended +8 hours so we see logout events and cleanup)
	    if ($r->dir_config("${auth_name}SessionExpire")) {
		$self->send_cookie($r, $ses_key, {expires => $r->dir_config("${auth_name}SessionExpire") + 28800});
	    } else {
		$self->send_cookie($r, $ses_key);
	    }
	    $self->handle_cache($r);
	    

lib/Apache/AppSamurai.pm  view on Meta::CPAN

    if ($r->dir_config("${auth_name}IPFailures")) {
        if ($MP eq 1) {
	    $self->CheckTracker($r, 'IPFailures', $r->dir_config("${auth_name}IPFailures"), $r->get_remote_host);
        } else {
            $self->CheckTracker($r, 'IPFailures', $r->dir_config("${auth_name}IPFailures"), $r->connection->get_remote_host);
        }
    }

    # Append special error message code and try to redirect to the entry
    # point. (Avoids having the LOGIN URL show up in the browser window)
    $r->err_headers_out->{'Location'} = $self->URLErrorCode($destination, 'bad_credentials');
    $r->status(REDIRECT);
    return REDIRECT;
    # Handle this ol' style - XXX remove?
    #$r->subprocess_env('AuthCookieReason', 'bad_credentials');
    #$r->uri($destination);
    #return $auth_type->login_form($r);
}

# Special version of login that handles Basic Auth login instead of form
# Can be called by authenticate() if there is no valid session but a
# Authorization: Basic header is detected.  Can also be called directly,
# just like login() for targeted triggering
sub loginBasic_mp1 ($$) { &loginBasic_real }
sub loginBasic_mp2 : method { &loginBasic_real }
*loginBasic = ($MP eq 1) ? \&loginBasic_mp1 : \&loginBasic_mp2;
sub loginBasic_real {
    my ($self, $r) = @_;
    my ($auth_type, $auth_name) = ($r->auth_type, $r->auth_name);
    
    my ($ses_key, $t, @at, $tc);
    my @credentials = ();

    return DECLINED unless $r->is_initial_req; # Authenticate first req only
    
    # Count input credentials to figure how to split input
    my @authmethods = $self->GetAuthMethods($r);
    (@authmethods) || (die("loginBasic(): Missing authentication methods\n"));    
    my $amc = scalar(@authmethods);

    # Extract basic auth info and fill out @credentials array
    my ($stat, $pass) = $r->get_basic_auth_pw;

    if ($r->user && $pass) {
	# Strip "domain\" portion of user if present.
	# (Thanks Windows Mobile ActiveSync for forcing domain\username syntax)
	$t = $r->user;
	$t =~ s/^.*\\+//;
	$r->user($t);
	push(@credentials, $t);

	# Use custom map pattern if set; else just a generic split on semicolon
	if (defined($r->dir_config("${auth_name}BasicAuthMap"))) {
	    push(@credentials, $self->ApplyAuthMap($r,$pass,$amc));
	} else {
	    # Boring old in-order split
	    foreach (split(';', $pass, $amc)) {
		push(@credentials, $_);
	    }
	}

	# Log partial first char of each credential
	if ($r->dir_config("${auth_name}Debug")) {
	    for (my $i = 0; $i < scalar(@credentials); $i++) {
		$credentials[$i] =~ /^(.)/;
		$self->Log($r, ('debug', "loginBasic(): Received credential_$i: $1 (hint)"));
	    }
	}

	# Check against credential cache if AuthUnique is set
	if ($r->dir_config("${auth_name}AuthUnique")) {
	    unless ($self->CheckTracker($r, 'AuthUnique', @credentials)) {
		# Tried to send the same credentials twice (or tracker system
		# failure. Delete the credentials to fall through
		@credentials = ();
		$self->Log($r, ('warn', "loginBasic(): AuthUnique check failed: Same credentials have been sent before"));
	    }
	}
 
	if (@credentials) {
	    # Exchange the credentials for a session key.
	    $ses_key = $self->authen_cred($r, @credentials);
	    if ($ses_key) {
		# Set session cookie with expiration included if SessionExpire
		# is set. (Extended +8 hours for logouts/cleanup)
		if ($r->dir_config("${auth_name}SessionExpire")) {
		    $self->send_cookie($r, $ses_key, {expires => $r->dir_config("${auth_name}SessionExpire") + 28800});
		} else {
		    $self->send_cookie($r, $ses_key);
		}
		$self->handle_cache($r);

lib/Apache/AppSamurai.pm  view on Meta::CPAN


# Retrieve session cookie path
sub get_cookie_path {
    my ($self, $r) = @_;
    
    my $auth_name = $r->auth_name;
    
    return $r->dir_config("${auth_name}Path");
}

# Check authentication credentials and return a new session key
sub authen_cred {
    my $self = shift;
    my $r = shift;
    my $username = shift;
    my @creds = @_;
    my $alterlist = {};

    # Check for matching credentials and configured authentication methods
    unless (@creds) {
	$self->Log($r, ('error', "LOGIN FAILURE: Missing credentials"));
	return undef;
    }

    my @authmethods = $self->GetAuthMethods($r);
    unless (@authmethods) {
        $self->Log($r, ('error', "LOGIN FAILURE: No authentication methods defined"));
	return undef;
    }
    unless (scalar(@creds) == scalar(@authmethods)) {
        $self->Log($r, ('error', "LOGIN FAILURE: Wrong number of credentials supplied"));
        return undef;
    }
    
    my $authenticated = 0;
    
    my ($ret, $errors);
    
    # Require and get new instance of each authentication module
    my $authenticators = $self->InitAuthenticators($r, @authmethods);
    

lib/Apache/AppSamurai.pm  view on Meta::CPAN

    unless (CheckSidFormat($serverkey)) {
	# Not good, dude.  This should not happen
	$self->Log($r, ('error', "GetServerKey(): Invalid server session key (CheckSidFormat() failure) for $auth_name"));
	return undef;
    }

    return $serverkey;
}


# Apply the configured BasicAuthMap to the passed in credentials
# BasicAuthMap allows for flexibly parsing a single line of authentication
# data into multiple credentials in any order.  (Keep those users happy...)
# Returns an array with the parsed credentials in order, or an empty set on
# failure.
sub ApplyAuthMap {
    my ($self, $r, $pass, $amc) = @_;
    my $auth_name = ($r->auth_name) || ('');
    my ($o, $m, $i, @ct);
    my @creds = ();

    # Check basic map format
    ($r->dir_config("${auth_name}BasicAuthMap") =~ /^\s*([\d\,]+)\s*\=\s*(.+?)\s*$/) || (die("ApplyAuthMap(): Bad format in ${auth_name}BasicAuthMap\n"));
    $o = $1;
    $m = $2;
    
    # Try to map values from pass string
    (@ct) = $pass =~ /^$m$/;
    unless (scalar(@ct) eq $amc) {
	$self->Log($r, ('warn', "ApplyAuthMap: Unable to match credentials with ${auth_name}BasicAuthMap"));
	return ();
    }
    
    # Check credential numbers for sanity and assign values
    foreach $i (split(',', $o)) {
	($i =~ s/^\s*(\d+)\s*$/$1/) || (die("ApplyAuthMap(): Bad mapping format in ${auth_name}BasicAuthMap\n"));
	push(@creds, $ct[$i - 1]);
    }
	    
    return @creds;

lib/Apache/AppSamurai.pm  view on Meta::CPAN

	}
    }

    # Expired or New entry: Set timestamp to now and count to 1
    $trak->{$ip} = join(':', "ts$time", "cnt1");
    
    return 1;
}

# Check given tracker hash ($_[0]), make sure we have not seen the same
# set of credentials ($_[1] - $_[n-1]) before.  Stores a hash of credential
# string to minimize security risk.
sub CheckTrackerAuthUnique {
    my $trak = shift;
    my $ch = HashAny(@_);
    my $time = time();

    # If defined, the jig is up!
    if ($trak->{$ch}) {
	die("CheckTrackerAuthUnique(): RULE VIOLATION: credkey=$ch\n");
    } else {

lib/Apache/AppSamurai.pm  view on Meta::CPAN

All configuration is done within Apache.  Requires Apache 1.3.x/mod_perl1 or 
Apache 2.0.x/mod_perl2.  See L</EXAMPLES> for sample configuration segments.

=head1 DESCRIPTION

B<Apache::AppSamurai> protects web applications from direct attack by
unauthenticated users, and adds a flexible authentication front end
to local or proxied applications with limited authentication options.

Unauthenticated users are presented with either a login form, or a basic
authentication popup (depending on configuration.)  User supplied credentials
are checked against one or more authentication systems before the user's
session is created and a session authentication cookie is passed back to the
browser.  Only authenticated and authorized requests are proxied through
to the backend server.

Apache::AppSamurai is based on, and includes some code from,
L<Apache::AuthCookie|Apache::AuthCookie>.
Upon that core is added a full authentication and session handling framework.
(No coding required.)  Features include:

lib/Apache/AppSamurai.pm  view on Meta::CPAN


=head2 AUTHENTICATION CONFIGURATION

Most authentication is specific to the authentication module(s) being used.
Review their specific documentation while configuring.

=head3 I<AuthMethods> C<METHOD1,METHOD2...>

(Default: undef)
A comma separated list of the authentication sub-modules to use.  The order of
the list must match the order of the C<credentials_X> parameters in the login
form. (Note - C<credential_0> is always the username, and is passed as such to
all the authentication modules.)

=head3 I<BasicAuthMap> C<N1,N2,.. = REGEX>

(Default: undef)

Custom mapping of Basic authentication password input to specific and separate
individual credentials. This allows for AppSamurai to request basic
authentication for an area, then split the input into credentials that can be
checked against multiple targets, just like a form based login.  This is very
useful for clients, like Windows Mobile ActiveSync, that only support basic
auth logins.  Using this feature you can add SecurID or other additional
authentication factors without having to pick only one.

The syntax is a bit odd.  First, specify a list of the credential numbers
you want mapped, in order they will be found within the input. Then
create a regular expression that will match the input, and group each item
you want mapped.

lib/Apache/AppSamurai.pm  view on Meta::CPAN

C<OK> is returned if conditions are satisfied, otherwise C<HTTP_FORBIDDEN> is
returned.

=head3 login()

Should be configured in the Apache config as the PerlHandler, (or
"PerlResponseHandler" for mod_perl 2.x), for a special pseudo file under
the F<AppSamurai/> directory.  In example configs and
the example F<login.pl> form page, the pseudo file is named B<LOGIN>.

C<login()> expects an Apache request with a list of credentials included as
arguments.  B<credential_0> is the username.  All further credentials are
mapped in order to the authentication modules defined in L</AuthMethods>.
Each configured authentication method is checked, in order.  If all
succeed, a session is created and a session authentication cookie is returned
along with a redirect to the page requested by the web browser.

If login fails, the browser is redirected to the login form.

=head3 logout()

Should be called directly by your logout page or logout pseudo file.

lib/Apache/AppSamurai.pm  view on Meta::CPAN

 # Require secure sessions (default: 1)
 #PerlSetVar ExampleSecure 1

 # Set proprietary MS flag
 PerlSetVar ExampleHttpOnly 1

 # Define authentication sources, in order
 PerlSetVar ExampleAuthMethods "AuthRadius,AuthBasic"

 # Custom mapping of xxxxxx;yyyyyy Basic authentication password input
 # to specific and separate individual credentials. (default: undef)
 PerlSetVar ExampleBasicAuthMap "2,1=(.+);([^;]+)"

 
 ## Apache::AppSamurai::AuthRadius options ##
 # (Note - See L<Apache::AppSamurai::AuthRadius> for more info)
 PerlSetVar ExampleAuthRadiusConnect "192.168.168.168:1645"
 PerlSetVar ExampleAuthRadiusSecret "radiuspassword"

 
 ## Apache::AppSamurai::AuthBasic options.##

lib/Apache/AppSamurai/AuthBase.pm  view on Meta::CPAN


The username is validated using L</CheckInputUser()>, and then the password
is validated using L</CheckInputPass()>.  (If either of those fail,
an error is added and 0 is returned.)

After validation, L</Initialize()> is called if the C<< $self->{init} >>
flag has not been set.  (Note - Apache::AppSamurai calls C<Initialize()>
separately.  This functionality is added as a fail safe, or for testing.)

Finally, the object's L</Authenticator()> method is called to perform the
actual work of checking the credentials.  It's result is returned by
C<Authenticate()> to the caller.

C<Authenticate()> should not generally need to be overridden.

=head2 CheckInputUser()

Is called with an object ref and expects a scalar username as its only
argument.  If successful, the validated username is returned.  In case of
a failure or violation, C<undef> is returned.

lib/Apache/AppSamurai/AuthBase.pm  view on Meta::CPAN

exist, undef is returned.

C<Errors()> is called by Apache::AppSamurai after using the authentication
module's C<Authenticate()> method.  It generally does not need to be
overridden.

=head1 EXAMPLES

Here is an example authentication module based on Auth::Base.  Let's call
it Apache::AppSamurai::AuthGarbage and have it use the fictitious module
Junk::Dumpster to check credentials.

 package Apache::AppSamurai::AuthGarbage;

 use Apache::AppSamurai::AuthBase;
 use Junk::Dumpster;  # Kids, don't try this at home 

 # Inherit the AuthBase wind...
 @ISA = qw( Apache::AppSamurai::AuthBase );
 
 # Override the Configure method to add special config options

lib/Apache/AppSamurai/AuthBasic.pm  view on Meta::CPAN

	    
	    $check = 1;
	    last;
	}
    }
    unless ($check) {
	$self->AddError('error', "URL \"" . $self->{conf}{LoginUrl} . "\" did not list \"Basic\" as an allowed authentication method");
	return 0;
    }
    
    # Set credentials in request
    $self->{request}->authorization_basic($user, $pass);

    # Connect with beans and check return
    $response = $self->{client}->request($self->{request});

    if ($response->code() == $self->{conf}{SuccessCode}) {
	# YAY!  Now collect and store cookies and headers as directed
	if (($self->{conf}{PassBackCookies}) && (@tmp = $response->header('set-cookie'))) {
	    foreach (@tmp) {
		# Trim whitespace

lib/Apache/AppSamurai/AuthBasic.pm  view on Meta::CPAN

    # DEFAULT DENY #
    return 0;
}

1; # End of Apache::AppSamurai::AuthBasic

__END__

=head1 NAME

Apache::AppSamurai::AuthBasic - Check credentials against backend web server
using HTTP basic auth

=head1 SYNOPSIS

The module is selected and configured inside the Apache configuration.

 # Example with an authname of "fred" for use as part of an Apache config.

 # Configure as an authentication method
 PerlSetVar fredAuthMethods "AuthBasic"

lib/Apache/AppSamurai/AuthRadius.pm  view on Meta::CPAN

    # DEFAULT DENY # 
    return 0;
}
    
1; # End of Apache::AppSamurai::AuthRadius

__END__

=head1 NAME

Apache::AppSamurai::AuthRadius - Check credentials against RADIUS service

=head1 SYNOPSIS

The module is selected and configured inside the Apache configuration.

 # Example with an authname of "fred" for use as part of an Apache config.

 # Configure as an authentication method
 PerlSetVar fredAuthMethods "AuthRadius"

lib/Apache/AppSamurai/AuthSimple.pm  view on Meta::CPAN


    $self->AddError($severity, $msg);
}

1; # End of Apache::AppSamurai::AuthSimple

__END__

=head1 NAME

Apache::AppSamurai::AuthSimple - Check credentials with Authen::Simple framework

=head1 SYNOPSIS

The module is selected and configured inside the Apache configuration.

 # Example with an authname of "fred" for use as part of an Apache config.

 # Configure as an authentication method (Authen::Simple::Passwd shown)
 PerlSetVar fredAuthMethods "AuthSimplePasswd"

t/conf/extra.conf.in  view on Meta::CPAN

PerlRequire @ServerRoot@/startup.pl
PerlModule Apache::AppSamurai

PerlSetVar WhatEverPath /
PerlSetVar WhatEverLoginScript /docs/login.pl
PerlSetVar WhatEverDebug 3

PerlSetVar WhatEverCookieName CakeNotCookie
PerlSetVar WhatEverSecure 1

# Map Basic auth password into 3 credentials, separated by
# semicolons and reverse mapped for fun!
PerlSetVar WhatEverBasicAuthMap "3,2,1=(.+);([^;]+);([^;]+)"

# Please, don't use these auth modules... they kinda suck.
# (Note that the names hint to the static password for each.
# You know that ain't cool.)
PerlSetVar WhatEverAuthMethods "AuthTestFLUFFY,AuthTestPASSWORD,AuthTest123456"

# Some trackers and tracker system setup
PerlSetVar WhatEverAuthUnique 1



( run in 0.720 second using v1.01-cache-2.11-cpan-4d50c553e7e )