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 )