Apache2-AuthCAS
view release on metacpan or search on metacpan
lib/Apache2/AuthCAS.pm view on Meta::CPAN
$Apache2::AuthCAS::VERSION = "0.4";
use strict;
use warnings FATAL => 'all';
use Apache2::RequestRec ();
use Apache2::RequestIO ();
use Apache2::RequestUtil ();
#use Apache2::ServerRec ();
use Apache2::Module ();
use Apache2::URI ();
use Apache2::Const -compile => qw(FORBIDDEN HTTP_MOVED_TEMPORARILY OK DECLINED HTTP_OK :log);
use mod_perl2;
use vars qw($INITIALIZED $SESSION_CLEANUP_COUNTER);
use APR::URI;
use Apache2::Log;
use Net::SSLeay;
use MIME::Base64;
use DBI;
use URI::Escape;
use XML::Simple;
# logging flags
my $LOG_ERROR = 0;
my $LOG_WARN = 1;
my $LOG_INFO = 2;
my $LOG_DEBUG = 3;
my $LOG_EMERG = 4;
my %ERROR_CODES = (
"DB" => "Database Service Error",
"PGT" => "CAS Proxy Service Error",
"PGT_RECEPTOR" => "Proxy Receptor Error",
"INVALID_RESPONSE" => "Invalid Service Response",
"INVALID_PGT" => "Invalid Proxy Granting Ticket",
"MISSING_PGT" => "Missing Proxy Granting Ticket",
"CAS_CONNECT" => "CAS couldn't validate service ticket",
);
my %DEFAULTS = (
"Host" => "localhost",
"Port" => "443",
"LoginUri" => "/cas/login",
"LogoutUri" => "/cas/logout",
"ProxyUri" => "/cas/proxy",
"ProxyValidateUri" => "/cas/proxyValidate",
"ServiceValidateUri" => "/cas/serviceValidate",
"LogLevel" => 0,
"PretendBasicAuth" => 0,
"Service" => undef,
"ProxyService" => undef,
"ErrorUrl" => "http://localhost/cas/error/",
"SessionCleanupThreshold" => 10,
"SessionCookieName" => "APACHECAS",
"SessionCookieDomain" => undef,
"SessionCookieSecure" => 0,
"SessionTimeout" => 1800,
"RemoveTicket" => 0,
"NumProxyTickets" => 0,
"DbDriver" => "Pg",
"DbDataSource" => "dbname=apache_cas;host=localhost;port=5432",
"DbSessionTable" => "cas_sessions",
"DbUser" => "cas",
"DbPass" => "cas",
);
# default to 0
$SESSION_CLEANUP_COUNTER = 0 if (!defined($SESSION_CLEANUP_COUNTER));
sub dbConnect($)
{
my($self) = @_;
my $dbh = DBI->connect(
"dbi:" . $self->casConfig("DbDriver")
. ":" . $self->casConfig("DbDataSource"),
$self->casConfig("DbUser"), $self->casConfig("DbPass"),
{ AutoCommit => 1 }
);
if (!defined($dbh))
{
$self->logMsg("db connect error: $DBI::errstr");
return undef;
}
return $dbh;
}
sub getApacheConfig($)
{
my($self) = @_;
$self->{'casConfig'} = Apache2::Module::get_config('Apache2::AuthCAS::Configuration'
, $self->{'request'}->server
, $self->{'request'}->per_dir_config);
# Now add in our defaults
foreach my $key (keys(%DEFAULTS))
{
$self->{'casConfig'}->{$key} = $DEFAULTS{$key}
if !exists($self->{'casConfig'}->{$key});
}
$self->logMsg("Apache Config:", $LOG_DEBUG);
foreach my $key (sort(keys(%{$self->{'casConfig'}})))
{
my $val = $self->casConfig($key) || 'undef';
$self->logMsg(" $key => $val", $LOG_DEBUG);
}
}
sub casConfig($$)
{
my($self, $var) = @_;
return $self->{'casConfig'}->{$var};
}
lib/Apache2/AuthCAS.pm view on Meta::CPAN
return $self->redirect($self->casConfig("ErrorUrl"), $error);
}
# map a new session id to this pgtiou and give the client a cookie
my $sid = $self->create_session($user, $pgtiou, $ticket);
if (!$sid)
{
# if something bad happened, like database unavailability
return $self->redirect($self->casConfig("ErrorUrl"), $ERROR_CODES{"DB"});
}
my $cookie = $self->casConfig("SessionCookieName") . "=$sid;path=/";
if ($self->casConfig("SessionCookieDomain"))
{
$cookie .= ";domain=." . $self->casConfig("SessionCookieDomain");
}
if ($self->casConfig("SessionCookieSecure"))
{
$cookie .= ";secure";
}
# send the cookie to the browser
$self->setHeader(0, 'Set-Cookie', $cookie);
# in case we redirect (considered an "error")
$r->err_headers_out->{"Set-Cookie"} = $cookie;
if ($self->casConfig("ProxyService"))
{
return $self->do_proxy($sid, undef, $user, 1);
}
else
{
$self->setHeader(1, 'CAS_FILTER_USER', $user);
$self->add_basic_auth($user);
# redirect to this same page minus the ticket
return $self->redirect_without_ticket() if ($self->casConfig("RemoveTicket"));
return (Apache2::Const::OK);
}
}
# No valid session, no ticket. Redirect to CAS login
return $self->redirect_login();
}
sub check_session($$$)
{
my($self, $sid) = @_;
# we set up our own session here, so that we don't have to continually
# go through this whole process! we associate a session id with a PGTIOU
# try to get a session record for the session id we received
# session_data - session id, last accessed, netid, pgtiou
if (my($last_accessed, $user, $pgt) = $self->get_session_data($sid))
{
# make sure the session is still valid
if ($last_accessed + $self->casConfig("SessionTimeout") >= time())
{
# session is still valid
$self->logMsg("session '$sid' is still valid", $LOG_DEBUG);
# record the last time the session was accessed
# if something bad happened, like database unavailability
if (!$self->touch_session($sid))
{
return $self->redirect($self->casConfig("ErrorUrl"), $ERROR_CODES{"DB"});
}
if ($self->casConfig("ProxyService"))
{
return $self->do_proxy($sid, $pgt, $user, 0);
}
else
{
$self->setHeader(1, 'CAS_FILTER_USER', $user);
$self->add_basic_auth($user);
return (Apache2::Const::OK);
}
}
else
{
$self->logMsg("session '$sid' has expired", $LOG_DEBUG);
$self->delete_session_data($sid);
}
}
else
{
$self->logMsg("session '$sid' is invalid", $LOG_DEBUG);
}
return undef;
}
sub cleanup()
{
my($self) = @_;
$SESSION_CLEANUP_COUNTER++;
$self->logMsg("counter=$SESSION_CLEANUP_COUNTER", $LOG_DEBUG);
# perform session cleanup
if ($SESSION_CLEANUP_COUNTER == 1)
{
$self->delete_expired_sessions();
}
# reset counter if we have reached our threshold
$SESSION_CLEANUP_COUNTER = 0
if ($SESSION_CLEANUP_COUNTER >= $self->casConfig("SessionCleanupThreshold"));
}
sub add_basic_auth($$)
{
my($self, $user) = @_;
if ($self->casConfig("PretendBasicAuth"))
lib/Apache2/AuthCAS.pm view on Meta::CPAN
{
my($self, $sid) = @_;
$self->logMsg("retrieving session data for sid='$sid'", $LOG_DEBUG);
# retrieve a session object for this session id
my $dbh = $self->dbConnect() or return ();
my($last_accessed, $uid, $pgt) = $dbh->selectrow_array(
"SELECT last_accessed, user_id, pgt FROM "
. $self->casConfig("DbSessionTable")
. " WHERE id = ?"
, undef, $sid
);
$dbh->disconnect();
if (!$dbh->err and $last_accessed)
{
$self->logMsg("session data for sid='$sid':"
. " last_accessed='$last_accessed' uid='$uid'"
. ($pgt ? "pgt='$pgt'" : ""), $LOG_DEBUG);
return ($last_accessed, $uid, $pgt);
}
$self->logMsg("couldn't get session data for sid='$sid'", $LOG_DEBUG);
return ();
}
# delete session
sub delete_session_data($$)
{
my($self, $sid) = @_;
$self->logMsg("deleting session mapping for sid='$sid'", $LOG_DEBUG);
# retrieve a session object for this session id
my $dbh = $self->dbConnect() or return 0;
$dbh->do("DELETE FROM " . $self->casConfig("DbSessionTable") . " WHERE id = ?"
, undef, $sid
);
my $rc = 1;
if ($dbh->err)
{
$self->logMsg("error deleting session mapping for sid='$sid' ($DBI::errstr)", $LOG_DEBUG);
$rc = 0;
}
$dbh->disconnect();
return $rc;
}
# delete expired sessions
sub delete_expired_sessions($)
{
my($self) = @_;
my $oldestValidTime = time() - $self->casConfig("SessionTimeout");
$self->logMsg("deleting sessions older than '$oldestValidTime'", $LOG_DEBUG);
# retrieve a session object for this session id
my $dbh = $self->dbConnect() or return 0;
$dbh->do("DELETE FROM " . $self->casConfig("DbSessionTable")
. " WHERE last_accessed < ?"
, undef, $oldestValidTime
);
my $rc = 1;
if ($dbh->err)
{
$self->logMsg("error deleting expired sessions ($DBI::errstr)", $LOG_ERROR);
$rc = 0;
}
$dbh->disconnect();
return $rc;
}
# place the pgt mapping in the database
sub set_pgt($$$)
{
my($self, $pgtiou, $pgt) = @_;
$self->logMsg("adding map for pgtiou='$pgtiou' pgt='$pgt'", $LOG_DEBUG);
my $dbh = $self->dbConnect() or return 0;
$dbh->do(
"UPDATE " . $self->casConfig("DbSessionTable") . "
SET pgt = ?
WHERE pgtiou = ?"
, undef, $pgt, $pgtiou
);
my $rc = 1;
if ($dbh->err)
{
$self->logMsg("error adding map ($DBI::errstr)", $LOG_ERROR);
$rc = 0;
}
$dbh->disconnect();
return $rc;
}
sub do_proxy($$$$$$)
{
my($self, $sid, $pgt, $user, $removeTicket) = @_;
$self->logMsg("proxying request, sid='$sid'", $LOG_DEBUG);
$self->logMsg("pgt='$pgt'", $LOG_DEBUG) if ($pgt);
if (!$pgt)
{
my(@sessionData) = $self->get_session_data($sid);
lib/Apache2/AuthCAS.pm view on Meta::CPAN
Example configuration with proxiable credentials:
AuthType Apache2::AuthCAS
AuthName "CAS"
PerlAuthenHandler Apache2::AuthCAS->authenticate
require valid-user
CASService "https://yourdomain.com/email/"
CASProxyService "mail.yourdomain.com"
Example configuration with proxiable credentials, using custom database parameters:
AuthType Apache2::AuthCAS
AuthName "CAS"
PerlAuthenHandler Apache2::AuthCAS->authenticate
require valid-user
CASService "https://yourdomain.com/email/"
CASProxyService "mail.yourdomain.com"
CASDbDriver "Oracle
CASDbDataSource "sid=yourdb;host=dbhost.yourdomain.com;port=1521"
CASDbUser "cas_user"
CASDbPass "cas_pass"
CASDbSessionTable "cas_sessions_service1"
=head2 Configuration Options
These are the Apache configuration options, defaults, and descriptions
for Apache2::AuthCAS.
# The CAS server parameters. These should be self explanatory.
CASHost "localhost"
CASPort "443"
CASLoginUri "/cas/login"
CASLogoutUri "/cas/logout"
CASProxyUri "/cas/proxy"
CASProxyValidateUri "/cas/proxyValidate"
CASServiceValidateUri "/cas/serviceValidate"
# The level of logging, ERROR(0) - EMERG(4)
CASLogLevel 0
# Should we set the 'Basic' authentication header?
CASPretendBasicAuth 0
# Where do we redirect if there is an error?
CASErrorUrl "http://localhost/cas/error/"
# Session cleanup threshold (1 in N requests)
# Session cleanup will occur for each Apache thread or process -
# i.e. for 10 processes, it may take as many as 100 requests before
# session cleanup is performed with a threshold of 10)
CASSessionCleanupThreshold 10
# Session cookie configuration for this service
CASSessionCookieDomain ""
CASSessionCookieName "APACHECAS"
CASSessionTimeout 1800
# Should the ticket parameter be removed from the URL?
CASRemoveTicket 0
# Optional override for this service name
CASService ""
# If you are proxying for a backend service you will need to specify
# these parameters. The service is the name of the backend service
# you are proxying for, the receptor is the URL you will listen at
# for pgtiou/pgt mappings from the CAS server, and the final parameter
# specifies how many proxy tickets should be requested for the backend
# service.
CASProxyService ""
CASNumProxyTickets 0
# Database parameters for session and ticket management
CASDbDriver "Pg"
CASDbDataSource "dbname=apache_cas;host=localhost;port=5432"
CASDbSessionTable "cas_sessions"
CASDbUser "cas"
CASDbPass "cas"
=head1 NOTES
Configuration
Any options that are not set in the Apache configuration will default to the
values preconfigured in the Apache2::AuthCAS module. You should explicitly
override those options that do not match your environment.
Database
If you installed this module via CPAN shell, cpan2rpm, or some other automated installer, don't forget to create the session table!
The SQL-92 format of the table is:
CREATE TABLE cas_sessions (
id varchar(32) not null primary key,
last_accessed int8 not null,
user_id varchar(32) not null,
pgtiou varchar(256),
pgt varchar(256)
service_ticket varchar(256)
);
Add indexes and adjust as appropriate for your database and usage.
SSL
Be careful not to use the CASSessionCookieSecure flag with an HTTP resource.
If this flag is set and the protocol is HTTP, then no cookie will get sent
to Apache and Apache2::AuthCAS may act very strange.
Be sure to set CASSessionCookieSecure only on HTTPS resources!
=head1 COMPATIBILITY
This module will only work with mod_perl2. mod_perl1 is not supported.
=head1 SEE ALSO
=head2 Official JA-SIG CAS Website
( run in 0.865 second using v1.01-cache-2.11-cpan-140bd7fdf52 )