Apache2-AuthCAS

 view release on metacpan or  search on metacpan

lib/Apache2/AuthCAS.pm  view on Meta::CPAN

# Apache2::AuthCAS
# Jason Hitt, March 2007
#
# Apache auth module to protect underlying resources using JA-SIG's Central
# Authentication Service
package Apache2::AuthCAS;

$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);

lib/Apache2/AuthCAS.pm  view on Meta::CPAN

        $self->logMsg("deleting session mapping for service_ticket='$delete_service_ticket'", $LOG_DEBUG);

        my $dbh = $self->dbConnect() or return 0;
        $dbh->do("DELETE FROM " . $self->casConfig("DbSessionTable")
            . " WHERE service_ticket= ?", undef, $delete_service_ticket 
        );
        if ($dbh->err)
        {
            $self->logMsg("error deleting session mapping for service_ticket='$delete_service_ticket' ($DBI::errstr)", $LOG_DEBUG);
        }
    }

    # perform any cleanup that is needed
    $self->cleanup();

    # see if any of our other handlers have specified that they have already
    # sufficiently checked the authenticating user
    my $authenticated = $r->subprocess_env->{'AUTHENTICATED'} || "";
    $self->logMsg("authenticated='$authenticated'", $LOG_DEBUG);
    return (Apache2::Const::OK) if ($authenticated eq "true");

    # Parse the query string to get the ticket, plus any GET variables
    # to rebuild our service string (which is needed for CAS to send the
    # client back to the originating service).

    my %params = $self->parse_query_parameters($uri->query);

    # Check for a proxy receptor call
    if ($params{'pgt'} and $params{'pgtIou'})
    {
        return $self->proxy_receptor($params{'pgtIou'}, $params{'pgt'});
    }

    # Check for a session cookie
    if (my $cookie = $r->headers_in->{'Cookie'})
    {
        # we have a session cookie, so we need to get the session id
        $self->logMsg("cookie found: '$cookie'", $LOG_DEBUG);

        # get session id from the cookie
        my $cookieName = $self->casConfig("SessionCookieName");
        $cookie =~ /.*$cookieName=([^;]+)(\s*;.*|\s*$)/;
        my $sid = $1;
        $self->logMsg(($sid ? "" : "no") . " session id found", $LOG_DEBUG);

        # Check for a valid session id
        if ($sid and defined(my $rc = $self->check_session($sid)))
        {
            return $rc;
        }
    }
    else
    {
        my $service = $self->this_url(1);
        $self->logMsg("no session cookie for service: '$service'", $LOG_DEBUG);
    }

    # No session (or an expired one).  Check for a ticket
    if (my $ticket = $params{'ticket'})
    {
        # validate service ticket through CAS, since no valid cookie was found
        my($error, $user, $pgtiou) = $self->validate_service_ticket($ticket);

        if ($error)
        {
            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

lib/Apache2/AuthCAS.pm  view on Meta::CPAN

    my($self) = @_;

    $self->logMsg("redirecting to remove service ticket from service string", $LOG_INFO);

    $self->setHeader(0, 'Location', $self->this_url());
    return (Apache2::Const::HTTP_MOVED_TEMPORARILY);
}

sub redirect_login($)
{
    my($self) = @_;

    $self->logMsg("start", $LOG_DEBUG);

    my $service = $self->this_url(1);
    $self->logMsg("redirecting to CAS for service: '$service'", $LOG_INFO);

    $service = uri_escape($service);
    $self->setHeader(0, 'Location', "https://"
        . $self->casConfig("Host") . ":" . $self->casConfig("Port")
        . $self->casConfig("LoginUri") . "?service=$service");
    return (Apache2::Const::HTTP_MOVED_TEMPORARILY);
}

sub redirect($;$$)
{
    my($self, $url, $errcode) = @_;

    if ($url)
    {
        my $service = $self->this_url(1);
        $self->logMsg("redirecting to url: '$url' service: '$service'", $LOG_INFO);

        $self->setHeader(0, 'CAS_FILTER_CAS_HOST',      $self->casConfig("Host"));
        $self->setHeader(0, 'CAS_FILTER_CAS_PORT',      $self->casConfig("Port"));
        $self->setHeader(0, 'CAS_FILTER_CAS_LOGIN_URI', $self->casConfig("LoginUri"));
        $self->setHeader(0, 'CAS_FILTER_SERVICE',       $service);

        $self->logMsg("redirecting to error page") if ($errcode);
        $errcode = "" if (!$errcode);

        $service = uri_escape($service);
        $self->setHeader(0, 'Location'
            , "$url?login_url=https://" . $self->casConfig("Host")
            . ":" . $self->casConfig("Port") . $self->casConfig("LoginUri")
            . "?service=$service&errcode=$errcode");
        return (Apache2::Const::HTTP_MOVED_TEMPORARILY);
    }
    else
    {
        $self->logMsg("no redirect URL, displaying message", $LOG_INFO);
        $self->{'request'}->content_type('text/html');
        $self->{'request'}->print("<html><body>service misconfigured</body></html>");
        $self->{'request'}->rflush();
        return (Apache2::Const::HTTP_OK);
    }
}

# params
#     apache request object
#     ticket to be validated
# returns a hash with keys on success
#       'user', 'pgtiou'
# NULL on failure
sub validate_service_ticket($$$)
{
    my($self, $ticket) = @_;

    my $proxy = $self->casConfig("ProxyService") ? "1" : "0";

    my $service = $self->this_url(1);
    $self->logMsg("Validating service ticket '$ticket' for service '$service'", $LOG_DEBUG);

    my $url;
    if ($proxy)
    {
        my $pgtUrl = $self->{'request'}->construct_url();
        $url = $self->casConfig("ProxyValidateUri") . "?pgtUrl=$pgtUrl&";
    }
    else
    {
        $url = $self->casConfig("ServiceValidateUri") . "?";
    }

    $service = uri_escape($service);
    $url .= "service=$service&ticket=$ticket";

    $self->logMsg("request URL: '$url'", $LOG_DEBUG);

    # Net::SSLeay::trace options
    # 0=no debugging, 1=ciphers, 2=trace, 3=dump data
    $Net::SSLeay::trace = ($self->casConfig("LogLevel") >= $LOG_EMERG) ? 3 : 0;

    my($page) = Net::SSLeay::get_https(
        $self->casConfig("Host"), $self->casConfig("Port"), $url);
    $self->logMsg("response page: $page", $LOG_EMERG);

    # if we had some type of connection problem
    if (!defined($page))
    {
        $self->logMsg("error validating service");
        return ($ERROR_CODES{"CAS_CONNECT"});
    }

    my $casResponse = eval { XMLin($page); } || {};

    my($errorMsg, $user, $pgtiou);
    if (my $successBlock = $casResponse->{"cas:authenticationSuccess"})
    {
        $user = $successBlock->{"cas:user"};
        $self->logMsg("valid service ticket, user='$user'", $LOG_DEBUG);

        # only try to get PGTIOU if we are doing proxy stuff
        if ($proxy)
        {
            if ($pgtiou = $successBlock->{"cas:proxyGrantingTicket"})
            {
                $self->logMsg("proxying - pgtiou='$pgtiou'", $LOG_DEBUG);
            }
            else
            {
                $self->logMsg("proxying and no pgtiou in response from CAS", $LOG_ERROR);
                $errorMsg = $ERROR_CODES{"PGT"};
            }
        }



( run in 0.854 second using v1.01-cache-2.11-cpan-140bd7fdf52 )