Apache-AppSamurai
view release on metacpan or search on metacpan
lib/Apache/AppSamurai.pm view on Meta::CPAN
my ($self, $r) = @_;
return unless $r->method eq 'POST';
$self->Log($r, ('debug', "Converting POST -> GET"));
# Use Apache::Request for immediate access to all arguments.
my $ar = ($MP eq 1) ?
Apache::Request->instance($r) :
Apache2::Request->new($r);
# Pull list if GET and POST args
my @params = $ar->param;
my ($name, @values, $value);
my @pairs = ();
foreach $name (@params) {
# we don't want to copy login data, only extra data.
$name =~ /^(destination|credential_\d+)$/ and next;
# Pull list of values for this key
@values = $ar->param($name);
# Make sure there is at least one value, which can be empty
(scalar(@values)) or ($values[0] = '');
foreach $value (@values) {
if ($MP eq 1) {
push(@pairs, Apache::Util::escape_uri($name) . '=' .
Apache::Util::escape_uri($value));
} else {
# Assume mod_perl 2 behaviour
push(@pairs, Apache2::Util::escape_path($name, $r->pool) .
'=' . Apache2::Util::escape_path($value, $r->pool));
}
}
}
$r->args(join '&', @pairs) if scalar(@pairs) > 0;
$r->method('GET');
$r->method_number(M_GET);
$r->headers_in->unset('Content-Length');
}
# Handle regular (form based) login
sub login_mp1 ($$) { &login_real }
sub login_mp2 : method { &login_real }
*login = ($MP eq 1) ? \&login_mp1 : \&login_mp2;
sub login_real {
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"));
$r->subprocess_env('AuthCookieReason', 'no_cookie');
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);
# Log 1/2 of session key to debug
$self->Log($r, ('debug', "login(): session key (browser cookie value): " . XHalf($ses_key)));
# Godspeed You Black Emperor!
$r->headers_out->{"Location"} = $destination;
return HTTP_MOVED_TEMPORARILY;
}
}
# Add their IP to the failure tracker
# Ignores return (refusing a login page to an attacker doesn't stop them
# from blindly reposting... can add a fail here if an embedded form
# verification key is added to the mix in the future)
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);
# Log 1/2 of session key to debug
$self->Log($r, ('debug', "loginBasic(): session key (browser cookie value): " . XHalf($ses_key)));
# Godspeed You Black Emperor!
$t = $r->uri;
($r->args) && ($t .= '?' . $r->args);
$self->Log($r, ('debug', "loginBasic(): REDIRECTING TO: $t"));
$r->err_headers_out->{'Location'} = $t;
return REDIRECT;
}
}
}
# Unset the username if set
$r->user() and $r->user(undef);
# Add their IP to the failure tracker and just return HTTP_FORBIDDEN
# if they exceed the limit
if ($r->dir_config("${auth_name}IPFailures")) {
if ($MP eq 1) {
unless ($self->CheckTracker($r, 'IPFailures', $r->dir_config("${auth_name}IPFailures"), $r->get_remote_host)) {
$self->Log($r, ('warn', "loginBasic(): Returning HTTP_FORBIDDEN to IPFailires banned IP"));
return HTTP_FORBIDDEN;
}
} else {
unless ($self->CheckTracker($r, 'IPFailures', $r->dir_config("${auth_name}IPFailures"), $r->connection->get_remote_host)) {
$self->Log($r, ('warn', "loginBasic(): Returning HTTP_FORBIDDEN to IPFailires banned IP"));
return HTTP_FORBIDDEN;
}
}
}
# Set the basic auth header and send back to the client
$r->note_basic_auth_failure;
return HTTP_UNAUTHORIZED;
}
# Logout, kill session, kill, kill, kill
sub logout_mp1 ($$) { &logout_real }
sub logout_mp2 : method { &logout_real }
*logout = ($MP eq 1) ? \&logout_mp1 : \&logout_mp2;
sub logout_real {
my $self = shift;
my $r = shift;
my $auth_name = $r->auth_name;
my $redirect = shift || "";
my ($sid, %sess, $sessconfig, $username, $alterlist);
# Get the Cookie header. If there is a session key for this realm, strip
lib/Apache/AppSamurai.pm view on Meta::CPAN
my $auth_name = $r->auth_name;
if (my $expires = $p{expires} || $r->dir_config("${auth_name}Expires")) {
$expires = Apache::AppSamurai::Util::expires($expires);
$string .= "; expires=$expires";
}
$string .= '; path=' . ( $self->get_cookie_path($r) || '/' );
if (my $domain = $r->dir_config("${auth_name}Domain")) {
$string .= "; domain=$domain";
}
if (!$r->dir_config("${auth_name}Secure") || ($r->dir_config("${auth_name}Secure") == 1)) {
$string .= '; secure';
}
# HttpOnly is an MS extension. See
# http://msdn.microsoft.com/workshop/author/dhtml/httponly_cookies.asp
if ($r->dir_config("${auth_name}HttpOnly")) {
$string .= '; HttpOnly';
}
return $string;
}
# Retrieve session cookie value
sub key {
my ($self, $r) = @_;
my $auth_name = $r->auth_name;
my $key = "";
my $allcook = ($r->headers_in->{"Cookie"} || "");
my $cookie_name = $self->cookie_name($r);
($key) = $allcook =~ /(?:^|\s)$cookie_name=([^;]*)/;
# Try custom keysource if no cookie is present and Keysource is configured
if (!$key && $auth_name && $r->dir_config("${auth_name}Keysource")) {
# Pull in key text
$key = $self->FetchKeysource($r);
# Non-empty, so use to generate the real session auth key
if ($key) {
$key = CreateSessionAuthKey($key);
}
}
return $key;
}
# 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);
$self->Log($r, ('debug', "authen_cred(): About to cycle authenticators"));
for (my $i = 0; $i < scalar(@authmethods); $i++) {
$self->Log($r, ('debug', "authen_cred(): Checking $authmethods[$i]"));
# Perform auth check
$ret = $authenticators->{$authmethods[$i]}->Authenticate($username, $_[$i]);
# Log any errors, warnings, etc.
($errors = $authenticators->{$authmethods[$i]}->Errors) && ($self->Log($r, $errors));
$self->Log($r, ('debug', "authen_cred(): Done checking $authmethods[$i]"));
if ($ret) {
# Success!
$authenticated++;
# Modify header (add/delete/filter) and cookie
# (add/delete/filter/pass) rules
$self->AlterlistMod($alterlist, $authenticators->{$authmethods[$i]}->{alterlist});
$self->Log($r, ('debug', "authen_cred(): Added alterlist groups for" . join(",", keys %{$alterlist})));
} else {
# Failure! Stop checking auth.
last;
}
}
$self->Log($r, ('debug', "authen_cred(): Done cycling authenticators"));
# If the number of successful authentications equals the number of
# authentication methods, you may pass.
if (($authenticated == scalar(@authmethods)) && ($ret = $self->CreateSession($r, $username, $alterlist))) {
return $ret;
} else {
# Log username (Log handles cleanup of the username and all log lines)
if ($username) {
$self->Log($r, ('error', "LOGIN FAILURE: Authentication failed for \"$username\""));
} else {
$self->Log($r, ('error', "LOGIN FAILURE: Authentication failed for missing or malformed username"));
}
# Lame excuse for brute force protection! Sleep from 0.0 to 1.0 secs
# on failure to ensure someone can DoS us. :) IPFailures tracker needs
# work to have a pre-check, or to have a call out to a script to
# do something (like add the IP to a firewall block table)
usleep(rand(1000000));
}
return undef;
}
# Check session key and return user ID
lib/Apache/AppSamurai.pm view on Meta::CPAN
# for supported ciphers)
$self->Log($r, ('error', "GetSessionConfig(): Could not auto-detect a suitable ${auth_name}SerializeCipher value (Please configure manualy): $!"));
return undef;
}
}
}
# Set a 1hr Timeout if neither Timeout or Expire are set
unless ($sessconfig->{Timeout} || $sessconfig->{Expire}) {
$sessconfig->{Timeout} = 3600;
}
return $sessconfig;
}
# Compute/check server key from server pass, returning key.
sub GetServerKey {
my ($self, $r) = @_;
my $auth_name = ($r->auth_name()) || (die("GetServerKey(): No auth name defined!\n"));
my $dirconfig = $r->dir_config;
my $serverkey = '';
if (exists($dirconfig->{$auth_name . "SessionServerPass"})) {
my $serverpass = $dirconfig->{$auth_name . "SessionServerPass"};
unless ($serverpass =~ s/^\s*([[:print:]]{8,}?)\s*$/$1/s) {
$self->Log($r, ('error', "GetServerKey(): Invalid ${auth_name}SessionServerPass (must be use at least 8 printable characters"));
return undef;
}
if ($serverpass =~ /^(password|serverkey|serverpass|12345678)$/i) {
$self->Log($r, ('error', "GetServerKey(): ${auth_name}SessionServerPass is $1... That is too lousy"));
return undef;
}
unless ($serverkey = HashPass($serverpass)) {
$self->Log($r, ('error', "GetServerKey(): Problem computing server key hash for ${auth_name}SessionServerPass"));
return undef;
}
} elsif (exists($dirconfig->{$auth_name . "SessionServerKey"})) {
$serverkey = $dirconfig->{$auth_name . "SessionServerKey"};
} else {
$self->Log($r, ('error', "GetServerKey(): You must define either ${auth_name}SessionServerPass or ${auth_name}SessionServerKey in your Apache configuration"));
return undef;
}
# Check for valid key format
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;
}
# Gather header and argument items from request to build custom session
# authentication key. Not nearly as secure as random generation, but
# for cookie losing clients (generally automated), it is the only choice.
#
# Synatax:
#
# TYPE:NAME
#
# TYPE - Type of item (header or arg) to pull in
# NAME - Name of header or argument to pull in
#
# The name match is case insensitive, but strict: Only the exact names
# will be used to ensure a consistent key text source. MAKE SURE TO USE
# PER-CLIENT UNIQUE VALUES! The less random the key text source is, the
# easier it can be guessed/hacked. (Once again: Do not use the custom
# key text source feature if you can avoid it!)
sub FetchKeysource {
my ($self, $r) = @_;
my $auth_name = ($r->auth_name()) || (die("FetchKeysource(): No auth name defined!\n"));
my @srcs = $r->dir_config->get("${auth_name}Keysource");
# Return empty, which session key creators MUST interpret as a request
# for a fully randomized key
return '' unless (scalar @srcs);
# Use Apache::Request for immediate access to all arguments.
my $ar = ($MP eq 1) ? Apache::Request->instance($r) : Apache2::Request->new($r);
my ($s, $t);
my $keytext = '';
# Pull values in with very moderate checking
foreach $s (@srcs) {
if ($s =~ /^\s*header:([\w\d\-\_]+)\s*$/i) {
if ($r->headers_in->{$1} and
($t) = $r->headers_in->{$1} =~ /^\s*([\x20-\x7e]+?)\s*$/s) {
$keytext .= $t;
$self->Log($r, ('debug', "FetchKeysource(): Collected $s: " . XHalf($t)));
} else {
$self->Log($r, ('warn', "FetchKeysource(): Missing header field: \"$1\": Can not calculate session key"));
return undef;
}
} elsif ($s =~ /^\s*arg:([\w\d\.\-\_]+)\s*$/i) {
if (($t = $ar->param($1)) && ($t =~ s/^\s*([^\r\n]+?)\s*$/$1/)) {
$keytext .= $t;
$self->Log($r, ('debug', "FetchKeysource(): Collected $s: " . XHalf($t)));
} else {
lib/Apache/AppSamurai.pm view on Meta::CPAN
#}
untie(%{$trak});
} else {
$self->Log($r, ('error', "CheckTracker(): Unknown tracker type $tmod"));
$ret = 0;
}
return $ret;
}
# TODO - GET ALL TRACKER CHECKS AND MANAGEMENT REFACTORED TO OUTSIDE MODULES
# Check given tracker hash ($_[0]) for IP ($_[1]) hitting more than max ($_[2])
# times with no less than in interval ($_[3]) seconds between.
# Updates tracker item.
sub CheckTrackerIPFailures {
my ($trak, $setting, $ip) = @_;
my ($max, $interval, $tc, $tts);
my $time = time();
($max,$interval) = split(':', $setting);
unless (($max) && ($max =~ /^\d+$/) && ($interval) && ($interval =~ /^\d+$/)) {
die("CheckTrackerIPFailures(): FATAL: Bad arguments to IPFailures: \"$setting\"\n");
}
($ip = CheckHostIP($ip)) || (die("CheckTrackerIPFailures(): FATAL: Bad IP address\n"));
# Force defaults of 10 failures in 1 minute or less intervals.
($max) || ($max = 10);
($interval) || ($interval = 60);
# If defined and not timed out: add. Else starts fresh
if ($trak->{$ip}) {
($tts, $tc) = split(':', $trak->{$ip}, 2);
# Sanity check, and pull actual numbers
(($tts =~ s/^ts(\d+)$/$1/) && ($tc =~ s/^cnt(\d+)$/$1/)) || (die("CheckTrackerIPFailures(): FATAL: Corrupt entry for $ip detected\n"));
# Not yet timed out
if (($tts + $interval) > $time) {
$tc++;
$tts = $time;
$trak->{$ip} = join(':', "ts$tts", "cnt$tc");
if ($tc >= $max) {
die("CheckTrackerIPFailures(): RULE VIOLATION: ip=$ip, count=$tc\n");
}
return 1;
}
}
# 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 {
# Set value to
$trak->{$ch} = 'ts' . $time . ":cnt1";
}
return 1;
}
# Check given tracker hash ($_[0]), make sure we have not seen the same
# session authentication key (cookie) before. Stores a hash of session key
# string to minimize security risk.
sub CheckTrackerSessionUnique {
my $trak = shift;
my $ch = HashAny(@_);
my $time = time();
# If defined, the jig is up!
if ($trak->{$ch}) {
die("CheckTrackerSessionUnique(): RULE VIOLATION: sesskey=$ch\n");
} else {
# Set value to
$trak->{$ch} = 'ts' . $time . ":cnt1";
}
return 1;
}
# Check the last access time stamp, and update if needed, for a given session.
# Does NOT update the time if a fixed timeout has been set.
# Returns undef if the atime is more than the session's timeout age
# or if etime is set and is over the session's expire age.
sub CheckTime {
my ($self, $sess) = @_;
my $time = time();
my $tdiff;
my $ret = undef;
# All sessions require at least a floating or fixed timeout!
($sess->{atime} || $sess->{etime}) or return undef;
# Check the hard timeout first, if it exists.
# This short circuits further checking since the hard timeout is king!
if ($sess->{etime}) {
if ($time >= $sess->{etime}) {
return undef;
} else {
$ret = $sess->{etime};
}
}
if ($sess->{atime}) {
lib/Apache/AppSamurai.pm view on Meta::CPAN
return 1;
}
# Check debug setting
sub _debug {
my $self = shift;
my $r = shift;
my $debug = 0;
if ($r->auth_name) {
my $auth_name = $r->auth_name;
if ($r->dir_config("${auth_name}Debug")) {
($r->dir_config("${auth_name}Debug") =~ /^(\d+)$/) && ($debug = $1);
}
}
return $debug;
}
# Filter the output line before logging. Restricts to no more than CharMax
# characters and converts everything matching BlankChars to a space to
# try and protect logging systems and log monitors from attack.
sub FilterLogLine {
my $self = shift;
my $line = (shift || return undef);
my $LogCharMax = 1024;
# Strip surrounding whitespace
$line =~ s/^\s*(.+?)\s*$/$1/s;
# Convert newlines to ', '
$line =~ s/\r?\n/, /sg;
# Check length and truncate if needed
$line = substr($line, 0, $LogCharMax);
# Convert BlankChars matches to blanks
$line =~ s/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f\'\\]+/ /g;
return $line;
}
1; # End of Apache::AppSamurai
__END__
=head1 NAME
Apache::AppSamurai - An Authenticating Mod_Perl Front End
"Protect your master, even if he is without honour...."
=head1 SYNOPSIS
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:
=over 4
=item *
B<Modular authentication> - Uses authentication sub-modules for the easy
addition custom authentication methods
=item *
B<Form based or Basic Auth login> - On the front end, supports standard
form based logins, or optionally Basic Auth login. (For use with automated
systems that can not process a form.)
=item *
B<Apache::Session> - Used for session data handling
=item *
B<Session data encrypted on server> - By default, all session
data encrypted before storing to proxy's filesystem (Uses custom
B<Apache::Session> compatible session generator and session serialization
modules)
=item *
B<Unified mod_perl 1 and 2 support> - One module set supports both
Apache 1.x/mod_perl 1.x and Apache 2.x/mod_perl 2.x
=back
=head1 SESSION STORAGE SECURITY
Server side session data may include sensitive information, including the basic
authentication C<Authorization> header to be sent to the backend server.
(This is just a Base64 encoded value, revealing the username and password
if stolen.)
To protect the data on-disk, Apache::AppSamurai includes
its own HMAC based session ID generator and encrypting session serializer.
(L<Apache::AppSamurai::Session::Generate::HMAC_SHA|Apache::AppSamurai::Session::Generate::HMAC_SHA>
and
L<Apache::AppSamurai::Session::Serialize::CryptBase64|Apache::AppSamurai::Session::Serialize::CryptBase64>
, respectively.)
These modules are configured by default and may be used directly with
Apache::Session, or outside of Apache::AppSamurai if desired.
=head1 USAGE
Almost all options are set using C<PerlSetVar> statements, and can be used
lib/Apache/AppSamurai.pm view on Meta::CPAN
=head3 I<Path> C<PATH>
(Default: /)
The URL path to protect.
=head3 I<Domain> C<DOMAIN>
(Default: not set)
The optional domain to set for all session cookies. Do not configure this
unless you are sure you need it: A misconfigured domain can result in session
stealing.
=head3 I<Satisfy> C<All|Any>
(Default: All)
Set C<require> behaviour within protected areas. Either C<All> to require all
authentication checks to succeed, or C<Any> to require only one to.
=head3 I<Secure> C<0|1>
(Default: 1)
Set to 1 to require the C<secure> flag to be set on the session cookie, forcing
the use of SSL/TLS.
=head3 I<HttpOnly> C<0|1>
(Default: 0)
Set to 1 to require the Microsoft proprietary C<http-only> flag to be set on
session cookies.
=head3 I<LoginDestination> C<PATH>
(Default: undef)
Set an optional hard coded destination URI path all users will be directed to
after login. (While full URLs are allowed, a path starting in / is
recommended.) This setting only applies so form based login. Basic Auth
logins always follow the requested URL.
=head3 I<LogoutDestination> C<PATH>
(Default: undef)
Set an optional hard coded destination URI path all users will be directed to
after logging out. (While full URLs are allowed, a path starting in / is
recommended.) This setting only applies so form based login. Basic Auth
logins always follow the requested URL.
If I<LogoutDestination> is unset and I<LoginDestination> is set,
users will be directed to I<LoginDestination> after logout. (This is
to prevent a user from logging back into the logout URI, which would log them
back out again. Oh the humanity!)
=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.
Example:
PerlSetVar BobAuthBasicAuthMap "2,1=(.+);([^;]+)"
If the user logs into the basic auth popup with the password:
C<myRockinPassword;1234123456> ,the map above will set credential_1 as
C<1234123456> and credential_2 as C<myRockinPassword>, then proceed as if
the same were entered into a form login.
=head3 ADDITIONAL AUTHENTICATION OPTIONS
Authentication submodules usually have one or more required settings. All
settings are passed using PerlSetVar directives with variable names prefixed
with the AuthName and the module's name.
Example:
PerlSetVar BobAuthBasicLoginUrl C<https://bob.org/login>
For AuthName C<Bob>, set the C<LoginUrl> for the C<AuthBasic> authentication
module to C<https://bob.org/login>
See L<Apache::AppSamurai::AuthBase> for general authentication module
information. If you need an authentication type that is not supported
by the authentication modules shipped with AppSamurai, and is not
available as an add on module, please review L<Apache::AppSamurai::AuthBase>
and use the skeletal code from AuthTest.pm, which is included under
/examples/auth/ in the AppSamurai distribution.
=head2 SESSION CONFIGURATION
Each Apache::AppSamurai instance must have its local (proxy server side)
session handling defined.
L<Apache::Session|Apache::Session> provides the majority of the session
framework. Around Apache::Session is wrapped
L<Apache::AppSamurai::Session|Apache::AppSamurai::Session>, which
adds features to allow for more flexible selection of sub-modules.
Most Apache::Session style configuration options can be passed directly to the
session system by prefixing them with C<authnameSession>.
Module selection is slightly different than the default supplied with
Apache::Session. Plain names, without any path or ::, are handled
exactly the same: Modules are loaded from within the Apache::Session
tree. Two additional alternatives are provided:
=over 4
=item *
lib/Apache/AppSamurai.pm view on Meta::CPAN
elapsed since the last connection attempt.
=head3 I<AuthUnique> C<0|1>
(Default: 0)
If set to 1, forces at least one credential to be unique per-login.
(Requires dynamic token or other non-static authentication type.)
=head3 I<SessionUnique> C<0|1>
(Default: 0)
If 1, prohibits a new session from using the same session ID as a previous
session. This is generally only relevant for non-random sessions that use the
C<Keysource> directive to calculate a pseudo-cookie value.
=head1 METHODS
The following methods are to be used directly by Apache. (This is not
a full list of all Apache::AppSamurai methods.)
=head3 authenticate()
Should be configured in the Apache config as the PerlAuthenHandler for
areas protected by Apache::AppSamurai.
C<authenticate()> is called by object reference and expects an Apache request
object as input.C<authenticate()> uses a session authentication key, either
from a cookie or from the optional C<Keysource>, and tries to open the session
tied to the session authentication key.
If the session exists and is valid, the username is extracted from the session
and the method returns C<OK> to allow the request through.
If no key is present, if the session is not present, or if the session
is invalid, a login request is returned. (Either a redirect to a login form,
or in the case of an area set to basic authentication, a
C<401 Authorization Required> code.)
=head3 authorize()
Should be configured in the Apache config as the PerlAuthzHandler for
areas protected by Apache::AppSamurai.
C<authorize()> is called by object reference and expects an Apache request
object as input. It then checks the authorization requirements for the
requested location. In most cases, "require valid-user" is used in conjunction
with the "Satisfy All" Apache::AppSamurai setting. This authorizes any logged
in user to pass. This method could be replaced or expanded at a later date if
more granular authorization is required. (Groups, roles, etc.)
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.
This expects an Apache request handle. It can also take a second
option, which should be a scalar URI path to redirect users to after
logout. C<logout()> attempts to look up and destroy the session tied to the
passed in session authentication key.
Like C<login()>, you may create a special pseudo file named LOGOUT and
use PerlHandler, (or "PerlResponseHandler" for mod_perl 2.x), to map it
to the C<logout()> method. This is particularly handy when paired with
mod_rewrite to map a specific application URI to a pseudo file mapped to
C<logout()> (See L</EXAMPLES> for a sample config that uses this method.)
=head1 EXAMPLES
## This is a partial configuration example showing most supported
## configuration options and a reverse proxy setup. See examples/conf/
## in the Apache::AppSamurai distribution for real-world example configs.
## Apache 1.x/mod_perl 1.x settings are enabled with Apache 2.x/mod_perl 2.x
## config alternatives commented out. ("*FOR MODPERL2 USE:" precedes
## the Apache 2.x/mod_perl 2.x version of any alternative config items.)
## Note that example configs in examples/conf/ use IfDefine to support
## both version sets without having to comment out items. Also note that it
## is far too ugly looking to include in this example.
## General mod_perl setup
# Apache::AppSamurai is always strict, warn, and taint clean. (Unless
# I mucked something up ;)
PerlWarn On
PerlTaintCheck On
PerlModule Apache::Registry
#*FOR MODPERL2 USE:
# PerlSwitches -wT
# PerlModule ModPerl::Registry
# Load the main module and define configuration options for the
# "Example" auth_name
PerlModule Apache::AppSamurai
PerlSetVar ExampleDebug 0
PerlSetVar ExampleCookieName MmmmCookies
PerlSetVar ExamplePath /
PerlSetVar ExampleLoginScript /login.pl
# Defaults to All by may also be Any
#PerlSetVar ExampleSatisty All
# Optional session cookie domain (Avoid unless absolutely needed.)
#PerlSetVar ExampleDomain ".thing.er"
# 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.##
# (Note - See L<Apache::AppSamurai::AuthBasic> for more info)
# Set the URL to send Basic auth checks to
PerlSetVar ExampleAuthBasicLoginUrl "https://ex.amp.le/thing/login"
# Always send Basic authentication header to backend server
PerlSetVar ExampleAuthBasicKeepAuth 1
# Capture cookies from AuthBasic login and set in client browser
PerlSetVar ExampleAuthBasicPassBackCookies 1
# Abort the check unless the "realm" returned by the server matches
PerlSetVar ExampleAuthBasicRequireRealm "blah.bleh.blech"
# Pass the named header directly through to the AuthBasic server
PerlSetVar ExampleAuthBasicUserAgent "header:User-Agent"
## Session storage options ##
# (Note - See L<Apache::AppSamurai::Session> and L<Apache::Session> for
# more information.)
# Inactivity timeout (in seconds)
PerlSetVar ExampleSessionTimeout 1800
# Use the File storage and lock types from Apache::Session
PerlSetVar ExampleSessionStore "File"
PerlSetVar ExampleSessionLock "File"
# File storage options (Relevant only to File storage and lock types)
PerlSetVar ExampleSessionDirectory "/var/www/session/sessions"
PerlSetVar ExampleSessionLockDirectory "/var/www/session/slock"
# Use the Apache::AppSamurai::Session::Generate::HMAC_SHA generator
PerlSetVar ExampleSessionGenerate "AppSamurai/HMAC_SHA"
# Use the Apache::AppSamurai::Session::Serialize::CryptBase64
# data serializer module with Crypt::Rijndael (AES) as the block
# cipher provider
PerlSetVar ExampleSessionSerialize "AppSamurai/CryptBase64"
PerlSetVar ExampleSessionSerializeCipher "Crypt::Rijndael"
# Set the server's encryption passphrase (for use with HMAC session
# generation and/or encrypted session storage)
PerlSetVar ExampleSessionServerPass "This is an example passphrase"
## Tracker storage options ##
# Cleanup tracker entries that have not changed in 1 day
( run in 0.657 second using v1.01-cache-2.11-cpan-39bf76dae61 )