# Apache::AuthCAS # David Castro, April 2004 # $Revision: 1.7 $ # # Apache auth module to protect underlying resources using Yale's Central # Authentication service package Apache::AuthCAS; $^W = 1; use diagnostics; use warnings; use strict; use mod_perl qw(StackedHandlers MethodHandlers Authen Authz); use constant MP2 => $mod_perl::VERSION >= 1.99; use vars qw($INITIALIZED $SESSION_CLEANUP_COUNTER); BEGIN { if (MP2) { require Apache::Const; require APR::URI; Apache::Const->import(-compile => qw(FORBIDDEN HTTP_MOVED_TEMPORARILY OK DECLINED HTTP_OK)); } else { require Apache::Constants; Apache::Constants->import(qw(FORBIDDEN HTTP_MOVED_TEMPORARILY OK DECLINED HTTP_OK)); } } use Apache::URI; use Net::SSLeay; use MIME::Base64; use DBI; # logging flags my $LOG_ERROR = "0"; my $LOG_WARN = "1"; my $LOG_INFO = "2"; my $LOG_DEBUG = "3"; my $LOG_INSANE = "4"; my $DEFAULT_LOG_LEVEL = $LOG_ERROR; my $LOG_LEVEL = $DEFAULT_LOG_LEVEL; # the URL the client is redirected to when an error occurs my $DEFAULT_ERROR_URL="http://localhost/cas/error/"; my $ERROR_URL=$DEFAULT_ERROR_URL; # error codes my $DB_ERROR_CODE = "Database Service Error"; my $PGT_ERROR_CODE = "CAS Proxy Service Error"; my $INVALID_ST_ERROR_CODE = "Invalid Service Ticket"; my $INVALID_PGT_ERROR_CODE = "Invalid Proxy Granting Ticket"; my $MISSING_NETID_ERROR_CODE = "CAS failed to return NetID"; my $CAS_CONNECT_ERROR_CODE = "CAS couldn't validate service ticket"; # the URL a client is redirected to after logging in my $SERVICE=""; # the service proxy tickets will be granted for my $PROXY_SERVICE=""; # the host name of the CAS server my $CAS_HOST=""; my $DEVEL_CAS_HOST="devel.localhost"; my $PROD_CAS_HOST="localhost"; # the port number for the CAS server my $CAS_PORT=""; my $DEVEL_CAS_PORT="443"; my $PROD_CAS_PORT="443"; # CAS login URI my $DEFAULT_CAS_LOGIN_URI="/cas/login"; my $CAS_LOGIN_URI=$DEFAULT_CAS_LOGIN_URI; # CAS logout URI my $DEFAULT_CAS_LOGOUT_URI="/cas/logout"; my $CAS_LOGOUT_URI=$DEFAULT_CAS_LOGOUT_URI; # CAS proxy URI my $DEFAULT_CAS_PROXY_URI="/cas/proxy"; my $CAS_PROXY_URI=$DEFAULT_CAS_PROXY_URI; # CAS proxy validate URI my $DEFAULT_CAS_PROXY_VALIDATE_URI="/cas/proxyValidate"; my $CAS_PROXY_VALIDATE_URI=$DEFAULT_CAS_PROXY_VALIDATE_URI; # CAS service validate URI my $DEFAULT_CAS_SERVICE_VALIDATE_URI="/cas/serviceValidate"; my $CAS_SERVICE_VALIDATE_URI=$DEFAULT_CAS_SERVICE_VALIDATE_URI; # parameter used to pass in PGTIOU my $PGT_IOU_PARAM = "pgtIou"; # parameter used to pass in PGT my $PGT_ID_PARAM = "pgtId"; # number of proxy tickets to give the underlying application my $DEFAULT_NUM_PROXY_TICKETS = 1; my $NUM_PROXY_TICKETS = $DEFAULT_NUM_PROXY_TICKETS; # the name of the cookie that will be used for sessions my $DEFAULT_SESSION_COOKIE_NAME = "APACHECAS"; my $SESSION_COOKIE_NAME = $DEFAULT_SESSION_COOKIE_NAME; # the domain the session cookies will be sent for my $DEFAULT_SESSION_COOKIE_DOMAIN = ""; my $SESSION_COOKIE_DOMAIN = ""; # the max time before a session expires (in seconds) my $DEFAULT_SESSION_TIMEOUT = 1800; my $SESSION_TIMEOUT = $DEFAULT_SESSION_TIMEOUT; # the name of the DBI database driver my $DB_DRIVER = ""; my $DEVEL_DB_DRIVER = "Pg"; my $PROD_DB_DRIVER = "Pg"; # the host name of the database server my $DB_HOST = ""; my $DEVEL_DB_HOST = "devel.localhost"; my $PROD_DB_HOST = "localhost"; # the port number of the database server my $DB_PORT = ""; my $DEVEL_DB_PORT = "5432"; my $PROD_DB_PORT = "5432"; # the name of the database for sessions/pgtiou mapping my $DB_NAME = ""; my $DEVEL_DB_NAME = "apache_cas"; my $PROD_DB_NAME = "apache_cas"; # the name of the session table my $DB_SESSION_TABLE = ""; my $DEVEL_DB_SESSION_TABLE = "cas_sessions"; my $PROD_DB_SESSION_TABLE = "cas_sessions"; # the name of the pgtiou to pgt mapping table my $DB_PGTIOU_TABLE = ""; my $DEVEL_DB_PGTIOU_TABLE = "cas_pgtiou_to_pgt"; my $PROD_DB_PGTIOU_TABLE = "cas_pgtiou_to_pgt"; # the user to connnect to the database with my $DB_USER = ""; my $DEVEL_DB_USER = "develuser"; my $PROD_DB_USER = "produser"; # the password to connect to the databse with my $DB_PASS = ""; my $DEVEL_DB_PASS = "develpass"; my $PROD_DB_PASS = "prodpass"; # whether or not we want redirect magic to remove service ticket from URL my $DEFAULT_REMOVE_TICKET = "0"; my $REMOVE_TICKET = $DEFAULT_REMOVE_TICKET; # are we running with production config, or other? my $PRODUCTION = "0"; # 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 for a threshold of 10) my $SESSION_CLEANUP_THRESHOLD = "10"; # when set to true, this module will attempt to make the underlying authz # mechanism believe that "Basic" authentication has occurred my $PRETEND_BASIC_AUTH = "0"; # this will turn on initialization that will only occur once for each apache # process, meaning that changes will require a restart. This has the benefit # of speed for high-load sites, but will typically not be what you want. The # config of the first resource protected by AuthCAS will persist for the apache # process that served up the request my $STATIC_INITIALIZATION = "0"; if (!defined($INITIALIZED)) { # default to not initialized $INITIALIZED = 0; } if (!defined($SESSION_CLEANUP_COUNTER)) { # default to 0 $SESSION_CLEANUP_COUNTER = 0; } my $tmp; sub initialize($$) { my $self = shift; my $r = shift; # get all of our settings from the server config # logging if ($tmp = $r->dir_config("CASLogLevel")) { $LOG_LEVEL = $tmp; } else { # default $LOG_LEVEL = $DEFAULT_LOG_LEVEL; } Apache->warn("$$: CAS: initialize()") unless ($LOG_LEVEL < $LOG_INFO); # determine if we are running in production if ($tmp = $r->dir_config("CASProduction")) { if (defined($tmp) and ($tmp ne "") and (($tmp eq "1") or ($tmp =~ /true/i))) { $PRODUCTION = 1; } Apache->warn("$$: CAS: initialize(): setting CASProduction to $PRODUCTION") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $PRODUCTION = 0; } # error pages if ($tmp = $r->dir_config("CASErrorURL")) { $ERROR_URL = $tmp; Apache->warn("$$: CAS: initialize(): setting CASErrorURL to $ERROR_URL") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $ERROR_URL = $DEFAULT_ERROR_URL; } # service settings if ($tmp = $r->dir_config("CASService")) { $SERVICE = $tmp; Apache->warn("$$: CAS: initialize(): setting CASService to $SERVICE") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $SERVICE = ""; } if ($tmp = $r->dir_config("CASProxyService")) { $PROXY_SERVICE = $tmp; Apache->warn("$$: CAS: initialize(): setting CASProxyService to $PROXY_SERVICE") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $PROXY_SERVICE = ""; } # CAS server settings if ($tmp = $r->dir_config("CASHost")) { $CAS_HOST = $tmp; Apache->warn("$$: CAS: initialize(): setting cas host to $CAS_HOST") unless ($LOG_LEVEL < $LOG_INFO); } elsif ($PRODUCTION) { $CAS_HOST = $PROD_CAS_HOST; Apache->warn("$$: CAS: initialize(): setting cas host to $CAS_HOST") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $CAS_HOST = $DEVEL_CAS_HOST; Apache->warn("$$: CAS: initialize(): setting cas host to $CAS_HOST") unless ($LOG_LEVEL < $LOG_INFO); } if ($tmp = $r->dir_config("CASPort")) { $CAS_PORT = $tmp; Apache->warn("$$: CAS: initialize(): setting cas port to $CAS_PORT") unless ($LOG_LEVEL < $LOG_INFO); } elsif ($PRODUCTION) { $CAS_PORT = $PROD_CAS_PORT; Apache->warn("$$: CAS: initialize(): setting cas port to $CAS_PORT") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $CAS_PORT = $DEVEL_CAS_PORT; Apache->warn("$$: CAS: initialize(): setting cas port to $CAS_PORT") unless ($LOG_LEVEL < $LOG_INFO); } # CAS URIs if ($tmp = $r->dir_config("CASLoginURI")) { $CAS_LOGIN_URI = $tmp; Apache->warn("$$: CAS: initialize(): setting CASLoginURI to $CAS_LOGIN_URI") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $CAS_LOGIN_URI = $DEFAULT_CAS_LOGIN_URI; } if ($tmp = $r->dir_config("CASLogoutURI")) { $CAS_LOGOUT_URI = $tmp; Apache->warn("$$: CAS: initialize(): setting CASLogoutURI to $CAS_LOGOUT_URI") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $CAS_LOGOUT_URI = $DEFAULT_CAS_LOGOUT_URI; } if ($tmp = $r->dir_config("CASProxyURI")) { $CAS_PROXY_URI = $tmp; Apache->warn("$$: CAS: initialize(): setting CASProxyURI to $CAS_PROXY_URI") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $CAS_PROXY_URI = $DEFAULT_CAS_PROXY_URI; } if ($tmp = $r->dir_config("CASProxyValidateURI")) { $CAS_PROXY_VALIDATE_URI = $tmp; Apache->warn("$$: CAS: initialize(): setting CASProxyValidateURI to $CAS_PROXY_VALIDATE_URI") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $CAS_PROXY_VALIDATE_URI = $DEFAULT_CAS_PROXY_VALIDATE_URI; } if ($tmp = $r->dir_config("CASServiceValidateURI")) { $CAS_SERVICE_VALIDATE_URI = $tmp; Apache->warn("$$: CAS: initialize(): setting CASServiceValidateURI to $CAS_SERVICE_VALIDATE_URI") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $CAS_SERVICE_VALIDATE_URI = $DEFAULT_CAS_SERVICE_VALIDATE_URI; } # number of proxy tickets to add to the request if ($tmp = $r->dir_config("CASNumProxyTickets")) { $NUM_PROXY_TICKETS = $tmp; Apache->warn("$$: CAS: initialize(): setting CASNumProxyTickets to $NUM_PROXY_TICKETS") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $NUM_PROXY_TICKETS = $DEFAULT_NUM_PROXY_TICKETS; } # session settings if ($tmp = $r->dir_config("CASSessionCookieName")) { $SESSION_COOKIE_NAME = $tmp; Apache->warn("$$: CAS: initialize(): setting CASSessionCookieName to $SESSION_COOKIE_NAME") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $SESSION_COOKIE_NAME = $DEFAULT_SESSION_COOKIE_NAME; } if ($tmp = $r->dir_config("CASSessionCookieDomain")) { $SESSION_COOKIE_DOMAIN = $tmp; Apache->warn("$$: CAS: initialize(): setting CASSessionCookieDomain to $SESSION_COOKIE_DOMAIN") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $SESSION_COOKIE_DOMAIN = $DEFAULT_SESSION_COOKIE_DOMAIN; } if ($tmp = $r->dir_config("CASSessionTimeout")) { $SESSION_TIMEOUT= $tmp; Apache->warn("$$: CAS: initialize(): setting CASSessionTimeout to $SESSION_TIMEOUT") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $SESSION_TIMEOUT= $DEFAULT_SESSION_TIMEOUT; } # database settings if ($tmp = $r->dir_config("CASDatabaseDriver")) { $DB_DRIVER = $tmp; Apache->warn("$$: CAS: initialize(): setting database driver to $DB_DRIVER") unless ($LOG_LEVEL < $LOG_INFO); } elsif ($PRODUCTION) { $DB_DRIVER = $PROD_DB_DRIVER; Apache->warn("$$: CAS: initialize(): setting database driver to $DB_DRIVER") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $DB_DRIVER = $DEVEL_DB_DRIVER; Apache->warn("$$: CAS: initialize(): setting database driver to $DB_DRIVER") unless ($LOG_LEVEL < $LOG_INFO); } if ($tmp = $r->dir_config("CASDatabaseHost")) { $DB_HOST = $tmp; Apache->warn("$$: CAS: initialize(): setting database host to $DB_HOST") unless ($LOG_LEVEL < $LOG_INFO); } elsif ($PRODUCTION) { $DB_HOST = $PROD_DB_HOST; Apache->warn("$$: CAS: initialize(): setting database host to $DB_HOST") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $DB_HOST = $DEVEL_DB_HOST; Apache->warn("$$: CAS: initialize(): setting database host to $DB_HOST") unless ($LOG_LEVEL < $LOG_INFO); } if ($tmp = $r->dir_config("CASDatabasePort")) { $DB_PORT = $tmp; Apache->warn("$$: CAS: initialize(): setting database port to $DB_PORT") unless ($LOG_LEVEL < $LOG_INFO); } elsif ($PRODUCTION) { $DB_PORT = $PROD_DB_PORT; Apache->warn("$$: CAS: initialize(): setting database port to $DB_PORT") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $DB_PORT = $DEVEL_DB_PORT; Apache->warn("$$: CAS: initialize(): setting database port to $DB_PORT") unless ($LOG_LEVEL < $LOG_INFO); } if ($tmp = $r->dir_config("CASDatabaseName")) { $DB_NAME = $tmp; Apache->warn("$$: CAS: initialize(): setting database name to $DB_NAME") unless ($LOG_LEVEL < $LOG_INFO); } elsif ($PRODUCTION) { $DB_NAME = $PROD_DB_NAME; Apache->warn("$$: CAS: initialize(): setting database name to $DB_NAME") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $DB_NAME = $DEVEL_DB_NAME; Apache->warn("$$: CAS: initialize(): setting database name to $DB_NAME") unless ($LOG_LEVEL < $LOG_INFO); } if ($tmp = $r->dir_config("CASDatabaseSessionTable")) { $DB_SESSION_TABLE = $tmp; Apache->warn("$$: CAS: initialize(): setting session table to $DB_SESSION_TABLE") unless ($LOG_LEVEL < $LOG_INFO); } elsif ($PRODUCTION) { $DB_SESSION_TABLE = $PROD_DB_SESSION_TABLE; Apache->warn("$$: CAS: initialize(): setting session table to $DB_SESSION_TABLE") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $DB_SESSION_TABLE = $DEVEL_DB_SESSION_TABLE; Apache->warn("$$: CAS: initialize(): setting session table to $DB_SESSION_TABLE") unless ($LOG_LEVEL < $LOG_INFO); } if ($tmp = $r->dir_config("CASDatabasePGTIOUTable")) { $DB_PGTIOU_TABLE = $tmp; Apache->warn("$$: CAS: initialize(): setting pgtiou table to $DB_PGTIOU_TABLE") unless ($LOG_LEVEL < $LOG_INFO); } elsif ($PRODUCTION) { $DB_PGTIOU_TABLE = $PROD_DB_PGTIOU_TABLE; Apache->warn("$$: CAS: initialize(): setting pgtiou table to $DB_PGTIOU_TABLE") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $DB_PGTIOU_TABLE = $DEVEL_DB_PGTIOU_TABLE; Apache->warn("$$: CAS: initialize(): setting pgtiou table to $DB_PGTIOU_TABLE") unless ($LOG_LEVEL < $LOG_INFO); } if ($tmp = $r->dir_config("CASDatabaseUser")) { $DB_USER = $tmp; Apache->warn("$$: CAS: initialize(): setting database user to $DB_USER") unless ($LOG_LEVEL < $LOG_INFO); } elsif ($PRODUCTION) { $DB_USER = $PROD_DB_USER; Apache->warn("$$: CAS: initialize(): setting database user to $DB_USER") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $DB_USER = $DEVEL_DB_USER; Apache->warn("$$: CAS: initialize(): setting database user to $DB_USER") unless ($LOG_LEVEL < $LOG_INFO); } if ($tmp = $r->dir_config("CASDatabasePass")) { $DB_PASS = $tmp; Apache->warn("$$: CAS: initialize(): setting database password to $DB_PASS") unless ($LOG_LEVEL < $LOG_INFO); } elsif ($PRODUCTION) { $DB_PASS = $PROD_DB_PASS; Apache->warn("$$: CAS: initialize(): setting database password to $DB_PASS") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $DB_PASS = $DEVEL_DB_PASS; Apache->warn("$$: CAS: initialize(): setting database password to $DB_PASS") unless ($LOG_LEVEL < $LOG_INFO); } if (!$DB_HOST or !$DB_PORT or !$DB_NAME or !$DB_USER or !$DB_PASS) { Apache->warn("$$: CAS: initialize(): database not properly configured. Please specify: 'CASDatabaseHost', 'CASDatabasePort', 'CASDatabaseName', 'CASDatabaseUser', 'CASDatabasePassword'"); } if ($tmp = $r->dir_config("CASRemoveTicket")) { $REMOVE_TICKET = $tmp; Apache->warn("$$: CAS: initialize(): setting CASRemoveTicket to $REMOVE_TICKET") unless ($LOG_LEVEL < $LOG_INFO); } else { # default $REMOVE_TICKET = $DEFAULT_REMOVE_TICKET; } # specify that we have been successfully initialized $INITIALIZED = 1; } # used for underlying services that need proxy tickets (PTs) sub authenticate($$) { my $self = shift; my $r = shift; my $tmp; # Only authenticate the first internal request return (MP2 ? Apache::OK : Apache::Constants::OK) unless $r->is_initial_req; # get our configuration, unless we already have and we are running static unless ($STATIC_INITIALIZATION and $INITIALIZED) { $self->initialize($r); } # perform any cleanup that is needed $self->cleanup($r); Apache->warn("$$: CAS: authenticate()") unless ($LOG_LEVEL < $LOG_DEBUG); # see if any of our other handlers have specified that they have already # sufficiently checked the authenticating user my $authenticated = $r->subprocess_env->{'AUTHENTICATED'} || ""; Apache->warn("$$: CAS: authenticated='$authenticated'") unless ($LOG_LEVEL < $LOG_DEBUG); if ($authenticated eq "true") { return (MP2 ? Apache::OK : Apache::Constants::OK); } # 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). # grab the uri that was requested my $uri = $r->parsed_uri; my $path = $uri->path(); my $unparsed = $uri->unparse(); my $query = $uri->query || ""; if ($query) { $path .= "?$query"; } elsif ($unparsed =~ /\?$/) { $path .= "?"; } # grab out the params we need to use for tests my $ticket = ""; my $pgt = ""; my $pgtiou = ""; if ($query ne "") { my @params = split(/&/, $query); foreach (@params) { my ($key, $value) = split(/=/, $_); Apache->warn("$$: CAS: authenticate(): PARAMS: '$key' => '$value'") unless ($LOG_LEVEL < $LOG_DEBUG); if ($key eq "ticket") { Apache->warn("$$: CAS: authenticate(): ticket found: '$value'") unless ($LOG_LEVEL < $LOG_DEBUG); $ticket = $value || ""; } if ($key eq $PGT_ID_PARAM) { Apache->warn("$$: CAS: authenticate(): PGTID found: '$value'") unless ($LOG_LEVEL < $LOG_DEBUG); $pgt = $value; } if ($key eq $PGT_IOU_PARAM) { Apache->warn("$$: CAS: authenticate(): PGTIOU found: '$value'") unless ($LOG_LEVEL < $LOG_DEBUG); $pgtiou = $value; } } } # this is the proxy receptor, should only enter here when CAS sends us the # PGTIOU and the PGT if (($pgtiou ne "") and ($pgt ne "")) { Apache->warn("$$: CAS: authenticate(): proxy receptor invoked with '$pgtiou' => '$pgt'") unless ($LOG_LEVEL < $LOG_DEBUG); # save the pgtiou/pgt mapping if (!$self->set_pgt($pgtiou, $pgt)) { Apache->warn("$$: CAS: authenticate(): couldn't save '$pgtiou' => '$pgt', redirecting to error page") unless ($LOG_LEVEL < $LOG_ERROR); return $self->redirect($r, $ERROR_URL, $DB_ERROR_CODE); } Apache->warn("$$: CAS: authenticate(): saved '$pgtiou' => '$pgt'") unless ($LOG_LEVEL < $LOG_DEBUG); # return a successful response to CAS # have to not let request fall through to real content here $r->push_handlers(PerlResponseHandler => \&send_proxysuccess); } # else treat this as a normal authentication request # determine any session cookies/session id we may have recieved my ($cookie, $sid) = ("", ""); if (!defined($cookie = $r->header_in('Cookie'))) { # if we don't have a session cookie, the user can't be valid Apache->warn("$$: CAS: authenticate(): no session cookie found") unless ($LOG_LEVEL < $LOG_DEBUG); my $service; if ($SERVICE eq "") { # use the current URL as the service $service = $self->this_url_encoded($r); } else { # use the static entry point into this service $service = $self->urlEncode($SERVICE); } Apache->warn("$$: CAS: authenticate(): no session cookie for service: '$service'") unless ($LOG_LEVEL < $LOG_DEBUG); } else { # we have a session cookie, so we need to get the session id Apache->warn("$$: CAS: authenticate(): cookie found: '$cookie'") unless ($LOG_LEVEL < $LOG_DEBUG); # get session id from the cookie $cookie =~ /.*$SESSION_COOKIE_NAME=([^;]+)(\s*;.*|\s*$)/; $sid = $1 || ""; if (!$sid) { # no sessions id in cookie? Apache->warn("$$: CAS: authenticate(): no session id found in cookie: '$cookie'") unless ($LOG_LEVEL < $LOG_DEBUG); } else { Apache->warn("$$: CAS: authenticate(): session id '$sid' found in cookie: '$cookie'") unless ($LOG_LEVEL < $LOG_DEBUG); } } # if we don't have a session id and there is no service ticket, redirect # the user to CAS (they have never been authenticated) if (!$ticket and !$sid) { Apache->warn("$$: CAS: authenticate(): no ticket and no cookie, redirecting to login") unless ($LOG_LEVEL < $LOG_DEBUG); return $self->redirect_login($r); } # note: we should have a session id or a service ticket. # if we have a session id my $user=""; if ($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 recieved my @session_data; # session id, last accessed, netid, pgtiou if (@session_data = $self->get_session_data($sid)) { Apache->warn("$$: CAS: authenticate(): session data: ".join(",",@session_data)) unless ($LOG_LEVEL < $LOG_DEBUG); # we found the session id in out session hash my $last_accessed = $session_data[1]; # make sure the session is still valid Apache->warn("$$: CAS: authenticate(): session last_accessed=$last_accessed") unless ($LOG_LEVEL < $LOG_DEBUG); if ($last_accessed + $SESSION_TIMEOUT >= time()) { # session is still valid Apache->warn("$$: CAS: authenticate(): session '$sid' is still valid") unless ($LOG_LEVEL < $LOG_DEBUG); # record the last time the session was accessed $session_data[1] = time(); Apache->warn("$$: CAS: authenticate(): setting last accessed time to '".time()."'") unless ($LOG_LEVEL < $LOG_DEBUG); # if something bad happened, like database unavailability if (!$self->set_session_data(@session_data)) { Apache->warn("$$: CAS: authenticate(): problem saving session data, redirecting to the error page") unless ($LOG_LEVEL < $LOG_ERROR); return $self->redirect($r, $ERROR_URL, $DB_ERROR_CODE); } else { Apache->warn("$$: CAS: authenticate(): saved session data: ".join(",",@session_data)) unless ($LOG_LEVEL < $LOG_DEBUG); } # set the pgtiou $user = $session_data[2]; $pgtiou = $session_data[3]; if ($PROXY_SERVICE) { return $self->do_proxy($r, $sid, $pgtiou, $user, 0); } else { # no proxy stuff, so we are done Apache->warn("$$: CAS: authenticate(): no proxy stuff, we are done") unless ($LOG_LEVEL < $LOG_DEBUG); Apache->warn("$$: CAS: authenticate(): setting header CAS_FILTER_USER=$user") unless ($LOG_LEVEL < $LOG_DEBUG); $r->header_in('CAS_FILTER_USER', $user); if ($PRETEND_BASIC_AUTH) { # setup this up for underlying authz modules that rely on Basic auth having been performed $r->header_in('Authorization', "Basic " . encode_base64($user . ":DUMMYPASS")); $r->user($user); $r->connection->user($user); $r->connection->auth_type("Basic"); } return (MP2 ? Apache::OK : Apache::Constants::OK); } } else { Apache->warn("$$: CAS: authenticate(): session '$sid' has expired") unless ($LOG_LEVEL < $LOG_DEBUG); if (!$self->delete_session_data($sid)) { Apache->warn("$$: CAS: authenticate(): couldn't delete expired session id='$sid'") unless ($LOG_LEVEL < $LOG_WARN); } Apache->warn("$$: CAS: authenticate(): deleted expired session '$sid'") unless ($LOG_LEVEL < $LOG_DEBUG); $sid = ""; } } else { Apache->warn("$$: CAS: authenticate(): session '$sid' is invalid") unless ($LOG_LEVEL < $LOG_DEBUG); $sid = ""; } } # note: not an else if, because we may find an invalid session id and # fallback to ticket # if we have a service ticket if (($sid eq "") and ($ticket ne "")) { # validate service ticket through CAS, since no valid cookie was found my %properties = $self->validate_service_ticket($r, $ticket, $PROXY_SERVICE ?"1":"0"); if ($properties{'error'}) { # error occurred validating service ticket return $self->redirect($r, $ERROR_URL, $properties{'error'}); } else { Apache->warn("$$: CAS: authenticate(): valid service ticket '$ticket'") unless ($LOG_LEVEL < $LOG_DEBUG); } $pgtiou = $properties{'pgtiou'} || ""; $user = $properties{'user'} || ""; # we should get back a netid when validating a service ticket if ($user eq "") { return $self->redirect($r, $ERROR_URL, $MISSING_NETID_ERROR_CODE); } $sid = &create_session_id(); Apache->warn("$$: CAS: authenticate(): setting sid='$sid' for netid='$user'") unless ($LOG_LEVEL < $LOG_DEBUG); # map a new session id to this pgtiou and give the client a cookie my $time = time(); Apache->warn("$$: CAS: authenticate(): trying to save session data: ".join(",",$sid, $time, $user, $pgtiou)) unless ($LOG_LEVEL < $LOG_DEBUG); if (!$self->set_session_data($sid, $time, $user, $pgtiou)) { # if something bad happened, like database unavailability Apache->warn("$$: CAS: authenticate(): problem saving session data, redirecting to the error page") unless ($LOG_LEVEL < $LOG_ERROR); return $self->redirect($r, $ERROR_URL, $DB_ERROR_CODE); } else { Apache->warn("$$: CAS: authenticate(): saved session data: ".join(",",$sid, $time, $user, $pgtiou)) unless ($LOG_LEVEL < $LOG_DEBUG); } Apache->warn("$$: CAS: authenticate(): sending session cookie") unless ($LOG_LEVEL < $LOG_DEBUG); my $cookie = "$SESSION_COOKIE_NAME=$sid;path=/"; if ($SESSION_COOKIE_DOMAIN ne "") { $cookie .= ";domain=.$SESSION_COOKIE_DOMAIN"; } # send the cookie to the browser $r->header_out("Set-Cookie" => $cookie); # in case we redirect (considered an "error") $r->err_header_out("Set-Cookie" => $cookie); } else { Apache->warn("$$: CAS: authenticate(): no valid session id or ticket") unless ($LOG_LEVEL < $LOG_DEBUG); return $self->redirect_login($r); } Apache->warn("$$: CAS: authenticate(): got user: '$user'") unless ($LOG_LEVEL < $LOG_DEBUG); Apache->warn("$$: CAS: authenticate(): got PGTIOU: '$pgtiou'") unless ($LOG_LEVEL < $LOG_DEBUG); if ($PROXY_SERVICE) { return $self->do_proxy($r, $sid, $pgtiou, $user, 1); } else { # no proxy stuff, so we are done Apache->warn("$$: CAS: authenticate(): no proxy stuff, so we are done") unless ($LOG_LEVEL < $LOG_DEBUG); # redirect to this same page minus the ticket if (($REMOVE_TICKET eq "true") || ($REMOVE_TICKET eq "1")) { Apache->warn("$$: CAS: authenticate(): setting header CAS_FILTER_USER=$user") unless ($LOG_LEVEL < $LOG_DEBUG); $r->header_in('CAS_FILTER_USER', $user); if ($PRETEND_BASIC_AUTH) { # setup this up for underlying authz modules that rely on Basic auth having been performed $r->header_in('Authorization', "Basic " . encode_base64($user . ":DUMMYPASS")); $r->user($user); $r->connection->user($user); $r->connection->auth_type("Basic"); } Apache->warn("$$: CAS: authenticate(): trying to remove service ticket from URI") unless ($LOG_LEVEL < $LOG_DEBUG); return $self->redirect_without_ticket($r); } else { Apache->warn("$$: CAS: authenticate(): setting header CAS_FILTER_USER=$user") unless ($LOG_LEVEL < $LOG_DEBUG); $r->header_in('CAS_FILTER_USER', $user); if ($PRETEND_BASIC_AUTH) { # setup this up for underlying authz modules that rely on Basic auth having been performed $r->header_in('Authorization', "Basic " . encode_base64($user . ":DUMMYPASS")); $r->user($user); $r->connection->user($user); $r->connection->auth_type("Basic"); } Apache->warn("$$: CAS: authenticate(): not trying to remove service ticket from URI") unless ($LOG_LEVEL < $LOG_DEBUG); return (MP2 ? Apache::OK : Apache::Constants::OK); } } # failed if we got this far, but shouldn't return (MP2 ? Apache::FORBIDDEN : Apache::Constants::FORBIDDEN); } sub cleanup($$) { my $self = shift; my $r = shift; $SESSION_CLEANUP_COUNTER++; Apache->warn("$$: CAS: cleanup(): counter=$SESSION_CLEANUP_COUNTER") unless ($LOG_LEVEL < $LOG_DEBUG); # perform session cleanup if ($SESSION_CLEANUP_COUNTER == 1) { Apache->warn("$$: CAS: initialize(): performing session cleanup"); $self->delete_expired_sessions(); Apache->warn("$$: CAS: initialize(): performing pgt/pgtiou cleanup"); $self->delete_expired_pgts(); } # reset counter if we have reached our threshold if ($SESSION_CLEANUP_COUNTER >= $SESSION_CLEANUP_THRESHOLD) { # reset counter $SESSION_CLEANUP_COUNTER = 0; } } sub redirect_without_ticket($$) { my $self = shift; my $r = shift; Apache->warn("$$: CAS: redirect_without_ticket(): redirecting to remove service ticket from service string") unless ($LOG_LEVEL < $LOG_INFO); # this_url() strips the service ticket my $url = $self->this_url($r); $r->header_out("Location" => $url); return (MP2 ? Apache::HTTP_MOVED_TEMPORARILY : Apache::Constants::HTTP_MOVED_TEMPORARILY); } sub redirect_login($$) { my $self = shift; my $r = shift; Apache->warn("$$: CAS: redirect_login()") unless ($LOG_LEVEL < $LOG_DEBUG); my $service; if ($SERVICE eq "") { # use the current URL as the service $service = $self->this_url_encoded($r); } else { # use the static entry point into this service $service = $self->urlEncode($SERVICE); } Apache->warn("$$: CAS: redirect_login(): redirecting to CAS for service: '$service'") unless ($LOG_LEVEL < $LOG_INFO); my $redirect_url = "https://$CAS_HOST:$CAS_PORT$CAS_LOGIN_URI?service=$service"; $r->header_out("Location" => $redirect_url); return (MP2 ? Apache::HTTP_MOVED_TEMPORARILY : Apache::Constants::HTTP_MOVED_TEMPORARILY); } sub redirect($$) { my $self = shift; my $r = shift; my $url = shift || ""; my $errcode = shift || ""; Apache->warn("$$: CAS: redirect()") unless ($LOG_LEVEL < $LOG_DEBUG); if ($url) { my $service; if ($SERVICE eq "") { # use the current URL as the service $service = $self->this_url_encoded($r); Apache->warn("$$: CAS: redirect(): using self as service") unless ($LOG_LEVEL < $LOG_DEBUG); } else { # use the static entry point into this service $service = $self->urlEncode($SERVICE); Apache->warn("$$: CAS: redirect(): using configured service") unless ($LOG_LEVEL < $LOG_DEBUG); } $r->header_out("CAS_FILTER_CAS_HOST", $CAS_HOST); $r->header_out("CAS_FILTER_CAS_PORT", $CAS_PORT); $r->header_out("CAS_FILTER_CAS_LOGIN_URI", $CAS_LOGIN_URI); $r->header_out("CAS_FILTER_SERVICE", $service); Apache->warn("$$: CAS: redirect(): redirecting to url: '$url' service: '$service'") unless ($LOG_LEVEL < $LOG_INFO); $r->header_out("Location" => "$url?login_url=https://$CAS_HOST:$CAS_PORT$CAS_LOGIN_URI&service=$service&errcode=$errcode"); return (MP2 ? Apache::HTTP_MOVED_TEMPORARILY : Apache::Constants::HTTP_MOVED_TEMPORARILY); } else { Apache->warn("$$: CAS: redirect(): no redirect URL, displaying message") unless ($LOG_LEVEL < $LOG_INFO); $r->content_type ('text/html'); $r->print("
service misconfigured"); $r->rflush; return (MP2 ? Apache::HTTP_OK : Apache::Constants::HTTP_OK); } } # params # apache request object # ticket to be validated # 1 or 0, whether we need proxy tickets # returns a hash with keys on success # 'user', 'pgtiou' # NULL on failure sub validate_service_ticket($$) { my $self = shift; my $r = shift; my $ticket = shift; my $proxy = shift; Apache->warn("$$: CAS: validate_service_ticket(): validating service ticket '$ticket' through CAS") unless ($LOG_LEVEL < $LOG_DEBUG); my %properties; my $service; if ($SERVICE eq "") { # use the current URL as the service $service = $self->this_url_encoded($r); } else { # use the static entry point into this service $service = $self->urlEncode($SERVICE); } Apache->warn("$$: CAS: validate_service_ticket(): requesting validation for service: '$service'") unless ($LOG_LEVEL < $LOG_DEBUG); my $tmp; # FIXME - diff urls for proxy vs. none? if ($proxy) { $tmp = $CAS_PROXY_VALIDATE_URI . "?service=$service&ticket=$ticket&pgtUrl=$service"; } else { $tmp = $CAS_SERVICE_VALIDATE_URI . "?service=$service&ticket=$ticket"; } Apache->warn("$$: CAS: validate_service_ticket(): request URL: '$tmp'") unless ($LOG_LEVEL < $LOG_DEBUG); if ($LOG_LEVEL >= $LOG_INSANE) { $Net::SSLeay::trace = 3; # 0=no debugging, 1=ciphers, 2=trace, 3=dump data } else { $Net::SSLeay::trace = 0; # 0=no debugging, 1=ciphers, 2=trace, 3=dump data } #$Net::SSLeay::linux_debug = 1; my ($page, $response, %reply_headers) = Net::SSLeay::get_https($CAS_HOST, $CAS_PORT, $tmp); # if we had some type of connection problem if (!defined($page)) { Apache->warn("$$: CAS: validate_service_ticket(): error validating service"); $properties{'error'} = $CAS_CONNECT_ERROR_CODE; return %properties; } Apache->warn("$$: CAS: validate_service_ticket(): page: $page") unless ($LOG_LEVEL < $LOG_INSANE); Apache->warn("$$: CAS: validate_service_ticket(): response: $response") unless ($LOG_LEVEL < $LOG_INSANE); # FIXME - add a check for a 404 error/other errors if ($page =~ /