*/
class CAS_Client
{
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// XX XX
// XX CONFIGURATION XX
// XX XX
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// ########################################################################
// HTML OUTPUT
// ########################################################################
/**
* @addtogroup internalOutput
* @{
*/
/**
* This method filters a string by replacing special tokens by appropriate values
* and prints it. The corresponding tokens are taken into account:
* - __CAS_VERSION__
* - __PHPCAS_VERSION__
* - __SERVER_BASE_URL__
*
* Used by CAS_Client::PrintHTMLHeader() and CAS_Client::printHTMLFooter().
*
* @param $str the string to filter and output
*/
private function HTMLFilterOutput($str)
{
$str = str_replace('__CAS_VERSION__',$this->getServerVersion(),$str);
$str = str_replace('__PHPCAS_VERSION__',phpCAS::getVersion(),$str);
$str = str_replace('__SERVER_BASE_URL__',$this->getServerBaseURL(),$str);
echo $str;
}
/**
* A string used to print the header of HTML pages. Written by CAS_Client::setHTMLHeader(),
* read by CAS_Client::printHTMLHeader().
*
* @hideinitializer
* @see CAS_Client::setHTMLHeader, CAS_Client::printHTMLHeader()
*/
private $_output_header = '';
/**
* This method prints the header of the HTML output (after filtering). If
* CAS_Client::setHTMLHeader() was not used, a default header is output.
*
* @param $title the title of the page
*
* @see HTMLFilterOutput()
*/
private function printHTMLHeader($title)
{
$this->HTMLFilterOutput(str_replace('__TITLE__',
$title,
(empty($this->_output_header)
? '
__TITLE____TITLE__
'
: $this->_output_header)
)
);
}
/**
* A string used to print the footer of HTML pages. Written by CAS_Client::setHTMLFooter(),
* read by printHTMLFooter().
*
* @hideinitializer
* @see CAS_Client::setHTMLFooter, CAS_Client::printHTMLFooter()
*/
private $_output_footer = '';
/**
* This method prints the footer of the HTML output (after filtering). If
* CAS_Client::setHTMLFooter() was not used, a default footer is output.
*
* @see HTMLFilterOutput()
*/
private function printHTMLFooter()
{
$this->HTMLFilterOutput(empty($this->_output_footer)
?('
phpCAS __PHPCAS_VERSION__ '.$this->getString(CAS_STR_USING_SERVER).' __SERVER_BASE_URL__ (CAS __CAS_VERSION__)')
:$this->_output_footer);
}
/**
* This method set the HTML header used for all outputs.
*
* @param $header the HTML header.
*/
public function setHTMLHeader($header)
{
$this->_output_header = $header;
}
/**
* This method set the HTML footer used for all outputs.
*
* @param $footer the HTML footer.
*/
public function setHTMLFooter($footer)
{
$this->_output_footer = $footer;
}
/** @} */
// ########################################################################
// INTERNATIONALIZATION
// ########################################################################
/**
* @addtogroup internalLang
* @{
*/
/**
* A string corresponding to the language used by phpCAS. Written by
* CAS_Client::setLang(), read by CAS_Client::getLang().
* @note debugging information is always in english (debug purposes only).
*
* @hideinitializer
* @sa CAS_Client::_strings, CAS_Client::getString()
*/
private $_lang = '';
/**
* This method returns the language used by phpCAS.
*
* @return a string representing the language
*/
private function getLang()
{
if ( empty($this->_lang) )
$this->setLang(PHPCAS_LANG_DEFAULT);
return $this->_lang;
}
/**
* array containing the strings used by phpCAS. Written by CAS_Client::setLang(), read by
* CAS_Client::getString() and used by CAS_Client::setLang().
*
* @note This array is filled by instructions in CAS/languages/<$this->_lang>.php
*
* @see CAS_Client::_lang, CAS_Client::getString(), CAS_Client::setLang(), CAS_Client::getLang()
*/
private $_strings;
/**
* This method returns a string depending on the language.
*
* @param $str the index of the string in $_string.
*
* @return the string corresponding to $index in $string.
*
*/
private function getString($str)
{
// call CASclient::getLang() to be sure the language is initialized
$this->getLang();
if ( !isset($this->_strings[$str]) ) {
trigger_error('string `'.$str.'\' not defined for language `'.$this->getLang().'\'',E_USER_ERROR);
}
return $this->_strings[$str];
}
/**
* This method is used to set the language used by phpCAS.
* @note Can be called only once.
*
* @param $lang a string representing the language.
*
* @sa CAS_LANG_FRENCH, CAS_LANG_ENGLISH
*/
public function setLang($lang)
{
// include the corresponding language file
include(dirname(__FILE__).'/languages/'.$lang.'.php');
if ( !is_array($this->_strings) ) {
trigger_error('language `'.$lang.'\' is not implemented',E_USER_ERROR);
}
$this->_lang = $lang;
}
/** @} */
// ########################################################################
// CAS SERVER CONFIG
// ########################################################################
/**
* @addtogroup internalConfig
* @{
*/
/**
* a record to store information about the CAS server.
* - $_server["version"]: the version of the CAS server
* - $_server["hostname"]: the hostname of the CAS server
* - $_server["port"]: the port the CAS server is running on
* - $_server["uri"]: the base URI the CAS server is responding on
* - $_server["base_url"]: the base URL of the CAS server
* - $_server["login_url"]: the login URL of the CAS server
* - $_server["service_validate_url"]: the service validating URL of the CAS server
* - $_server["proxy_url"]: the proxy URL of the CAS server
* - $_server["proxy_validate_url"]: the proxy validating URL of the CAS server
* - $_server["logout_url"]: the logout URL of the CAS server
*
* $_server["version"], $_server["hostname"], $_server["port"] and $_server["uri"]
* are written by CAS_Client::CAS_Client(), read by CAS_Client::getServerVersion(),
* CAS_Client::getServerHostname(), CAS_Client::getServerPort() and CAS_Client::getServerURI().
*
* The other fields are written and read by CAS_Client::getServerBaseURL(),
* CAS_Client::getServerLoginURL(), CAS_Client::getServerServiceValidateURL(),
* CAS_Client::getServerProxyValidateURL() and CAS_Client::getServerLogoutURL().
*
* @hideinitializer
*/
private $_server = array(
'version' => -1,
'hostname' => 'none',
'port' => -1,
'uri' => 'none');
/**
* This method is used to retrieve the version of the CAS server.
* @return the version of the CAS server.
*/
private function getServerVersion()
{
return $this->_server['version'];
}
/**
* This method is used to retrieve the hostname of the CAS server.
* @return the hostname of the CAS server.
*/
private function getServerHostname()
{ return $this->_server['hostname']; }
/**
* This method is used to retrieve the port of the CAS server.
* @return the port of the CAS server.
*/
private function getServerPort()
{ return $this->_server['port']; }
/**
* This method is used to retrieve the URI of the CAS server.
* @return a URI.
*/
private function getServerURI()
{ return $this->_server['uri']; }
/**
* This method is used to retrieve the base URL of the CAS server.
* @return a URL.
*/
private function getServerBaseURL()
{
// the URL is build only when needed
if ( empty($this->_server['base_url']) ) {
$this->_server['base_url'] = 'https://' . $this->getServerHostname();
if ($this->getServerPort()!=443) {
$this->_server['base_url'] .= ':'
.$this->getServerPort();
}
$this->_server['base_url'] .= $this->getServerURI();
}
return $this->_server['base_url'];
}
/**
* This method is used to retrieve the login URL of the CAS server.
* @param $gateway true to check authentication, false to force it
* @param $renew true to force the authentication with the CAS server
* NOTE : It is recommended that CAS implementations ignore the
"gateway" parameter if "renew" is set
* @return a URL.
*/
public function getServerLoginURL($gateway=false,$renew=false) {
phpCAS::traceBegin();
// the URL is build only when needed
if ( empty($this->_server['login_url']) ) {
$this->_server['login_url'] = $this->getServerBaseURL();
$this->_server['login_url'] .= 'login?service=';
$this->_server['login_url'] .= urlencode($this->getURL());
}
$url = $this->_server['login_url'];
if($renew) {
// It is recommended that when the "renew" parameter is set, its value be "true"
$url = $this->buildQueryUrl($url, 'renew=true');
} elseif ($gateway) {
// It is recommended that when the "gateway" parameter is set, its value be "true"
$url = $this->buildQueryUrl($url, 'gateway=true');
}
phpCAS::traceEnd($url);
return $url;
}
/**
* This method sets the login URL of the CAS server.
* @param $url the login URL
* @since 0.4.21 by Wyman Chan
*/
public function setServerLoginURL($url)
{
return $this->_server['login_url'] = $url;
}
/**
* This method sets the serviceValidate URL of the CAS server.
* @param $url the serviceValidate URL
* @since 1.1.0 by Joachim Fritschi
*/
public function setServerServiceValidateURL($url)
{
return $this->_server['service_validate_url'] = $url;
}
/**
* This method sets the proxyValidate URL of the CAS server.
* @param $url the proxyValidate URL
* @since 1.1.0 by Joachim Fritschi
*/
public function setServerProxyValidateURL($url)
{
return $this->_server['proxy_validate_url'] = $url;
}
/**
* This method sets the samlValidate URL of the CAS server.
* @param $url the samlValidate URL
* @since 1.1.0 by Joachim Fritschi
*/
public function setServerSamlValidateURL($url)
{
return $this->_server['saml_validate_url'] = $url;
}
/**
* This method is used to retrieve the service validating URL of the CAS server.
* @return a URL.
*/
public function getServerServiceValidateURL()
{
phpCAS::traceBegin();
// the URL is build only when needed
if ( empty($this->_server['service_validate_url']) ) {
switch ($this->getServerVersion()) {
case CAS_VERSION_1_0:
$this->_server['service_validate_url'] = $this->getServerBaseURL().'validate';
break;
case CAS_VERSION_2_0:
$this->_server['service_validate_url'] = $this->getServerBaseURL().'serviceValidate';
break;
}
}
$url = $this->buildQueryUrl($this->_server['service_validate_url'], 'service='.urlencode($this->getURL()));
phpCAS::traceEnd($url);
return $url;
}
/**
* This method is used to retrieve the SAML validating URL of the CAS server.
* @return a URL.
*/
public function getServerSamlValidateURL()
{
phpCAS::traceBegin();
// the URL is build only when needed
if ( empty($this->_server['saml_validate_url']) ) {
switch ($this->getServerVersion()) {
case SAML_VERSION_1_1:
$this->_server['saml_validate_url'] = $this->getServerBaseURL().'samlValidate';
break;
}
}
$url = $this->buildQueryUrl($this->_server['saml_validate_url'], 'TARGET='.urlencode($this->getURL()));
phpCAS::traceEnd($url);
return $url;
}
/**
* This method is used to retrieve the proxy validating URL of the CAS server.
* @return a URL.
*/
public function getServerProxyValidateURL()
{
phpCAS::traceBegin();
// the URL is build only when needed
if ( empty($this->_server['proxy_validate_url']) ) {
switch ($this->getServerVersion()) {
case CAS_VERSION_1_0:
$this->_server['proxy_validate_url'] = '';
break;
case CAS_VERSION_2_0:
$this->_server['proxy_validate_url'] = $this->getServerBaseURL().'proxyValidate';
break;
}
}
$url = $this->buildQueryUrl($this->_server['proxy_validate_url'], 'service='.urlencode($this->getURL()));
phpCAS::traceEnd($url);
return $url;
}
/**
* This method is used to retrieve the proxy URL of the CAS server.
* @return a URL.
*/
public function getServerProxyURL()
{
// the URL is build only when needed
if ( empty($this->_server['proxy_url']) ) {
switch ($this->getServerVersion()) {
case CAS_VERSION_1_0:
$this->_server['proxy_url'] = '';
break;
case CAS_VERSION_2_0:
$this->_server['proxy_url'] = $this->getServerBaseURL().'proxy';
break;
}
}
return $this->_server['proxy_url'];
}
/**
* This method is used to retrieve the logout URL of the CAS server.
* @return a URL.
*/
public function getServerLogoutURL()
{
// the URL is build only when needed
if ( empty($this->_server['logout_url']) ) {
$this->_server['logout_url'] = $this->getServerBaseURL().'logout';
}
return $this->_server['logout_url'];
}
/**
* This method sets the logout URL of the CAS server.
* @param $url the logout URL
* @since 0.4.21 by Wyman Chan
*/
public function setServerLogoutURL($url)
{
return $this->_server['logout_url'] = $url;
}
/**
* An array to store extra curl options.
*/
private $_curl_options = array();
/**
* This method is used to set additional user curl options.
*/
public function setExtraCurlOption($key, $value)
{
$this->_curl_options[$key] = $value;
}
/** @} */
// ########################################################################
// Change the internal behaviour of phpcas
// ########################################################################
/**
* @addtogroup internalBehave
* @{
*/
/**
* The class to instantiate for making web requests in readUrl().
* The class specified must implement the CAS_RequestInterface.
* By default CAS_CurlRequest is used, but this may be overridden to
* supply alternate request mechanisms for testing.
*/
private $_requestImplementation = 'CAS_CurlRequest';
/**
* Override the default implementation used to make web requests in readUrl().
* This class must implement the CAS_RequestInterface.
*
* @param string $className
* @return void
*/
public function setRequestImplementation ($className) {
$obj = new $className;
if (!($obj instanceof CAS_RequestInterface))
throw new CAS_InvalidArgumentException('$className must implement the CAS_RequestInterface');
$this->_requestImplementation = $className;
}
/**
* @var boolean $_exitOnAuthError; If true, phpCAS will exit on an authentication error.
*/
private $_exitOnAuthError = true;
/**
* Configure the client to not call exit() when an authentication failure occurs.
*
* Needed for testing proper failure handling.
*
* @return void
*/
public function setNoExitOnAuthError () {
$this->_exitOnAuthError = false;
}
/**
* @var boolean $_clearTicketsFromUrl; If true, phpCAS will clear session tickets from the URL.
* After a successful authentication.
*/
private $_clearTicketsFromUrl = true;
/**
* Configure the client to not send redirect headers and call exit() on authentication
* success. The normal redirect is used to remove the service ticket from the
* client's URL, but for running unit tests we need to continue without exiting.
*
* Needed for testing authentication
*
* @return void
*/
public function setNoClearTicketsFromUrl () {
$this->_clearTicketsFromUrl = false;
}
/**
* @var callback $_postAuthenticateCallbackFunction;
*/
private $_postAuthenticateCallbackFunction = null;
/**
* @var array $_postAuthenticateCallbackArgs;
*/
private $_postAuthenticateCallbackArgs = array();
/**
* Set a callback function to be run when a user authenticates.
*
* The callback function will be passed a $logoutTicket as its first parameter,
* followed by any $additionalArgs you pass. The $logoutTicket parameter is an
* opaque string that can be used to map a session-id to the logout request in order
* to support single-signout in applications that manage their own sessions
* (rather than letting phpCAS start the session).
*
* phpCAS::forceAuthentication() will always exit and forward client unless
* they are already authenticated. To perform an action at the moment the user
* logs in (such as registering an account, performing logging, etc), register
* a callback function here.
*
* @param callback $function
* @param optional array $additionalArgs
* @return void
*/
public function setPostAuthenticateCallback ($function, array $additionalArgs = array()) {
$this->_postAuthenticateCallbackFunction = $function;
$this->_postAuthenticateCallbackArgs = $additionalArgs;
}
/**
* @var callback $_signoutCallbackFunction;
*/
private $_signoutCallbackFunction = null;
/**
* @var array $_signoutCallbackArgs;
*/
private $_signoutCallbackArgs = array();
/**
* Set a callback function to be run when a single-signout request is received.
*
* The callback function will be passed a $logoutTicket as its first parameter,
* followed by any $additionalArgs you pass. The $logoutTicket parameter is an
* opaque string that can be used to map a session-id to the logout request in order
* to support single-signout in applications that manage their own sessions
* (rather than letting phpCAS start and destroy the session).
*
* @param callback $function
* @param optional array $additionalArgs
* @return void
*/
public function setSingleSignoutCallback ($function, array $additionalArgs = array()) {
$this->_signoutCallbackFunction = $function;
$this->_signoutCallbackArgs = $additionalArgs;
}
/** @} */
// ########################################################################
// CONSTRUCTOR
// ########################################################################
/**
* @addtogroup internalConfig
* @{
*/
/**
* CAS_Client constructor.
*
* @param $server_version the version of the CAS server
* @param $proxy TRUE if the CAS client is a CAS proxy, FALSE otherwise
* @param $server_hostname the hostname of the CAS server
* @param $server_port the port the CAS server is running on
* @param $server_uri the URI the CAS server is responding on
* @param $start_session Have phpCAS start PHP sessions (default true)
*
* @return a newly created CAS_Client object
*/
public function __construct(
$server_version,
$proxy,
$server_hostname,
$server_port,
$server_uri,
$start_session = true) {
phpCAS::traceBegin();
$this->_start_session = $start_session;
if ($this->_start_session && session_id() !== "")
{
phpCAS :: error("Another session was started before phpcas. Either disable the session" .
" handling for phpcas in the client() call or modify your application to leave" .
" session handling to phpcas");
}
// skip Session Handling for logout requests and if don't want it'
if ($start_session && !$this->isLogoutRequest())
{
phpCAS :: trace("Starting a new session");
session_start();
}
// are we in proxy mode ?
$this->_proxy = $proxy;
// Make cookie handling available.
if ($this->isProxy()) {
if (!isset($_SESSION['phpCAS']))
$_SESSION['phpCAS'] = array();
if (!isset($_SESSION['phpCAS']['service_cookies']))
$_SESSION['phpCAS']['service_cookies'] = array();
$this->_serviceCookieJar = new CAS_CookieJar($_SESSION['phpCAS']['service_cookies']);
}
//check version
switch ($server_version) {
case CAS_VERSION_1_0:
if ( $this->isProxy() )
phpCAS::error('CAS proxies are not supported in CAS '
.$server_version);
break;
case CAS_VERSION_2_0:
break;
case SAML_VERSION_1_1:
break;
default:
phpCAS::error('this version of CAS (`'
.$server_version
.'\') is not supported by phpCAS '
.phpCAS::getVersion());
}
$this->_server['version'] = $server_version;
// check hostname
if ( empty($server_hostname)
|| !preg_match('/[\.\d\-abcdefghijklmnopqrstuvwxyz]*/',$server_hostname) ) {
phpCAS::error('bad CAS server hostname (`'.$server_hostname.'\')');
}
$this->_server['hostname'] = $server_hostname;
// check port
if ( $server_port == 0
|| !is_int($server_port) ) {
phpCAS::error('bad CAS server port (`'.$server_hostname.'\')');
}
$this->_server['port'] = $server_port;
// check URI
if ( !preg_match('/[\.\d\-_abcdefghijklmnopqrstuvwxyz\/]*/',$server_uri) ) {
phpCAS::error('bad CAS server URI (`'.$server_uri.'\')');
}
// add leading and trailing `/' and remove doubles
$server_uri = preg_replace('/\/\//','/','/'.$server_uri.'/');
$this->_server['uri'] = $server_uri;
// set to callback mode if PgtIou and PgtId CGI GET parameters are provided
if ( $this->isProxy() ) {
$this->setCallbackMode(!empty($_GET['pgtIou'])&&!empty($_GET['pgtId']));
}
if ( $this->isCallbackMode() ) {
//callback mode: check that phpCAS is secured
if ( !$this->isHttps() ) {
phpCAS::error('CAS proxies must be secured to use phpCAS; PGT\'s will not be received from the CAS server');
}
} else {
//normal mode: get ticket and remove it from CGI parameters for developpers
$ticket = (isset($_GET['ticket']) ? $_GET['ticket'] : null);
switch ($this->getServerVersion()) {
case CAS_VERSION_1_0: // check for a Service Ticket
if( preg_match('/^ST-/',$ticket) ) {
phpCAS::trace('ST \''.$ticket.'\' found');
//ST present
$this->setST($ticket);
//ticket has been taken into account, unset it to hide it to applications
unset($_GET['ticket']);
} else if ( !empty($ticket) ) {
//ill-formed ticket, halt
phpCAS::error('ill-formed ticket found in the URL (ticket=`'.htmlentities($ticket).'\')');
}
break;
case CAS_VERSION_2_0: // check for a Service or Proxy Ticket
if( preg_match('/^[SP]T-/',$ticket) ) {
phpCAS::trace('ST or PT \''.$ticket.'\' found');
$this->setPT($ticket);
unset($_GET['ticket']);
} else if ( !empty($ticket) ) {
//ill-formed ticket, halt
phpCAS::error('ill-formed ticket found in the URL (ticket=`'.htmlentities($ticket).'\')');
}
break;
case SAML_VERSION_1_1: // SAML just does Service Tickets
if( preg_match('/^[SP]T-/',$ticket) ) {
phpCAS::trace('SA \''.$ticket.'\' found');
$this->setSA($ticket);
unset($_GET['ticket']);
} else if ( !empty($ticket) ) {
//ill-formed ticket, halt
phpCAS::error('ill-formed ticket found in the URL (ticket=`'.htmlentities($ticket).'\')');
}
break;
}
}
phpCAS::traceEnd();
}
/** @} */
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// XX XX
// XX Session Handling XX
// XX XX
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
/**
* @addtogroup internalConfig
* @{
*/
/**
* A variable to whether phpcas will use its own session handling. Default = true
* @hideinitializer
*/
private $_start_session = true;
private function setStartSession($session)
{
$this->_start_session = session;
}
public function getStartSession($session)
{
$this->_start_session = session;
}
/** @} */
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// XX XX
// XX AUTHENTICATION XX
// XX XX
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
/**
* @addtogroup internalAuthentication
* @{
*/
/**
* The Authenticated user. Written by CAS_Client::setUser(), read by CAS_Client::getUser().
* @attention client applications should use phpCAS::getUser().
*
* @hideinitializer
*/
private $_user = '';
/**
* This method sets the CAS user's login name.
*
* @param $user the login name of the authenticated user.
*
*/
private function setUser($user)
{
$this->_user = $user;
}
/**
* This method returns the CAS user's login name.
* @warning should be called only after CAS_Client::forceAuthentication() or
* CAS_Client::isAuthenticated(), otherwise halt with an error.
*
* @return the login name of the authenticated user
*/
public function getUser()
{
if ( empty($this->_user) ) {
phpCAS::error('this method should be used only after '.__CLASS__.'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()');
}
return $this->_user;
}
/***********************************************************************************************************************
* Atrributes section
*
* @author Matthias Crauwels , Ghent University, Belgium
*
***********************************************************************************************************************/
/**
* The Authenticated users attributes. Written by CAS_Client::setAttributes(), read by CAS_Client::getAttributes().
* @attention client applications should use phpCAS::getAttributes().
*
* @hideinitializer
*/
private $_attributes = array();
public function setAttributes($attributes)
{ $this->_attributes = $attributes; }
public function getAttributes() {
if ( empty($this->_user) ) { // if no user is set, there shouldn't be any attributes also...
phpCAS::error('this method should be used only after '.__CLASS__.'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()');
}
return $this->_attributes;
}
public function hasAttributes()
{ return !empty($this->_attributes); }
public function hasAttribute($key)
{ return (is_array($this->_attributes) && array_key_exists($key, $this->_attributes)); }
public function getAttribute($key) {
if($this->hasAttribute($key)) {
return $this->_attributes[$key];
}
}
/**
* This method is called to renew the authentication of the user
* If the user is authenticated, renew the connection
* If not, redirect to CAS
*/
public function renewAuthentication(){
phpCAS::traceBegin();
// Either way, the user is authenticated by CAS
if( isset( $_SESSION['phpCAS']['auth_checked'] ) )
unset($_SESSION['phpCAS']['auth_checked']);
if ( $this->isAuthenticated() ) {
phpCAS::trace('user already authenticated; renew');
$this->redirectToCas(false,true);
} else {
$this->redirectToCas();
}
phpCAS::traceEnd();
}
/**
* This method is called to be sure that the user is authenticated. When not
* authenticated, halt by redirecting to the CAS server; otherwise return TRUE.
* @return TRUE when the user is authenticated; otherwise halt.
*/
public function forceAuthentication()
{
phpCAS::traceBegin();
if ( $this->isAuthenticated() ) {
// the user is authenticated, nothing to be done.
phpCAS::trace('no need to authenticate');
$res = TRUE;
} else {
// the user is not authenticated, redirect to the CAS server
if (isset($_SESSION['phpCAS']['auth_checked'])) {
unset($_SESSION['phpCAS']['auth_checked']);
}
$this->redirectToCas(FALSE/* no gateway */);
// never reached
$res = FALSE;
}
phpCAS::traceEnd($res);
return $res;
}
/**
* An integer that gives the number of times authentication will be cached before rechecked.
*
* @hideinitializer
*/
private $_cache_times_for_auth_recheck = 0;
/**
* Set the number of times authentication will be cached before rechecked.
*
* @param $n an integer.
*/
public function setCacheTimesForAuthRecheck($n)
{
$this->_cache_times_for_auth_recheck = $n;
}
/**
* This method is called to check whether the user is authenticated or not.
* @return TRUE when the user is authenticated, FALSE when a previous gateway login failed or
* the function will not return if the user is redirected to the cas server for a gateway login attempt
*/
public function checkAuthentication()
{
phpCAS::traceBegin();
if ( $this->isAuthenticated() ) {
phpCAS::trace('user is authenticated');
$res = TRUE;
} else if (isset($_SESSION['phpCAS']['auth_checked'])) {
// the previous request has redirected the client to the CAS server with gateway=true
unset($_SESSION['phpCAS']['auth_checked']);
$res = FALSE;
} else {
// avoid a check against CAS on every request
if (! isset($_SESSION['phpCAS']['unauth_count']) )
$_SESSION['phpCAS']['unauth_count'] = -2; // uninitialized
if (($_SESSION['phpCAS']['unauth_count'] != -2 && $this->_cache_times_for_auth_recheck == -1)
|| ($_SESSION['phpCAS']['unauth_count'] >= 0 && $_SESSION['phpCAS']['unauth_count'] < $this->_cache_times_for_auth_recheck))
{
$res = FALSE;
if ($this->_cache_times_for_auth_recheck != -1)
{
$_SESSION['phpCAS']['unauth_count']++;
phpCAS::trace('user is not authenticated (cached for '.$_SESSION['phpCAS']['unauth_count'].' times of '.$this->_cache_times_for_auth_recheck.')');
}
else
{
phpCAS::trace('user is not authenticated (cached for until login pressed)');
}
}
else
{
$_SESSION['phpCAS']['unauth_count'] = 0;
$_SESSION['phpCAS']['auth_checked'] = true;
phpCAS::trace('user is not authenticated (cache reset)');
$this->redirectToCas(TRUE/* gateway */);
// never reached
$res = FALSE;
}
}
phpCAS::traceEnd($res);
return $res;
}
/**
* This method is called to check if the user is authenticated (previously or by
* tickets given in the URL).
*
* @return TRUE when the user is authenticated. Also may redirect to the same URL without the ticket.
*/
public function isAuthenticated()
{
phpCAS::traceBegin();
$res = FALSE;
$validate_url = '';
if ( $this->wasPreviouslyAuthenticated() ) {
if($this->hasST() || $this->hasPT() || $this->hasSA()){
// User has a additional ticket but was already authenticated
phpCAS::trace('ticket was present and will be discarded, use renewAuthenticate()');
header('Location: '.$this->getURL());
phpCAS::trace( "Prepare redirect to remove ticket: ".$this->getURL() );
phpCAS::traceExit();
exit();
}else{
// the user has already (previously during the session) been
// authenticated, nothing to be done.
phpCAS::trace('user was already authenticated, no need to look for tickets');
$res = TRUE;
}
}
else {
if ( $this->hasST() ) {
// if a Service Ticket was given, validate it
phpCAS::trace('ST `'.$this->getST().'\' is present');
$this->validateST($validate_url,$text_response,$tree_response); // if it fails, it halts
phpCAS::trace('ST `'.$this->getST().'\' was validated');
if ( $this->isProxy() ) {
$this->validatePGT($validate_url,$text_response,$tree_response); // idem
phpCAS::trace('PGT `'.$this->getPGT().'\' was validated');
$_SESSION['phpCAS']['pgt'] = $this->getPGT();
}
$_SESSION['phpCAS']['user'] = $this->getUser();
if($this->hasAttributes()){
$_SESSION['phpCAS']['attributes'] = $this->getAttributes();
}
$res = TRUE;
$logoutTicket = $this->getST();
}
elseif ( $this->hasPT() ) {
// if a Proxy Ticket was given, validate it
phpCAS::trace('PT `'.$this->getPT().'\' is present');
$this->validatePT($validate_url,$text_response,$tree_response); // note: if it fails, it halts
phpCAS::trace('PT `'.$this->getPT().'\' was validated');
if ( $this->isProxy() ) {
$this->validatePGT($validate_url,$text_response,$tree_response); // idem
phpCAS::trace('PGT `'.$this->getPGT().'\' was validated');
$_SESSION['phpCAS']['pgt'] = $this->getPGT();
}
$_SESSION['phpCAS']['user'] = $this->getUser();
if($this->hasAttributes()){
$_SESSION['phpCAS']['attributes'] = $this->getAttributes();
}
$res = TRUE;
$logoutTicket = $this->getPT();
}
elseif ( $this->hasSA() ) {
// if we have a SAML ticket, validate it.
phpCAS::trace('SA `'.$this->getSA().'\' is present');
$this->validateSA($validate_url,$text_response,$tree_response); // if it fails, it halts
phpCAS::trace('SA `'.$this->getSA().'\' was validated');
$_SESSION['phpCAS']['user'] = $this->getUser();
$_SESSION['phpCAS']['attributes'] = $this->getAttributes();
$res = TRUE;
$logoutTicket = $this->getSA();
}
else {
// no ticket given, not authenticated
phpCAS::trace('no ticket found');
}
if ($res) {
// Mark the auth-check as complete to allow post-authentication
// callbacks to make use of phpCAS::getUser() and similar methods
$dbg = debug_backtrace();
global $PHPCAS_AUTH_CHECK_CALL;
$PHPCAS_AUTH_CHECK_CALL = array (
'done' => TRUE,
'file' => $dbg[0]['file'],
'line' => $dbg[0]['line'],
'method' => __CLASS__ . '::' . __FUNCTION__,
'result' => $res
);
// call the post-authenticate callback if registered.
if ($this->_postAuthenticateCallbackFunction) {
$args = $this->_postAuthenticateCallbackArgs;
array_unshift($args, $logoutTicket);
call_user_func_array($this->_postAuthenticateCallbackFunction, $args);
}
// if called with a ticket parameter, we need to redirect to the app without the ticket so that CAS-ification is transparent to the browser (for later POSTS)
// most of the checks and errors should have been made now, so we're safe for redirect without masking error messages.
// remove the ticket as a security precaution to prevent a ticket in the HTTP_REFERRER
if ($this->_clearTicketsFromUrl) {
header('Location: '.$this->getURL());
phpCAS::trace( "Prepare redirect to : ".$this->getURL() );
phpCAS::traceExit();
exit();
}
}
}
phpCAS::traceEnd($res);
return $res;
}
/**
* This method tells if the current session is authenticated.
* @return true if authenticated based soley on $_SESSION variable
* @since 0.4.22 by Brendan Arnold
*/
public function isSessionAuthenticated ()
{
return !empty($_SESSION['phpCAS']['user']);
}
/**
* This method tells if the user has already been (previously) authenticated
* by looking into the session variables.
*
* @note This function switches to callback mode when needed.
*
* @return TRUE when the user has already been authenticated; FALSE otherwise.
*/
private function wasPreviouslyAuthenticated()
{
phpCAS::traceBegin();
if ( $this->isCallbackMode() ) {
$this->callback();
}
$auth = FALSE;
if ( $this->isProxy() ) {
// CAS proxy: username and PGT must be present
if ( $this->isSessionAuthenticated() && !empty($_SESSION['phpCAS']['pgt']) ) {
// authentication already done
$this->setUser($_SESSION['phpCAS']['user']);
if(isset($_SESSION['phpCAS']['attributes'])){
$this->setAttributes($_SESSION['phpCAS']['attributes']);
}
$this->setPGT($_SESSION['phpCAS']['pgt']);
phpCAS::trace('user = `'.$_SESSION['phpCAS']['user'].'\', PGT = `'.$_SESSION['phpCAS']['pgt'].'\'');
// Include the list of proxies
if (isset($_SESSION['phpCAS']['proxies'])) {
$this->setProxies($_SESSION['phpCAS']['proxies']);
phpCAS::trace('proxies = "'.implode('", "', $_SESSION['phpCAS']['proxies']).'"');
}
$auth = TRUE;
} elseif ( $this->isSessionAuthenticated() && empty($_SESSION['phpCAS']['pgt']) ) {
// these two variables should be empty or not empty at the same time
phpCAS::trace('username found (`'.$_SESSION['phpCAS']['user'].'\') but PGT is empty');
// unset all tickets to enforce authentication
unset($_SESSION['phpCAS']);
$this->setST('');
$this->setPT('');
} elseif ( !$this->isSessionAuthenticated() && !empty($_SESSION['phpCAS']['pgt']) ) {
// these two variables should be empty or not empty at the same time
phpCAS::trace('PGT found (`'.$_SESSION['phpCAS']['pgt'].'\') but username is empty');
// unset all tickets to enforce authentication
unset($_SESSION['phpCAS']);
$this->setST('');
$this->setPT('');
} else {
phpCAS::trace('neither user not PGT found');
}
} else {
// `simple' CAS client (not a proxy): username must be present
if ( $this->isSessionAuthenticated() ) {
// authentication already done
$this->setUser($_SESSION['phpCAS']['user']);
if(isset($_SESSION['phpCAS']['attributes'])){
$this->setAttributes($_SESSION['phpCAS']['attributes']);
}
phpCAS::trace('user = `'.$_SESSION['phpCAS']['user'].'\'');
// Include the list of proxies
if (isset($_SESSION['phpCAS']['proxies'])) {
$this->setProxies($_SESSION['phpCAS']['proxies']);
phpCAS::trace('proxies = "'.implode('", "', $_SESSION['phpCAS']['proxies']).'"');
}
$auth = TRUE;
} else {
phpCAS::trace('no user found');
}
}
phpCAS::traceEnd($auth);
return $auth;
}
/**
* This method is used to redirect the client to the CAS server.
* It is used by CAS_Client::forceAuthentication() and CAS_Client::checkAuthentication().
* @param $gateway true to check authentication, false to force it
* @param $renew true to force the authentication with the CAS server
*/
public function redirectToCas($gateway=false,$renew=false){
phpCAS::traceBegin();
$cas_url = $this->getServerLoginURL($gateway,$renew);
header('Location: '.$cas_url);
phpCAS::trace( "Redirect to : ".$cas_url );
$this->printHTMLHeader($this->getString(CAS_STR_AUTHENTICATION_WANTED));
printf(''.$this->getString(CAS_STR_SHOULD_HAVE_BEEN_REDIRECTED).'
',$cas_url);
$this->printHTMLFooter();
phpCAS::traceExit();
exit();
}
/**
* This method is used to logout from CAS.
* @params $params an array that contains the optional url and service parameters that will be passed to the CAS server
*/
public function logout($params) {
phpCAS::traceBegin();
$cas_url = $this->getServerLogoutURL();
$paramSeparator = '?';
if (isset($params['url'])) {
$cas_url = $cas_url . $paramSeparator . "url=" . urlencode($params['url']);
$paramSeparator = '&';
}
if (isset($params['service'])) {
$cas_url = $cas_url . $paramSeparator . "service=" . urlencode($params['service']);
}
header('Location: '.$cas_url);
phpCAS::trace( "Prepare redirect to : ".$cas_url );
session_unset();
session_destroy();
$this->printHTMLHeader($this->getString(CAS_STR_LOGOUT));
printf(''.$this->getString(CAS_STR_SHOULD_HAVE_BEEN_REDIRECTED).'
',$cas_url);
$this->printHTMLFooter();
phpCAS::traceExit();
exit();
}
/**
* @return true if the current request is a logout request.
*/
private function isLogoutRequest() {
return !empty($_POST['logoutRequest']);
}
/**
* This method handles logout requests.
* @param $check_client true to check the client bofore handling the request,
* false not to perform any access control. True by default.
* @param $allowed_clients an array of host names allowed to send logout requests.
* By default, only the CAs server (declared in the constructor) will be allowed.
*/
public function handleLogoutRequests($check_client=true, $allowed_clients=false) {
phpCAS::traceBegin();
if (!$this->isLogoutRequest()) {
phpCAS::trace("Not a logout request");
phpCAS::traceEnd();
return;
}
if(!$this->_start_session && is_null($this->_signoutCallbackFunction)){
phpCAS::trace("phpCAS can't handle logout requests if it does not manage the session.");
}
phpCAS::trace("Logout requested");
phpCAS::trace("SAML REQUEST: ".$_POST['logoutRequest']);
if ($check_client) {
if (!$allowed_clients) {
$allowed_clients = array( $this->getServerHostname() );
}
$client_ip = $_SERVER['REMOTE_ADDR'];
$client = gethostbyaddr($client_ip);
phpCAS::trace("Client: ".$client."/".$client_ip);
$allowed = false;
foreach ($allowed_clients as $allowed_client) {
if (($client == $allowed_client) or ($client_ip == $allowed_client)) {
phpCAS::trace("Allowed client '".$allowed_client."' matches, logout request is allowed");
$allowed = true;
break;
} else {
phpCAS::trace("Allowed client '".$allowed_client."' does not match");
}
}
if (!$allowed) {
phpCAS::error("Unauthorized logout request from client '".$client."'");
printf("Unauthorized!");
phpCAS::traceExit();
exit();
}
} else {
phpCAS::trace("No access control set");
}
// Extract the ticket from the SAML Request
preg_match("|(.*)|", $_POST['logoutRequest'], $tick, PREG_OFFSET_CAPTURE, 3);
$wrappedSamlSessionIndex = preg_replace('||','',$tick[0][0]);
$ticket2logout = preg_replace('||','',$wrappedSamlSessionIndex);
phpCAS::trace("Ticket to logout: ".$ticket2logout);
// call the post-authenticate callback if registered.
if ($this->_signoutCallbackFunction) {
$args = $this->_signoutCallbackArgs;
array_unshift($args, $ticket2logout);
call_user_func_array($this->_signoutCallbackFunction, $args);
}
// If phpCAS is managing the session, destroy it.
if ($this->_start_session) {
$session_id = preg_replace('/[^\w]/','',$ticket2logout);
phpCAS::trace("Session id: ".$session_id);
// destroy a possible application session created before phpcas
if(session_id() !== ""){
session_unset();
session_destroy();
}
// fix session ID
session_id($session_id);
$_COOKIE[session_name()]=$session_id;
$_GET[session_name()]=$session_id;
// Overwrite session
session_start();
session_unset();
session_destroy();
}
printf("Disconnected!");
phpCAS::traceExit();
exit();
}
/** @} */
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// XX XX
// XX BASIC CLIENT FEATURES (CAS 1.0) XX
// XX XX
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// ########################################################################
// ST
// ########################################################################
/**
* @addtogroup internalBasic
* @{
*/
/**
* the Service Ticket provided in the URL of the request if present
* (empty otherwise). Written by CAS_Client::CAS_Client(), read by
* CAS_Client::getST() and CAS_Client::hasPGT().
*
* @hideinitializer
*/
private $_st = '';
/**
* This method returns the Service Ticket provided in the URL of the request.
* @return The service ticket.
*/
public function getST()
{ return $this->_st; }
/**
* This method stores the Service Ticket.
* @param $st The Service Ticket.
*/
public function setST($st)
{ $this->_st = $st; }
/**
* This method tells if a Service Ticket was stored.
* @return TRUE if a Service Ticket has been stored.
*/
public function hasST()
{ return !empty($this->_st); }
/** @} */
// ########################################################################
// ST VALIDATION
// ########################################################################
/**
* @addtogroup internalBasic
* @{
*/
/**
* the certificate of the CAS server CA.
*
* @hideinitializer
*/
private $_cas_server_ca_cert = '';
/**
* Set to true not to validate the CAS server.
*
* @hideinitializer
*/
private $_no_cas_server_validation = false;
/**
* Set the CA certificate of the CAS server.
*
* @param $cert the PEM certificate of the CA that emited the cert of the server
*/
public function setCasServerCACert($cert)
{
$this->_cas_server_ca_cert = $cert;
}
/**
* Set no SSL validation for the CAS server.
*/
public function setNoCasServerValidation()
{
$this->_no_cas_server_validation = true;
}
/**
* This method is used to validate a ST; halt on failure, and sets $validate_url,
* $text_reponse and $tree_response on success. These parameters are used later
* by CAS_Client::validatePGT() for CAS proxies.
* Used for all CAS 1.0 validations
* @param $validate_url the URL of the request to the CAS server.
* @param $text_response the response of the CAS server, as is (XML text).
* @param $tree_response the response of the CAS server, as a DOM XML tree.
*
* @return bool TRUE when successfull, halt otherwise by calling CAS_Client::authError().
*/
public function validateST($validate_url,&$text_response,&$tree_response)
{
phpCAS::traceBegin();
// build the URL to validate the ticket
$validate_url = $this->getServerServiceValidateURL().'&ticket='.$this->getST();
if ( $this->isProxy() ) {
// pass the callback url for CAS proxies
$validate_url .= '&pgtUrl='.urlencode($this->getCallbackURL());
}
// open and read the URL
if ( !$this->readURL($validate_url,$headers,$text_response,$err_msg) ) {
phpCAS::trace('could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')');
$this->authError('ST not validated',
$validate_url,
TRUE/*$no_response*/);
}
// analyze the result depending on the version
switch ($this->getServerVersion()) {
case CAS_VERSION_1_0:
if (preg_match('/^no\n/',$text_response)) {
phpCAS::trace('ST has not been validated');
$this->authError('ST not validated',
$validate_url,
FALSE/*$no_response*/,
FALSE/*$bad_response*/,
$text_response);
}
if (!preg_match('/^yes\n/',$text_response)) {
phpCAS::trace('ill-formed response');
$this->authError('ST not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
// ST has been validated, extract the user name
$arr = preg_split('/\n/',$text_response);
$this->setUser(trim($arr[1]));
break;
case CAS_VERSION_2_0:
// create new DOMDocument object
$dom = new DOMDocument();
// Fix possible whitspace problems
$dom->preserveWhiteSpace = false;
// read the response of the CAS server into a DOM object
if ( !($dom->loadXML($text_response))) {
phpCAS::trace('dom->loadXML() failed');
$this->authError('ST not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
// read the root node of the XML tree
if ( !($tree_response = $dom->documentElement) ) {
phpCAS::trace('documentElement() failed');
$this->authError('ST not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
// insure that tag name is 'serviceResponse'
if ( $tree_response->localName != 'serviceResponse' ) {
phpCAS::trace('bad XML root node (should be `serviceResponse\' instead of `'.$tree_response->localName.'\'');
$this->authError('ST not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
if ( $tree_response->getElementsByTagName("authenticationSuccess")->length != 0) {
// authentication succeded, extract the user name
$success_elements = $tree_response->getElementsByTagName("authenticationSuccess");
if ( $success_elements->item(0)->getElementsByTagName("user")->length == 0) {
// no user specified => error
$this->authError('ST not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
$this->setUser(trim($success_elements->item(0)->getElementsByTagName("user")->item(0)->nodeValue));
$this->readExtraAttributesCas20($success_elements);
} else if ( $tree_response->getElementsByTagName("authenticationFailure")->length != 0) {
phpCAS::trace(' found');
// authentication failed, extract the error code and message
$auth_fail_list = $tree_response->getElementsByTagName("authenticationFailure");
$this->authError('ST not validated',
$validate_url,
FALSE/*$no_response*/,
FALSE/*$bad_response*/,
$text_response,
$auth_fail_list->item(0)->getAttribute('code')/*$err_code*/,
trim($auth_fail_list->item(0)->nodeValue)/*$err_msg*/);
} else {
phpCAS::trace('neither nor found');
$this->authError('ST not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
break;
}
$this->renameSession($this->getST());
// at this step, ST has been validated and $this->_user has been set,
phpCAS::traceEnd(TRUE);
return TRUE;
}
/** @} */
// ########################################################################
// SAML VALIDATION
// ########################################################################
/**
* @addtogroup internalSAML
* @{
*/
/**
* This method is used to validate a SAML TICKET; halt on failure, and sets $validate_url,
* $text_reponse and $tree_response on success. These parameters are used later
* by CAS_Client::validatePGT() for CAS proxies.
*
* @param $validate_url the URL of the request to the CAS server.
* @param $text_response the response of the CAS server, as is (XML text).
* @param $tree_response the response of the CAS server, as a DOM XML tree.
*
* @return bool TRUE when successfull, halt otherwise by calling CAS_Client::authError().
*/
public function validateSA($validate_url,&$text_response,&$tree_response)
{
phpCAS::traceBegin();
// build the URL to validate the ticket
$validate_url = $this->getServerSamlValidateURL();
// open and read the URL
if ( !$this->readURL($validate_url,$headers,$text_response,$err_msg) ) {
phpCAS::trace('could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')');
$this->authError('SA not validated', $validate_url, TRUE/*$no_response*/);
}
phpCAS::trace('server version: '.$this->getServerVersion());
// analyze the result depending on the version
switch ($this->getServerVersion()) {
case SAML_VERSION_1_1:
// create new DOMDocument Object
$dom = new DOMDocument();
// Fix possible whitspace problems
$dom->preserveWhiteSpace = false;
// read the response of the CAS server into a DOM object
if ( !($dom->loadXML($text_response))) {
phpCAS::trace('dom->loadXML() failed');
$this->authError('SA not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
// read the root node of the XML tree
if ( !($tree_response = $dom->documentElement) ) {
phpCAS::trace('documentElement() failed');
$this->authError('SA not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
// insure that tag name is 'Envelope'
if ( $tree_response->localName != 'Envelope' ) {
phpCAS::trace('bad XML root node (should be `Envelope\' instead of `'.$tree_response->localName.'\'');
$this->authError('SA not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
// check for the NameIdentifier tag in the SAML response
if ( $tree_response->getElementsByTagName("NameIdentifier")->length != 0) {
$success_elements = $tree_response->getElementsByTagName("NameIdentifier");
phpCAS::trace('NameIdentifier found');
$user = trim($success_elements->item(0)->nodeValue);
phpCAS::trace('user = `'.$user.'`');
$this->setUser($user);
$this->setSessionAttributes($text_response);
} else {
phpCAS::trace('no tag found in SAML payload');
$this->authError('SA not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
break;
}
$this->renameSession($this->getSA());
// at this step, ST has been validated and $this->_user has been set,
phpCAS::traceEnd(TRUE);
return TRUE;
}
/**
* This method will parse the DOM and pull out the attributes from the SAML
* payload and put them into an array, then put the array into the session.
*
* @param $text_response the SAML payload.
* @return bool TRUE when successfull and FALSE if no attributes a found
*/
private function setSessionAttributes($text_response)
{
phpCAS::traceBegin();
$result = FALSE;
$attr_array = array();
// create new DOMDocument Object
$dom = new DOMDocument();
// Fix possible whitspace problems
$dom->preserveWhiteSpace = false;
if (($dom->loadXML($text_response))) {
$xPath = new DOMXpath($dom);
$xPath->registerNamespace('samlp', 'urn:oasis:names:tc:SAML:1.0:protocol');
$xPath->registerNamespace('saml', 'urn:oasis:names:tc:SAML:1.0:assertion');
$nodelist = $xPath->query("//saml:Attribute");
if($nodelist){
foreach($nodelist as $node){
$xres = $xPath->query("saml:AttributeValue", $node);
$name = $node->getAttribute("AttributeName");
$value_array = array();
foreach($xres as $node2){
$value_array[] = $node2->nodeValue;
}
$attr_array[$name] = $value_array;
}
// UGent addition...
foreach($attr_array as $attr_key => $attr_value) {
if(count($attr_value) > 1) {
$this->_attributes[$attr_key] = $attr_value;
phpCAS::trace("* " . $attr_key . "=" . $attr_value);
}
else {
$this->_attributes[$attr_key] = $attr_value[0];
phpCAS::trace("* " . $attr_key . "=" . $attr_value[0]);
}
}
$result = TRUE;
}else{
phpCAS::trace("SAML Attributes are empty");
$result = FALSE;
}
}
phpCAS::traceEnd($result);
return $result;
}
/**
* This method returns the SAML Ticket provided in the URL of the request.
* @return The SAML ticket.
*/
public function getSA()
{ return 'ST'.substr($this->_sa, 2); }
/**
* This method stores the SAML Ticket.
* @param $sa The SAML Ticket.
*/
public function setSA($sa)
{ $this->_sa = $sa; }
/**
* This method tells if a SAML Ticket was stored.
* @return TRUE if a SAML Ticket has been stored.
*/
public function hasSA()
{ return !empty($this->_sa); }
/** @} */
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// XX XX
// XX PROXY FEATURES (CAS 2.0) XX
// XX XX
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// ########################################################################
// PROXYING
// ########################################################################
/**
* @addtogroup internalProxy
* @{
*/
/**
* A boolean telling if the client is a CAS proxy or not. Written by CAS_Client::CAS_Client(),
* read by CAS_Client::isProxy().
*/
private $_proxy;
/**
* Handler for managing service cookies.
*/
private $_serviceCookieJar;
/**
* Tells if a CAS client is a CAS proxy or not
*
* @return TRUE when the CAS client is a CAs proxy, FALSE otherwise
*/
public function isProxy()
{
return $this->_proxy;
}
/** @} */
// ########################################################################
// PGT
// ########################################################################
/**
* @addtogroup internalProxy
* @{
*/
/**
* the Proxy Grnting Ticket given by the CAS server (empty otherwise).
* Written by CAS_Client::setPGT(), read by CAS_Client::getPGT() and CAS_Client::hasPGT().
*
* @hideinitializer
*/
private $_pgt = '';
/**
* This method returns the Proxy Granting Ticket given by the CAS server.
* @return The Proxy Granting Ticket.
*/
private function getPGT()
{ return $this->_pgt; }
/**
* This method stores the Proxy Granting Ticket.
* @param $pgt The Proxy Granting Ticket.
*/
private function setPGT($pgt)
{ $this->_pgt = $pgt; }
/**
* This method tells if a Proxy Granting Ticket was stored.
* @return TRUE if a Proxy Granting Ticket has been stored.
*/
private function hasPGT()
{ return !empty($this->_pgt); }
/** @} */
// ########################################################################
// CALLBACK MODE
// ########################################################################
/**
* @addtogroup internalCallback
* @{
*/
/**
* each PHP script using phpCAS in proxy mode is its own callback to get the
* PGT back from the CAS server. callback_mode is detected by the constructor
* thanks to the GET parameters.
*/
/**
* a boolean to know if the CAS client is running in callback mode. Written by
* CAS_Client::setCallBackMode(), read by CAS_Client::isCallbackMode().
*
* @hideinitializer
*/
private $_callback_mode = FALSE;
/**
* This method sets/unsets callback mode.
*
* @param $callback_mode TRUE to set callback mode, FALSE otherwise.
*/
private function setCallbackMode($callback_mode)
{
$this->_callback_mode = $callback_mode;
}
/**
* This method returns TRUE when the CAs client is running i callback mode,
* FALSE otherwise.
*
* @return A boolean.
*/
private function isCallbackMode()
{
return $this->_callback_mode;
}
/**
* the URL that should be used for the PGT callback (in fact the URL of the
* current request without any CGI parameter). Written and read by
* CAS_Client::getCallbackURL().
*
* @hideinitializer
*/
private $_callback_url = '';
/**
* This method returns the URL that should be used for the PGT callback (in
* fact the URL of the current request without any CGI parameter, except if
* phpCAS::setFixedCallbackURL() was used).
*
* @return The callback URL
*/
private function getCallbackURL()
{
// the URL is built when needed only
if ( empty($this->_callback_url) ) {
$final_uri = '';
// remove the ticket if present in the URL
$final_uri = 'https://';
$final_uri .= $this->getServerUrl();
$request_uri = $_SERVER['REQUEST_URI'];
$request_uri = preg_replace('/\?.*$/','',$request_uri);
$final_uri .= $request_uri;
$this->setCallbackURL($final_uri);
}
return $this->_callback_url;
}
/**
* This method sets the callback url.
*
* @param $callback_url url to set callback
*/
public function setCallbackURL($url)
{
return $this->_callback_url = $url;
}
/**
* This method is called by CAS_Client::CAS_Client() when running in callback
* mode. It stores the PGT and its PGT Iou, prints its output and halts.
*/
private function callback()
{
phpCAS::traceBegin();
if (preg_match('/PGTIOU-[\.\-\w]/', $_GET['pgtIou'])){
if(preg_match('/[PT]GT-[\.\-\w]/', $_GET['pgtId'])){
$this->printHTMLHeader('phpCAS callback');
$pgt_iou = $_GET['pgtIou'];
$pgt = $_GET['pgtId'];
phpCAS::trace('Storing PGT `'.$pgt.'\' (id=`'.$pgt_iou.'\')');
echo 'Storing PGT `'.$pgt.'\' (id=`'.$pgt_iou.'\').
';
$this->storePGT($pgt,$pgt_iou);
$this->printHTMLFooter();
}else{
phpCAS::error('PGT format invalid' . $_GET['pgtId']);
}
}else{
phpCAS::error('PGTiou format invalid' . $_GET['pgtIou']);
}
phpCAS::traceExit();
exit();
}
/** @} */
// ########################################################################
// PGT STORAGE
// ########################################################################
/**
* @addtogroup internalPGTStorage
* @{
*/
/**
* an instance of a class inheriting of PGTStorage, used to deal with PGT
* storage. Created by CAS_Client::setPGTStorageFile(), used
* by CAS_Client::setPGTStorageFile() and CAS_Client::initPGTStorage().
*
* @hideinitializer
*/
private $_pgt_storage = null;
/**
* This method is used to initialize the storage of PGT's.
* Halts on error.
*/
private function initPGTStorage()
{
// if no SetPGTStorageXxx() has been used, default to file
if ( !is_object($this->_pgt_storage) ) {
$this->setPGTStorageFile();
}
// initializes the storage
$this->_pgt_storage->init();
}
/**
* This method stores a PGT. Halts on error.
*
* @param $pgt the PGT to store
* @param $pgt_iou its corresponding Iou
*/
private function storePGT($pgt,$pgt_iou)
{
// ensure that storage is initialized
$this->initPGTStorage();
// writes the PGT
$this->_pgt_storage->write($pgt,$pgt_iou);
}
/**
* This method reads a PGT from its Iou and deletes the corresponding storage entry.
*
* @param $pgt_iou the PGT Iou
*
* @return The PGT corresponding to the Iou, FALSE when not found.
*/
private function loadPGT($pgt_iou)
{
// ensure that storage is initialized
$this->initPGTStorage();
// read the PGT
return $this->_pgt_storage->read($pgt_iou);
}
/**
* This method can be used to set a custom PGT storage object.
*
* @param $storage a PGT storage object that inherits from the CAS_PGTStorage class
*/
public function setPGTStorage($storage)
{
// check that the storage has not already been set
if ( is_object($this->_pgt_storage) ) {
phpCAS::error('PGT storage already defined');
}
// check to make sure a valid storage object was specified
if ( !($storage instanceof CAS_PGTStorage_AbstractStorage) ) {
phpCAS::error('Invalid PGT storage object');
}
// store the PGTStorage object
$this->_pgt_storage = $storage;
}
/**
* This method is used to tell phpCAS to store the response of the
* CAS server to PGT requests in a database.
*
* @param $dsn_or_pdo a dsn string to use for creating a PDO object or a PDO object
* @param $username the username to use when connecting to the database
* @param $password the password to use when connecting to the database
* @param $table the table to use for storing and retrieving PGT's
* @param $driver_options any driver options to use when connecting to the database
*/
public function setPGTStorageDb($dsn_or_pdo, $username='', $password='', $table='', $driver_options=null)
{
// create the storage object
$this->setPGTStorage(new CAS_PGTStorage_Db($this, $dsn_or_pdo, $username, $password, $table, $driver_options));
}
/**
* This method is used to tell phpCAS to store the response of the
* CAS server to PGT requests onto the filesystem.
*
* @param $format the format used to store the PGT's (`plain' and `xml' allowed)
* @param $path the path where the PGT's should be stored
*/
public function setPGTStorageFile($path='')
{
// create the storage object
$this->setPGTStorage(new CAS_PGTStorage_File($this,$path));
}
// ########################################################################
// PGT VALIDATION
// ########################################################################
/**
* This method is used to validate a PGT; halt on failure.
*
* @param $validate_url the URL of the request to the CAS server.
* @param $text_response the response of the CAS server, as is (XML text); result
* of CAS_Client::validateST() or CAS_Client::validatePT().
* @param $tree_response the response of the CAS server, as a DOM XML tree; result
* of CAS_Client::validateST() or CAS_Client::validatePT().
*
* @return bool TRUE when successfull, halt otherwise by calling CAS_Client::authError().
*/
private function validatePGT(&$validate_url,$text_response,$tree_response)
{
phpCAS::traceBegin();
if ( $tree_response->getElementsByTagName("proxyGrantingTicket")->length == 0) {
phpCAS::trace(' not found');
// authentication succeded, but no PGT Iou was transmitted
$this->authError('Ticket validated but no PGT Iou transmitted',
$validate_url,
FALSE/*$no_response*/,
FALSE/*$bad_response*/,
$text_response);
} else {
// PGT Iou transmitted, extract it
$pgt_iou = trim($tree_response->getElementsByTagName("proxyGrantingTicket")->item(0)->nodeValue);
if(preg_match('/PGTIOU-[\.\-\w]/',$pgt_iou)){
$pgt = $this->loadPGT($pgt_iou);
if ( $pgt == FALSE ) {
phpCAS::trace('could not load PGT');
$this->authError('PGT Iou was transmitted but PGT could not be retrieved',
$validate_url,
FALSE/*$no_response*/,
FALSE/*$bad_response*/,
$text_response);
}
$this->setPGT($pgt);
}else{
phpCAS::trace('PGTiou format error');
$this->authError('PGT Iou was transmitted but has wrong fromat',
$validate_url,
FALSE/*$no_response*/,
FALSE/*$bad_response*/,
$text_response);
}
}
phpCAS::traceEnd(TRUE);
return TRUE;
}
// ########################################################################
// PGT VALIDATION
// ########################################################################
/**
* This method is used to retrieve PT's from the CAS server thanks to a PGT.
*
* @param $target_service the service to ask for with the PT.
* @param $err_code an error code (PHPCAS_SERVICE_OK on success).
* @param $err_msg an error message (empty on success).
*
* @return a Proxy Ticket, or FALSE on error.
*/
public function retrievePT($target_service,&$err_code,&$err_msg)
{
phpCAS::traceBegin();
// by default, $err_msg is set empty and $pt to TRUE. On error, $pt is
// set to false and $err_msg to an error message. At the end, if $pt is FALSE
// and $error_msg is still empty, it is set to 'invalid response' (the most
// commonly encountered error).
$err_msg = '';
// build the URL to retrieve the PT
$cas_url = $this->getServerProxyURL().'?targetService='.urlencode($target_service).'&pgt='.$this->getPGT();
// open and read the URL
if ( !$this->readURL($cas_url,$headers,$cas_response,$err_msg) ) {
phpCAS::trace('could not open URL \''.$cas_url.'\' to validate ('.$err_msg.')');
$err_code = PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE;
$err_msg = 'could not retrieve PT (no response from the CAS server)';
phpCAS::traceEnd(FALSE);
return FALSE;
}
$bad_response = FALSE;
if ( !$bad_response ) {
// create new DOMDocument object
$dom = new DOMDocument();
// Fix possible whitspace problems
$dom->preserveWhiteSpace = false;
// read the response of the CAS server into a DOM object
if ( !($dom->loadXML($cas_response))) {
phpCAS::trace('dom->loadXML() failed');
// read failed
$bad_response = TRUE;
}
}
if ( !$bad_response ) {
// read the root node of the XML tree
if ( !($root = $dom->documentElement) ) {
phpCAS::trace('documentElement failed');
// read failed
$bad_response = TRUE;
}
}
if ( !$bad_response ) {
// insure that tag name is 'serviceResponse'
if ( $root->localName != 'serviceResponse' ) {
phpCAS::trace('localName failed');
// bad root node
$bad_response = TRUE;
}
}
if ( !$bad_response ) {
// look for a proxySuccess tag
if ( $root->getElementsByTagName("proxySuccess")->length != 0) {
$proxy_success_list = $root->getElementsByTagName("proxySuccess");
// authentication succeded, look for a proxyTicket tag
if ( $proxy_success_list->item(0)->getElementsByTagName("proxyTicket")->length != 0) {
$err_code = PHPCAS_SERVICE_OK;
$err_msg = '';
$pt = trim($proxy_success_list->item(0)->getElementsByTagName("proxyTicket")->item(0)->nodeValue);
phpCAS::trace('original PT: '.trim($pt));
phpCAS::traceEnd($pt);
return $pt;
} else {
phpCAS::trace(' was found, but not ');
}
}
// look for a proxyFailure tag
else if ( $root->getElementsByTagName("proxyFailure")->length != 0) {
$proxy_failure_list = $root->getElementsByTagName("proxyFailure");
// authentication failed, extract the error
$err_code = PHPCAS_SERVICE_PT_FAILURE;
$err_msg = 'PT retrieving failed (code=`'
.$proxy_failure_list->item(0)->getAttribute('code')
.'\', message=`'
.trim($proxy_failure_list->item(0)->nodeValue)
.'\')';
phpCAS::traceEnd(FALSE);
return FALSE;
} else {
phpCAS::trace('neither nor found');
}
}
// at this step, we are sure that the response of the CAS server was ill-formed
$err_code = PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE;
$err_msg = 'Invalid response from the CAS server (response=`'.$cas_response.'\')';
phpCAS::traceEnd(FALSE);
return FALSE;
}
/** @} */
// ########################################################################
// READ CAS SERVER ANSWERS
// ########################################################################
/**
* @addtogroup internalMisc
* @{
*/
/**
* This method is used to acces a remote URL.
*
* @param $url the URL to access.
* @param $headers an array containing the HTTP header lines of the response
* (an empty array on failure).
* @param $body the body of the response, as a string (empty on failure).
* @param $err_msg an error message, filled on failure.
*
* @return TRUE on success, FALSE otherwise (in this later case, $err_msg
* contains an error message).
*/
private function readURL($url, &$headers, &$body, &$err_msg)
{
$className = $this->_requestImplementation;
$request = new $className();
if (count($this->_curl_options)) {
$request->setCurlOptions($this->_curl_options);
}
$request->setUrl($url);
if (empty($this->_cas_server_ca_cert) && !$this->_no_cas_server_validation) {
phpCAS::error('one of the methods phpCAS::setCasServerCACert() or phpCAS::setNoCasServerValidation() must be called.');
}
if ($this->_cas_server_ca_cert != '') {
$request->setSslCaCert($this->_cas_server_ca_cert);
}
// add extra stuff if SAML
if ($this->hasSA()) {
$request->addHeader("soapaction: http://www.oasis-open.org/committees/security");
$request->addHeader("cache-control: no-cache");
$request->addHeader("pragma: no-cache");
$request->addHeader("accept: text/xml");
$request->addHeader("connection: keep-alive");
$request->addHeader("content-type: text/xml");
$request->makePost();
$request->setPostBody($this->buildSAMLPayload());
}
if ($request->send()) {
$headers = $request->getResponseHeaders();
$body = $request->getResponseBody();
$err_msg = '';
return true;
} else {
$headers = '';
$body = '';
$err_msg = $request->getErrorMessage();
return false;
}
}
/**
* This method is used to build the SAML POST body sent to /samlValidate URL.
*
* @return the SOAP-encased SAMLP artifact (the ticket).
*/
private function buildSAMLPayload()
{
phpCAS::traceBegin();
//get the ticket
$sa = $this->getSA();
$body=SAML_SOAP_ENV.SAML_SOAP_BODY.SAMLP_REQUEST.SAML_ASSERTION_ARTIFACT.$sa.SAML_ASSERTION_ARTIFACT_CLOSE.SAMLP_REQUEST_CLOSE.SAML_SOAP_BODY_CLOSE.SAML_SOAP_ENV_CLOSE;
phpCAS::traceEnd($body);
return ($body);
}
private $_curl_headers = array();
/**
* This method is the callback used by readURL method to request HTTP headers.
*/
public function _curl_read_headers($ch, $header)
{
$this->_curl_headers[] = $header;
return strlen($header);
}
/** @} **/
// ########################################################################
// ACCESS TO EXTERNAL SERVICES
// ########################################################################
/**
* @addtogroup internalProxyServices
* @{
*/
/**
* Answer a proxy-authenticated service handler.
*
* @param string $type The service type. One of:
* PHPCAS_PROXIED_SERVICE_HTTP_GET
* PHPCAS_PROXIED_SERVICE_HTTP_POST
* PHPCAS_PROXIED_SERVICE_IMAP
*
*
* @return CAS_ProxiedService
* @throws InvalidArgumentException If the service type is unknown.
*/
public function getProxiedService ($type) {
switch ($type) {
case PHPCAS_PROXIED_SERVICE_HTTP_GET:
case PHPCAS_PROXIED_SERVICE_HTTP_POST:
$requestClass = $this->_requestImplementation;
$request = new $requestClass();
if (count($this->_curl_options)) {
$request->setCurlOptions($this->_curl_options);
}
$proxiedService = new $type($request, $this->_serviceCookieJar);
if ($proxiedService instanceof CAS_ProxiedService_Testable)
$proxiedService->setCasClient($this);
return $proxiedService;
case PHPCAS_PROXIED_SERVICE_IMAP;
$proxiedService = new CAS_ProxiedService_Imap($this->getUser());
if ($proxiedService instanceof CAS_ProxiedService_Testable)
$proxiedService->setCasClient($this);
return $proxiedService;
default:
throw new CAS_InvalidArgumentException("Unknown proxied-service type, $type.");
}
}
/**
* Initialize a proxied-service handler with the proxy-ticket it should use.
*
* @param CAS_ProxiedService $proxiedService
* @return void
* @throws CAS_ProxyTicketException If there is a proxy-ticket failure.
* The code of the Exception will be one of:
* PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE
* PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE
* PHPCAS_SERVICE_PT_FAILURE
* @throws CAS_ProxiedService_Exception If there is a failure getting the url from the proxied service.
*/
public function initializeProxiedService (CAS_ProxiedService $proxiedService) {
$url = $proxiedService->getServiceUrl();
if (!is_string($url))
throw new CAS_ProxiedService_Exception("Proxied Service ".get_class($proxiedService)."->getServiceUrl() should have returned a string, returned a ".gettype($url)." instead.");
$pt = $this->retrievePT($url, $err_code, $err_msg);
if (!$pt)
throw new CAS_ProxyTicketException($err_msg, $err_code);
$proxiedService->setProxyTicket($pt);
}
/**
* This method is used to access an HTTP[S] service.
*
* @param $url the service to access.
* @param $err_code an error code Possible values are PHPCAS_SERVICE_OK (on
* success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE, PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE,
* PHPCAS_SERVICE_PT_FAILURE, PHPCAS_SERVICE_NOT_AVAILABLE.
* @param $output the output of the service (also used to give an error
* message on failure).
*
* @return TRUE on success, FALSE otherwise (in this later case, $err_code
* gives the reason why it failed and $output contains an error message).
*/
public function serviceWeb($url,&$err_code,&$output)
{
try {
$service = $this->getProxiedService(PHPCAS_PROXIED_SERVICE_HTTP_GET);
$service->setUrl($url);
$service->send();
$output = $service->getResponseBody();
$err_code = PHPCAS_SERVICE_OK;
return TRUE;
} catch (CAS_ProxyTicketException $e) {
$err_code = $e->getCode();
$output = $e->getMessage();
return FALSE;
} catch (CAS_ProxiedService_Exception $e) {
$output = sprintf($this->getString(CAS_STR_SERVICE_UNAVAILABLE), $url, $e->getMessage());
$err_code = PHPCAS_SERVICE_NOT_AVAILABLE;
return FALSE;
}
}
/**
* This method is used to access an IMAP/POP3/NNTP service.
*
* @param $url a string giving the URL of the service, including the mailing box
* for IMAP URLs, as accepted by imap_open().
* @param $service a string giving for CAS retrieve Proxy ticket
* @param $flags options given to imap_open().
* @param $err_code an error code Possible values are PHPCAS_SERVICE_OK (on
* success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE, PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE,
* PHPCAS_SERVICE_PT_FAILURE, PHPCAS_SERVICE_NOT_AVAILABLE.
* @param $err_msg an error message on failure
* @param $pt the Proxy Ticket (PT) retrieved from the CAS server to access the URL
* on success, FALSE on error).
*
* @return an IMAP stream on success, FALSE otherwise (in this later case, $err_code
* gives the reason why it failed and $err_msg contains an error message).
*/
public function serviceMail($url,$serviceUrl,$flags,&$err_code,&$err_msg,&$pt)
{
try {
$service = $this->getProxiedService(PHPCAS_PROXIED_SERVICE_IMAP);
$service->setServiceUrl($serviceUrl);
$service->setMailbox($url);
$service->setOptions($flags);
$stream = $service->open();
$err_code = PHPCAS_SERVICE_OK;
$pt = $service->getImapProxyTicket();
return $stream;
} catch (CAS_ProxyTicketException $e) {
$err_msg = $e->getMessage();
$err_code = $e->getCode();
$pt = FALSE;
return FALSE;
} catch (CAS_ProxiedService_Exception $e) {
$err_msg = sprintf($this->getString(CAS_STR_SERVICE_UNAVAILABLE), $url, $e->getMessage());
$err_code = PHPCAS_SERVICE_NOT_AVAILABLE;
$pt = FALSE;
return FALSE;
}
}
/** @} **/
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// XX XX
// XX PROXIED CLIENT FEATURES (CAS 2.0) XX
// XX XX
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// ########################################################################
// PT
// ########################################################################
/**
* @addtogroup internalProxied
* @{
*/
/**
* the Proxy Ticket provided in the URL of the request if present
* (empty otherwise). Written by CAS_Client::CAS_Client(), read by
* CAS_Client::getPT() and CAS_Client::hasPGT().
*
* @hideinitializer
*/
private $_pt = '';
/**
* This method returns the Proxy Ticket provided in the URL of the request.
* @return The proxy ticket.
*/
public function getPT()
{
// return 'ST'.substr($this->_pt, 2);
return $this->_pt;
}
/**
* This method stores the Proxy Ticket.
* @param $pt The Proxy Ticket.
*/
public function setPT($pt)
{ $this->_pt = $pt; }
/**
* This method tells if a Proxy Ticket was stored.
* @return TRUE if a Proxy Ticket has been stored.
*/
public function hasPT()
{ return !empty($this->_pt); }
/**
* This array will store a list of proxies in front of this application. This
* property will only be populated if this script is being proxied rather than
* accessed directly.
*
* It is set in CAS_Client::validatePT() and can be read by CAS_Client::getProxies()
* @access private
*/
private $_proxies = array();
/**
* Answer an array of proxies that are sitting in front of this application.
*
* This method will only return a non-empty array if we have received and validated
* a Proxy Ticket.
*
* @return array
* @access public
* @since 6/25/09
*/
public function getProxies () {
return $this->_proxies;
}
/**
* Set the Proxy array, probably from persistant storage.
*
* @param array $proxies
* @return void
* @access private
* @since 6/25/09
*/
private function setProxies ($proxies) {
$this->_proxies = $proxies;
}
/** @} */
// ########################################################################
// PT VALIDATION
// ########################################################################
/**
* @addtogroup internalProxied
* @{
*/
/**
* This method is used to validate a ST or PT; halt on failure
* Used for all CAS 2.0 validations
* @return bool TRUE when successfull, halt otherwise by calling CAS_Client::authError().
*/
public function validatePT(&$validate_url,&$text_response,&$tree_response)
{
phpCAS::traceBegin();
phpCAS::trace($text_response);
// build the URL to validate the ticket
$validate_url = $this->getServerProxyValidateURL().'&ticket='.$this->getPT();
if ( $this->isProxy() ) {
// pass the callback url for CAS proxies
$validate_url .= '&pgtUrl='.urlencode($this->getCallbackURL());
}
// open and read the URL
if ( !$this->readURL($validate_url,$headers,$text_response,$err_msg) ) {
phpCAS::trace('could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')');
$this->authError('PT not validated',
$validate_url,
TRUE/*$no_response*/);
}
// create new DOMDocument object
$dom = new DOMDocument();
// Fix possible whitspace problems
$dom->preserveWhiteSpace = false;
// read the response of the CAS server into a DOMDocument object
if ( !($dom->loadXML($text_response))) {
// read failed
$this->authError('PT not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
// read the root node of the XML tree
if ( !($tree_response = $dom->documentElement) ) {
// read failed
$this->authError('PT not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
// insure that tag name is 'serviceResponse'
if ( $tree_response->localName != 'serviceResponse' ) {
// bad root node
$this->authError('PT not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
if ( $tree_response->getElementsByTagName("authenticationSuccess")->length != 0) {
// authentication succeded, extract the user name
$success_elements = $tree_response->getElementsByTagName("authenticationSuccess");
if ( $success_elements->item(0)->getElementsByTagName("user")->length == 0) {
// no user specified => error
$this->authError('PT not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
$this->setUser(trim($success_elements->item(0)->getElementsByTagName("user")->item(0)->nodeValue));
$this->readExtraAttributesCas20($success_elements);
// Store the proxies we are sitting behind for authorization checking
if ( sizeof($arr = $success_elements->item(0)->getElementsByTagName("proxy")) > 0) {
foreach ($arr as $proxyElem) {
phpCAS::trace("Storing Proxy: ".$proxyElem->nodeValue);
$this->_proxies[] = trim($proxyElem->nodeValue);
}
$_SESSION['phpCAS']['proxies'] = $this->_proxies;
}
} else if ( $tree_response->getElementsByTagName("authenticationFailure")->length != 0) {
// authentication succeded, extract the error code and message
$auth_fail_list = $tree_response->getElementsByTagName("authenticationFailure");
$this->authError('PT not validated',
$validate_url,
FALSE/*$no_response*/,
FALSE/*$bad_response*/,
$text_response,
$auth_fail_list->item(0)->getAttribute('code')/*$err_code*/,
trim($auth_fail_list->item(0)->nodeValue)/*$err_msg*/);
} else {
$this->authError('PT not validated',
$validate_url,
FALSE/*$no_response*/,
TRUE/*$bad_response*/,
$text_response);
}
$this->renameSession($this->getPT());
// at this step, PT has been validated and $this->_user has been set,
phpCAS::traceEnd(TRUE);
return TRUE;
}
/**
* This method will parse the DOM and pull out the attributes from the XML
* payload and put them into an array, then put the array into the session.
*
* @param $text_response the XML payload.
* @return bool TRUE when successfull, halt otherwise by calling CAS_Client::authError().
*/
private function readExtraAttributesCas20($success_elements)
{
# PHPCAS-43 add CAS-2.0 extra attributes
phpCAS::traceBegin();
$extra_attributes = array();
// "Jasig Style" Attributes:
//
//
//
// jsmith
//
// RubyCAS
// Smith
// John
// CN=Staff,OU=Groups,DC=example,DC=edu
// CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu
//
// PGTIOU-84678-8a9d2sfa23casd
//
//
//
if ( $success_elements->item(0)->getElementsByTagName("attributes")->length != 0) {
$attr_nodes = $success_elements->item(0)->getElementsByTagName("attributes");
phpCas :: trace("Found nested jasig style attributes");
if($attr_nodes->item(0)->hasChildNodes()){
// Nested Attributes
foreach ($attr_nodes->item(0)->childNodes as $attr_child) {
phpCas :: trace("Attribute [".$attr_child->localName."] = ".$attr_child->nodeValue);
$this->addAttributeToArray($extra_attributes, $attr_child->localName, $attr_child->nodeValue);
}
}
}
// "RubyCAS Style" attributes
//
//
//
// jsmith
//
// RubyCAS
// Smith
// John
// CN=Staff,OU=Groups,DC=example,DC=edu
// CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu
//
// PGTIOU-84678-8a9d2sfa23casd
//
//
//
else {
phpCas :: trace("Testing for rubycas style attributes");
$childnodes = $success_elements->item(0)->childNodes;
foreach ($childnodes as $attr_node) {
switch ($attr_node->localName) {
case 'user':
case 'proxies':
case 'proxyGrantingTicket':
continue;
default:
if (strlen(trim($attr_node->nodeValue))) {
phpCas :: trace("Attribute [".$attr_node->localName."] = ".$attr_node->nodeValue);
$this->addAttributeToArray($extra_attributes, $attr_node->localName, $attr_node->nodeValue);
}
}
}
}
// "Name-Value" attributes.
//
// Attribute format from these mailing list thread:
// http://jasig.275507.n4.nabble.com/CAS-attributes-and-how-they-appear-in-the-CAS-response-td264272.html
// Note: This is a less widely used format, but in use by at least two institutions.
//
//
//
// jsmith
//
//
//
//
//
//
//
// PGTIOU-84678-8a9d2sfa23casd
//
//
//
if (!count($extra_attributes) && $success_elements->item(0)->getElementsByTagName("attribute")->length != 0) {
$attr_nodes = $success_elements->item(0)->getElementsByTagName("attribute");
$firstAttr = $attr_nodes->item(0);
if (!$firstAttr->hasChildNodes() && $firstAttr->hasAttribute('name') && $firstAttr->hasAttribute('value')) {
phpCas :: trace("Found Name-Value style attributes");
// Nested Attributes
foreach ($attr_nodes as $attr_node) {
if ($attr_node->hasAttribute('name') && $attr_node->hasAttribute('value')) {
phpCas :: trace("Attribute [".$attr_node->getAttribute('name')."] = ".$attr_node->getAttribute('value'));
$this->addAttributeToArray($extra_attributes, $attr_node->getAttribute('name'), $attr_node->getAttribute('value'));
}
}
}
}
$this->setAttributes($extra_attributes);
phpCAS::traceEnd();
return TRUE;
}
/**
* Add an attribute value to an array of attributes.
*
* @param ref array $array
* @param string $name
* @param string $value
* @return void
*/
private function addAttributeToArray (array &$attributeArray, $name, $value) {
// If multiple attributes exist, add as an array value
if (isset($attributeArray[$name])) {
// Initialize the array with the existing value
if (!is_array($attributeArray[$name])) {
$existingValue = $attributeArray[$name];
$attributeArray[$name] = array($existingValue);
}
$attributeArray[$name][] = trim($value);
} else {
$attributeArray[$name] = trim($value);
}
}
/** @} */
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// XX XX
// XX MISC XX
// XX XX
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
/**
* @addtogroup internalMisc
* @{
*/
// ########################################################################
// URL
// ########################################################################
/**
* the URL of the current request (without any ticket CGI parameter). Written
* and read by CAS_Client::getURL().
*
* @hideinitializer
*/
private $_url = '';
/**
* This method sets the URL of the current request
*
* @param $url url to set for service
*/
public function setURL($url)
{
$this->_url = $url;
}
/**
* This method returns the URL of the current request (without any ticket
* CGI parameter).
*
* @return The URL
*/
public function getURL()
{
phpCAS::traceBegin();
// the URL is built when needed only
if ( empty($this->_url) ) {
$final_uri = '';
// remove the ticket if present in the URL
$final_uri = ($this->isHttps()) ? 'https' : 'http';
$final_uri .= '://';
$final_uri .= $this->getServerUrl();
$request_uri = explode('?', $_SERVER['REQUEST_URI'], 2);
$final_uri .= $request_uri[0];
if (isset($request_uri[1]) && $request_uri[1])
{
$query_string = $this->removeParameterFromQueryString('ticket', $request_uri[1]);
// If the query string still has anything left, append it to the final URI
if ($query_string !== '')
$final_uri .= "?$query_string";
}
phpCAS::trace("Final URI: $final_uri");
$this->setURL($final_uri);
}
phpCAS::traceEnd($this->_url);
return $this->_url;
}
/**
* Try to figure out the server URL with possible Proxys / Ports etc.
* @return Server URL with domain:port
*/
private function getServerUrl(){
$server_url = '';
if(!empty($_SERVER['HTTP_X_FORWARDED_HOST'])){
// explode the host list separated by comma and use the first host
$hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_HOST']);
$server_url = $hosts[0];
}else if(!empty($_SERVER['HTTP_X_FORWARDED_SERVER'])){
$server_url = $_SERVER['HTTP_X_FORWARDED_SERVER'];
}else{
if (empty($_SERVER['SERVER_NAME'])) {
$server_url = $_SERVER['HTTP_HOST'];
} else {
$server_url = $_SERVER['SERVER_NAME'];
}
}
if (!strpos($server_url, ':')) {
if ( ($this->isHttps() && $_SERVER['SERVER_PORT']!=443)
|| (!$this->isHttps() && $_SERVER['SERVER_PORT']!=80) ) {
$server_url .= ':';
$server_url .= $_SERVER['SERVER_PORT'];
}
}
return $server_url;
}
/**
* This method checks to see if the request is secured via HTTPS
* @return true if https, false otherwise
*/
private function isHttps() {
if ( isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
return true;
} else {
return false;
}
}
/**
* Removes a parameter from a query string
*
* @param string $parameterName
* @param string $queryString
* @return string
*
* @link http://stackoverflow.com/questions/1842681/regular-expression-to-remove-one-parameter-from-query-string
*/
private function removeParameterFromQueryString($parameterName, $queryString)
{
$parameterName = preg_quote($parameterName);
return preg_replace("/&$parameterName(=[^&]*)?|^$parameterName(=[^&]*)?&?/", '', $queryString);
}
/**
* This method is used to append query parameters to an url. Since the url
* might already contain parameter it has to be detected and to build a proper
* URL
* @param $url base url to add the query params to
* @param $query params in query form with & separated
* @return url with query params
*/
private function buildQueryUrl($url, $query) {
$url .= (strstr($url,'?') === FALSE) ? '?' : '&';
$url .= $query;
return $url;
}
/**
* Renaming the session
*/
private function renameSession($ticket)
{
phpCAS::traceBegin();
if($this->_start_session){
if (!empty ($this->_user))
{
$old_session = $_SESSION;
session_destroy();
// set up a new session, of name based on the ticket
$session_id = preg_replace('/[^\w]/', '', $ticket);
phpCAS :: trace("Session ID: ".$session_id);
session_id($session_id);
session_start();
phpCAS :: trace("Restoring old session vars");
$_SESSION = $old_session;
} else
{
phpCAS :: error('Session should only be renamed after successfull authentication');
}
}else{
phpCAS :: trace("Skipping session rename since phpCAS is not handling the session.");
}
phpCAS::traceEnd();
}
// ########################################################################
// AUTHENTICATION ERROR HANDLING
// ########################################################################
/**
* This method is used to print the HTML output when the user was not authenticated.
*
* @param $failure the failure that occured
* @param $cas_url the URL the CAS server was asked for
* @param $no_response the response from the CAS server (other
* parameters are ignored if TRUE)
* @param $bad_response bad response from the CAS server ($err_code
* and $err_msg ignored if TRUE)
* @param $cas_response the response of the CAS server
* @param $err_code the error code given by the CAS server
* @param $err_msg the error message given by the CAS server
*/
private function authError($failure,$cas_url,$no_response,$bad_response='',$cas_response='',$err_code='',$err_msg='')
{
phpCAS::traceBegin();
$this->printHTMLHeader($this->getString(CAS_STR_AUTHENTICATION_FAILED));
printf($this->getString(CAS_STR_YOU_WERE_NOT_AUTHENTICATED),htmlentities($this->getURL()),$_SERVER['SERVER_ADMIN']);
phpCAS::trace('CAS URL: '.$cas_url);
phpCAS::trace('Authentication failure: '.$failure);
if ( $no_response ) {
phpCAS::trace('Reason: no response from the CAS server');
} else {
if ( $bad_response ) {
phpCAS::trace('Reason: bad response from the CAS server');
} else {
switch ($this->getServerVersion()) {
case CAS_VERSION_1_0:
phpCAS::trace('Reason: CAS error');
break;
case CAS_VERSION_2_0:
if ( empty($err_code) )
phpCAS::trace('Reason: no CAS error');
else
phpCAS::trace('Reason: ['.$err_code.'] CAS error: '.$err_msg);
break;
}
}
phpCAS::trace('CAS response: '.$cas_response);
}
$this->printHTMLFooter();
phpCAS::traceExit();
if ($this->_exitOnAuthError)
exit();
}
/** @} */
}
?>