1<?php 2 3/** 4 * Licensed to Jasig under one or more contributor license 5 * agreements. See the NOTICE file distributed with this work for 6 * additional information regarding copyright ownership. 7 * 8 * Jasig licenses this file to you under the Apache License, 9 * Version 2.0 (the "License"); you may not use this file except in 10 * compliance with the License. You may obtain a copy of the License at: 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, software 15 * distributed under the License is distributed on an "AS IS" BASIS, 16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 * See the License for the specific language governing permissions and 18 * limitations under the License. 19 * 20 * PHP Version 5 21 * 22 * @file CAS/Client.php 23 * @category Authentication 24 * @package PhpCAS 25 * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr> 26 * @author Olivier Berger <olivier.berger@it-sudparis.eu> 27 * @author Brett Bieber <brett.bieber@gmail.com> 28 * @author Joachim Fritschi <jfritschi@freenet.de> 29 * @author Adam Franco <afranco@middlebury.edu> 30 * @author Tobias Schiebeck <tobias.schiebeck@manchester.ac.uk> 31 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 32 * @link https://wiki.jasig.org/display/CASC/phpCAS 33 */ 34 35/** 36 * The CAS_Client class is a client interface that provides CAS authentication 37 * to PHP applications. 38 * 39 * @class CAS_Client 40 * @category Authentication 41 * @package PhpCAS 42 * @author Pascal Aubry <pascal.aubry@univ-rennes1.fr> 43 * @author Olivier Berger <olivier.berger@it-sudparis.eu> 44 * @author Brett Bieber <brett.bieber@gmail.com> 45 * @author Joachim Fritschi <jfritschi@freenet.de> 46 * @author Adam Franco <afranco@middlebury.edu> 47 * @author Tobias Schiebeck <tobias.schiebeck@manchester.ac.uk> 48 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 49 * @link https://wiki.jasig.org/display/CASC/phpCAS 50 * 51 */ 52 53class CAS_Client 54{ 55 56 // ######################################################################## 57 // HTML OUTPUT 58 // ######################################################################## 59 /** 60 * @addtogroup internalOutput 61 * @{ 62 */ 63 64 /** 65 * This method filters a string by replacing special tokens by appropriate values 66 * and prints it. The corresponding tokens are taken into account: 67 * - __CAS_VERSION__ 68 * - __PHPCAS_VERSION__ 69 * - __SERVER_BASE_URL__ 70 * 71 * Used by CAS_Client::PrintHTMLHeader() and CAS_Client::printHTMLFooter(). 72 * 73 * @param string $str the string to filter and output 74 * 75 * @return void 76 */ 77 private function _htmlFilterOutput($str) 78 { 79 $str = str_replace('__CAS_VERSION__', $this->getServerVersion(), $str); 80 $str = str_replace('__PHPCAS_VERSION__', phpCAS::getVersion(), $str); 81 $str = str_replace('__SERVER_BASE_URL__', $this->_getServerBaseURL(), $str); 82 echo $str; 83 } 84 85 /** 86 * A string used to print the header of HTML pages. Written by 87 * CAS_Client::setHTMLHeader(), read by CAS_Client::printHTMLHeader(). 88 * 89 * @hideinitializer 90 * @see CAS_Client::setHTMLHeader, CAS_Client::printHTMLHeader() 91 */ 92 private $_output_header = ''; 93 94 /** 95 * This method prints the header of the HTML output (after filtering). If 96 * CAS_Client::setHTMLHeader() was not used, a default header is output. 97 * 98 * @param string $title the title of the page 99 * 100 * @return void 101 * @see _htmlFilterOutput() 102 */ 103 public function printHTMLHeader($title) 104 { 105 $this->_htmlFilterOutput( 106 str_replace( 107 '__TITLE__', $title, 108 (empty($this->_output_header) 109 ? '<html><head><title>__TITLE__</title></head><body><h1>__TITLE__</h1>' 110 : $this->_output_header) 111 ) 112 ); 113 } 114 115 /** 116 * A string used to print the footer of HTML pages. Written by 117 * CAS_Client::setHTMLFooter(), read by printHTMLFooter(). 118 * 119 * @hideinitializer 120 * @see CAS_Client::setHTMLFooter, CAS_Client::printHTMLFooter() 121 */ 122 private $_output_footer = ''; 123 124 /** 125 * This method prints the footer of the HTML output (after filtering). If 126 * CAS_Client::setHTMLFooter() was not used, a default footer is output. 127 * 128 * @return void 129 * @see _htmlFilterOutput() 130 */ 131 public function printHTMLFooter() 132 { 133 $lang = $this->getLangObj(); 134 $this->_htmlFilterOutput( 135 empty($this->_output_footer)? 136 (phpCAS::getVerbose())? 137 '<hr><address>phpCAS __PHPCAS_VERSION__ ' 138 .$lang->getUsingServer() 139 .' <a href="__SERVER_BASE_URL__">__SERVER_BASE_URL__</a> (CAS __CAS_VERSION__)</a></address></body></html>' 140 :'</body></html>' 141 :$this->_output_footer 142 ); 143 } 144 145 /** 146 * This method set the HTML header used for all outputs. 147 * 148 * @param string $header the HTML header. 149 * 150 * @return void 151 */ 152 public function setHTMLHeader($header) 153 { 154 // Argument Validation 155 if (gettype($header) != 'string') 156 throw new CAS_TypeMismatchException($header, '$header', 'string'); 157 158 $this->_output_header = $header; 159 } 160 161 /** 162 * This method set the HTML footer used for all outputs. 163 * 164 * @param string $footer the HTML footer. 165 * 166 * @return void 167 */ 168 public function setHTMLFooter($footer) 169 { 170 // Argument Validation 171 if (gettype($footer) != 'string') 172 throw new CAS_TypeMismatchException($footer, '$footer', 'string'); 173 174 $this->_output_footer = $footer; 175 } 176 177 178 /** @} */ 179 180 181 // ######################################################################## 182 // INTERNATIONALIZATION 183 // ######################################################################## 184 /** 185 * @addtogroup internalLang 186 * @{ 187 */ 188 /** 189 * A string corresponding to the language used by phpCAS. Written by 190 * CAS_Client::setLang(), read by CAS_Client::getLang(). 191 192 * @note debugging information is always in english (debug purposes only). 193 */ 194 private $_lang = PHPCAS_LANG_DEFAULT; 195 196 /** 197 * This method is used to set the language used by phpCAS. 198 * 199 * @param string $lang representing the language. 200 * 201 * @return void 202 */ 203 public function setLang($lang) 204 { 205 // Argument Validation 206 if (gettype($lang) != 'string') 207 throw new CAS_TypeMismatchException($lang, '$lang', 'string'); 208 209 phpCAS::traceBegin(); 210 $obj = new $lang(); 211 if (!($obj instanceof CAS_Languages_LanguageInterface)) { 212 throw new CAS_InvalidArgumentException( 213 '$className must implement the CAS_Languages_LanguageInterface' 214 ); 215 } 216 $this->_lang = $lang; 217 phpCAS::traceEnd(); 218 } 219 /** 220 * Create the language 221 * 222 * @return CAS_Languages_LanguageInterface object implementing the class 223 */ 224 public function getLangObj() 225 { 226 $classname = $this->_lang; 227 return new $classname(); 228 } 229 230 /** @} */ 231 // ######################################################################## 232 // CAS SERVER CONFIG 233 // ######################################################################## 234 /** 235 * @addtogroup internalConfig 236 * @{ 237 */ 238 239 /** 240 * a record to store information about the CAS server. 241 * - $_server['version']: the version of the CAS server 242 * - $_server['hostname']: the hostname of the CAS server 243 * - $_server['port']: the port the CAS server is running on 244 * - $_server['uri']: the base URI the CAS server is responding on 245 * - $_server['base_url']: the base URL of the CAS server 246 * - $_server['login_url']: the login URL of the CAS server 247 * - $_server['service_validate_url']: the service validating URL of the 248 * CAS server 249 * - $_server['proxy_url']: the proxy URL of the CAS server 250 * - $_server['proxy_validate_url']: the proxy validating URL of the CAS server 251 * - $_server['logout_url']: the logout URL of the CAS server 252 * 253 * $_server['version'], $_server['hostname'], $_server['port'] and 254 * $_server['uri'] are written by CAS_Client::CAS_Client(), read by 255 * CAS_Client::getServerVersion(), CAS_Client::_getServerHostname(), 256 * CAS_Client::_getServerPort() and CAS_Client::_getServerURI(). 257 * 258 * The other fields are written and read by CAS_Client::_getServerBaseURL(), 259 * CAS_Client::getServerLoginURL(), CAS_Client::getServerServiceValidateURL(), 260 * CAS_Client::getServerProxyValidateURL() and CAS_Client::getServerLogoutURL(). 261 * 262 * @hideinitializer 263 */ 264 private $_server = array( 265 'version' => '', 266 'hostname' => 'none', 267 'port' => -1, 268 'uri' => 'none'); 269 270 /** 271 * This method is used to retrieve the version of the CAS server. 272 * 273 * @return string the version of the CAS server. 274 */ 275 public function getServerVersion() 276 { 277 return $this->_server['version']; 278 } 279 280 /** 281 * This method is used to retrieve the hostname of the CAS server. 282 * 283 * @return string the hostname of the CAS server. 284 */ 285 private function _getServerHostname() 286 { 287 return $this->_server['hostname']; 288 } 289 290 /** 291 * This method is used to retrieve the port of the CAS server. 292 * 293 * @return int the port of the CAS server. 294 */ 295 private function _getServerPort() 296 { 297 return $this->_server['port']; 298 } 299 300 /** 301 * This method is used to retrieve the URI of the CAS server. 302 * 303 * @return string a URI. 304 */ 305 private function _getServerURI() 306 { 307 return $this->_server['uri']; 308 } 309 310 /** 311 * This method is used to retrieve the base URL of the CAS server. 312 * 313 * @return string a URL. 314 */ 315 private function _getServerBaseURL() 316 { 317 // the URL is build only when needed 318 if ( empty($this->_server['base_url']) ) { 319 $this->_server['base_url'] = 'https://' . $this->_getServerHostname(); 320 if ($this->_getServerPort()!=443) { 321 $this->_server['base_url'] .= ':' 322 .$this->_getServerPort(); 323 } 324 $this->_server['base_url'] .= $this->_getServerURI(); 325 } 326 return $this->_server['base_url']; 327 } 328 329 /** 330 * This method is used to retrieve the login URL of the CAS server. 331 * 332 * @param bool $gateway true to check authentication, false to force it 333 * @param bool $renew true to force the authentication with the CAS server 334 * 335 * @return string a URL. 336 * @note It is recommended that CAS implementations ignore the "gateway" 337 * parameter if "renew" is set 338 */ 339 public function getServerLoginURL($gateway=false,$renew=false) 340 { 341 phpCAS::traceBegin(); 342 // the URL is build only when needed 343 if ( empty($this->_server['login_url']) ) { 344 $this->_server['login_url'] = $this->_buildQueryUrl($this->_getServerBaseURL().'login','service='.urlencode($this->getURL())); 345 } 346 $url = $this->_server['login_url']; 347 if ($renew) { 348 // It is recommended that when the "renew" parameter is set, its 349 // value be "true" 350 $url = $this->_buildQueryUrl($url, 'renew=true'); 351 } elseif ($gateway) { 352 // It is recommended that when the "gateway" parameter is set, its 353 // value be "true" 354 $url = $this->_buildQueryUrl($url, 'gateway=true'); 355 } 356 phpCAS::traceEnd($url); 357 return $url; 358 } 359 360 /** 361 * This method sets the login URL of the CAS server. 362 * 363 * @param string $url the login URL 364 * 365 * @return string login url 366 */ 367 public function setServerLoginURL($url) 368 { 369 // Argument Validation 370 if (gettype($url) != 'string') 371 throw new CAS_TypeMismatchException($url, '$url', 'string'); 372 373 return $this->_server['login_url'] = $url; 374 } 375 376 377 /** 378 * This method sets the serviceValidate URL of the CAS server. 379 * 380 * @param string $url the serviceValidate URL 381 * 382 * @return string serviceValidate URL 383 */ 384 public function setServerServiceValidateURL($url) 385 { 386 // Argument Validation 387 if (gettype($url) != 'string') 388 throw new CAS_TypeMismatchException($url, '$url', 'string'); 389 390 return $this->_server['service_validate_url'] = $url; 391 } 392 393 394 /** 395 * This method sets the proxyValidate URL of the CAS server. 396 * 397 * @param string $url the proxyValidate URL 398 * 399 * @return string proxyValidate URL 400 */ 401 public function setServerProxyValidateURL($url) 402 { 403 // Argument Validation 404 if (gettype($url) != 'string') 405 throw new CAS_TypeMismatchException($url, '$url', 'string'); 406 407 return $this->_server['proxy_validate_url'] = $url; 408 } 409 410 411 /** 412 * This method sets the samlValidate URL of the CAS server. 413 * 414 * @param string $url the samlValidate URL 415 * 416 * @return string samlValidate URL 417 */ 418 public function setServerSamlValidateURL($url) 419 { 420 // Argument Validation 421 if (gettype($url) != 'string') 422 throw new CAS_TypeMismatchException($url, '$url', 'string'); 423 424 return $this->_server['saml_validate_url'] = $url; 425 } 426 427 428 /** 429 * This method is used to retrieve the service validating URL of the CAS server. 430 * 431 * @return string serviceValidate URL. 432 */ 433 public function getServerServiceValidateURL() 434 { 435 phpCAS::traceBegin(); 436 // the URL is build only when needed 437 if ( empty($this->_server['service_validate_url']) ) { 438 switch ($this->getServerVersion()) { 439 case CAS_VERSION_1_0: 440 $this->_server['service_validate_url'] = $this->_getServerBaseURL() 441 .'validate'; 442 break; 443 case CAS_VERSION_2_0: 444 $this->_server['service_validate_url'] = $this->_getServerBaseURL() 445 .'serviceValidate'; 446 break; 447 case CAS_VERSION_3_0: 448 $this->_server['service_validate_url'] = $this->_getServerBaseURL() 449 .'p3/serviceValidate'; 450 break; 451 } 452 } 453 $url = $this->_buildQueryUrl( 454 $this->_server['service_validate_url'], 455 'service='.urlencode($this->getURL()) 456 ); 457 phpCAS::traceEnd($url); 458 return $url; 459 } 460 /** 461 * This method is used to retrieve the SAML validating URL of the CAS server. 462 * 463 * @return string samlValidate URL. 464 */ 465 public function getServerSamlValidateURL() 466 { 467 phpCAS::traceBegin(); 468 // the URL is build only when needed 469 if ( empty($this->_server['saml_validate_url']) ) { 470 switch ($this->getServerVersion()) { 471 case SAML_VERSION_1_1: 472 $this->_server['saml_validate_url'] = $this->_getServerBaseURL().'samlValidate'; 473 break; 474 } 475 } 476 477 $url = $this->_buildQueryUrl( 478 $this->_server['saml_validate_url'], 479 'TARGET='.urlencode($this->getURL()) 480 ); 481 phpCAS::traceEnd($url); 482 return $url; 483 } 484 485 /** 486 * This method is used to retrieve the proxy validating URL of the CAS server. 487 * 488 * @return string proxyValidate URL. 489 */ 490 public function getServerProxyValidateURL() 491 { 492 phpCAS::traceBegin(); 493 // the URL is build only when needed 494 if ( empty($this->_server['proxy_validate_url']) ) { 495 switch ($this->getServerVersion()) { 496 case CAS_VERSION_1_0: 497 $this->_server['proxy_validate_url'] = ''; 498 break; 499 case CAS_VERSION_2_0: 500 $this->_server['proxy_validate_url'] = $this->_getServerBaseURL().'proxyValidate'; 501 break; 502 case CAS_VERSION_3_0: 503 $this->_server['proxy_validate_url'] = $this->_getServerBaseURL().'p3/proxyValidate'; 504 break; 505 } 506 } 507 $url = $this->_buildQueryUrl( 508 $this->_server['proxy_validate_url'], 509 'service='.urlencode($this->getURL()) 510 ); 511 phpCAS::traceEnd($url); 512 return $url; 513 } 514 515 516 /** 517 * This method is used to retrieve the proxy URL of the CAS server. 518 * 519 * @return string proxy URL. 520 */ 521 public function getServerProxyURL() 522 { 523 // the URL is build only when needed 524 if ( empty($this->_server['proxy_url']) ) { 525 switch ($this->getServerVersion()) { 526 case CAS_VERSION_1_0: 527 $this->_server['proxy_url'] = ''; 528 break; 529 case CAS_VERSION_2_0: 530 case CAS_VERSION_3_0: 531 $this->_server['proxy_url'] = $this->_getServerBaseURL().'proxy'; 532 break; 533 } 534 } 535 return $this->_server['proxy_url']; 536 } 537 538 /** 539 * This method is used to retrieve the logout URL of the CAS server. 540 * 541 * @return string logout URL. 542 */ 543 public function getServerLogoutURL() 544 { 545 // the URL is build only when needed 546 if ( empty($this->_server['logout_url']) ) { 547 $this->_server['logout_url'] = $this->_getServerBaseURL().'logout'; 548 } 549 return $this->_server['logout_url']; 550 } 551 552 /** 553 * This method sets the logout URL of the CAS server. 554 * 555 * @param string $url the logout URL 556 * 557 * @return string logout url 558 */ 559 public function setServerLogoutURL($url) 560 { 561 // Argument Validation 562 if (gettype($url) != 'string') 563 throw new CAS_TypeMismatchException($url, '$url', 'string'); 564 565 return $this->_server['logout_url'] = $url; 566 } 567 568 /** 569 * An array to store extra curl options. 570 */ 571 private $_curl_options = array(); 572 573 /** 574 * This method is used to set additional user curl options. 575 * 576 * @param string $key name of the curl option 577 * @param string $value value of the curl option 578 * 579 * @return void 580 */ 581 public function setExtraCurlOption($key, $value) 582 { 583 $this->_curl_options[$key] = $value; 584 } 585 586 /** @} */ 587 588 // ######################################################################## 589 // Change the internal behaviour of phpcas 590 // ######################################################################## 591 592 /** 593 * @addtogroup internalBehave 594 * @{ 595 */ 596 597 /** 598 * The class to instantiate for making web requests in readUrl(). 599 * The class specified must implement the CAS_Request_RequestInterface. 600 * By default CAS_Request_CurlRequest is used, but this may be overridden to 601 * supply alternate request mechanisms for testing. 602 */ 603 private $_requestImplementation = 'CAS_Request_CurlRequest'; 604 605 /** 606 * Override the default implementation used to make web requests in readUrl(). 607 * This class must implement the CAS_Request_RequestInterface. 608 * 609 * @param string $className name of the RequestImplementation class 610 * 611 * @return void 612 */ 613 public function setRequestImplementation ($className) 614 { 615 $obj = new $className; 616 if (!($obj instanceof CAS_Request_RequestInterface)) { 617 throw new CAS_InvalidArgumentException( 618 '$className must implement the CAS_Request_RequestInterface' 619 ); 620 } 621 $this->_requestImplementation = $className; 622 } 623 624 /** 625 * @var boolean $_clearTicketsFromUrl; If true, phpCAS will clear session 626 * tickets from the URL after a successful authentication. 627 */ 628 private $_clearTicketsFromUrl = true; 629 630 /** 631 * Configure the client to not send redirect headers and call exit() on 632 * authentication success. The normal redirect is used to remove the service 633 * ticket from the client's URL, but for running unit tests we need to 634 * continue without exiting. 635 * 636 * Needed for testing authentication 637 * 638 * @return void 639 */ 640 public function setNoClearTicketsFromUrl () 641 { 642 $this->_clearTicketsFromUrl = false; 643 } 644 645 /** 646 * @var callback $_attributeParserCallbackFunction; 647 */ 648 private $_casAttributeParserCallbackFunction = null; 649 650 /** 651 * @var array $_attributeParserCallbackArgs; 652 */ 653 private $_casAttributeParserCallbackArgs = array(); 654 655 /** 656 * Set a callback function to be run when parsing CAS attributes 657 * 658 * The callback function will be passed a XMLNode as its first parameter, 659 * followed by any $additionalArgs you pass. 660 * 661 * @param string $function callback function to call 662 * @param array $additionalArgs optional array of arguments 663 * 664 * @return void 665 */ 666 public function setCasAttributeParserCallback($function, array $additionalArgs = array()) 667 { 668 $this->_casAttributeParserCallbackFunction = $function; 669 $this->_casAttributeParserCallbackArgs = $additionalArgs; 670 } 671 672 /** @var callable $_postAuthenticateCallbackFunction; 673 */ 674 private $_postAuthenticateCallbackFunction = null; 675 676 /** 677 * @var array $_postAuthenticateCallbackArgs; 678 */ 679 private $_postAuthenticateCallbackArgs = array(); 680 681 /** 682 * Set a callback function to be run when a user authenticates. 683 * 684 * The callback function will be passed a $logoutTicket as its first parameter, 685 * followed by any $additionalArgs you pass. The $logoutTicket parameter is an 686 * opaque string that can be used to map a session-id to the logout request 687 * in order to support single-signout in applications that manage their own 688 * sessions (rather than letting phpCAS start the session). 689 * 690 * phpCAS::forceAuthentication() will always exit and forward client unless 691 * they are already authenticated. To perform an action at the moment the user 692 * logs in (such as registering an account, performing logging, etc), register 693 * a callback function here. 694 * 695 * @param callable $function callback function to call 696 * @param array $additionalArgs optional array of arguments 697 * 698 * @return void 699 */ 700 public function setPostAuthenticateCallback ($function, array $additionalArgs = array()) 701 { 702 $this->_postAuthenticateCallbackFunction = $function; 703 $this->_postAuthenticateCallbackArgs = $additionalArgs; 704 } 705 706 /** 707 * @var callable $_signoutCallbackFunction; 708 */ 709 private $_signoutCallbackFunction = null; 710 711 /** 712 * @var array $_signoutCallbackArgs; 713 */ 714 private $_signoutCallbackArgs = array(); 715 716 /** 717 * Set a callback function to be run when a single-signout request is received. 718 * 719 * The callback function will be passed a $logoutTicket as its first parameter, 720 * followed by any $additionalArgs you pass. The $logoutTicket parameter is an 721 * opaque string that can be used to map a session-id to the logout request in 722 * order to support single-signout in applications that manage their own sessions 723 * (rather than letting phpCAS start and destroy the session). 724 * 725 * @param callable $function callback function to call 726 * @param array $additionalArgs optional array of arguments 727 * 728 * @return void 729 */ 730 public function setSingleSignoutCallback ($function, array $additionalArgs = array()) 731 { 732 $this->_signoutCallbackFunction = $function; 733 $this->_signoutCallbackArgs = $additionalArgs; 734 } 735 736 // ######################################################################## 737 // Methods for supplying code-flow feedback to integrators. 738 // ######################################################################## 739 740 /** 741 * Ensure that this is actually a proxy object or fail with an exception 742 * 743 * @throws CAS_OutOfSequenceBeforeProxyException 744 * 745 * @return void 746 */ 747 public function ensureIsProxy() 748 { 749 if (!$this->isProxy()) { 750 throw new CAS_OutOfSequenceBeforeProxyException(); 751 } 752 } 753 754 /** 755 * Mark the caller of authentication. This will help client integraters determine 756 * problems with their code flow if they call a function such as getUser() before 757 * authentication has occurred. 758 * 759 * @param bool $auth True if authentication was successful, false otherwise. 760 * 761 * @return null 762 */ 763 public function markAuthenticationCall ($auth) 764 { 765 // store where the authentication has been checked and the result 766 $dbg = debug_backtrace(); 767 $this->_authentication_caller = array ( 768 'file' => $dbg[1]['file'], 769 'line' => $dbg[1]['line'], 770 'method' => $dbg[1]['class'] . '::' . $dbg[1]['function'], 771 'result' => (boolean)$auth 772 ); 773 } 774 private $_authentication_caller; 775 776 /** 777 * Answer true if authentication has been checked. 778 * 779 * @return bool 780 */ 781 public function wasAuthenticationCalled () 782 { 783 return !empty($this->_authentication_caller); 784 } 785 786 /** 787 * Ensure that authentication was checked. Terminate with exception if no 788 * authentication was performed 789 * 790 * @throws CAS_OutOfSequenceBeforeAuthenticationCallException 791 * 792 * @return void 793 */ 794 private function _ensureAuthenticationCalled() 795 { 796 if (!$this->wasAuthenticationCalled()) { 797 throw new CAS_OutOfSequenceBeforeAuthenticationCallException(); 798 } 799 } 800 801 /** 802 * Answer the result of the authentication call. 803 * 804 * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false 805 * and markAuthenticationCall() didn't happen. 806 * 807 * @return bool 808 */ 809 public function wasAuthenticationCallSuccessful () 810 { 811 $this->_ensureAuthenticationCalled(); 812 return $this->_authentication_caller['result']; 813 } 814 815 816 /** 817 * Ensure that authentication was checked. Terminate with exception if no 818 * authentication was performed 819 * 820 * @throws CAS_OutOfSequenceException 821 * 822 * @return void 823 */ 824 public function ensureAuthenticationCallSuccessful() 825 { 826 $this->_ensureAuthenticationCalled(); 827 if (!$this->_authentication_caller['result']) { 828 throw new CAS_OutOfSequenceException( 829 'authentication was checked (by ' 830 . $this->getAuthenticationCallerMethod() 831 . '() at ' . $this->getAuthenticationCallerFile() 832 . ':' . $this->getAuthenticationCallerLine() 833 . ') but the method returned false' 834 ); 835 } 836 } 837 838 /** 839 * Answer information about the authentication caller. 840 * 841 * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false 842 * and markAuthenticationCall() didn't happen. 843 * 844 * @return string the file that called authentication 845 */ 846 public function getAuthenticationCallerFile () 847 { 848 $this->_ensureAuthenticationCalled(); 849 return $this->_authentication_caller['file']; 850 } 851 852 /** 853 * Answer information about the authentication caller. 854 * 855 * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false 856 * and markAuthenticationCall() didn't happen. 857 * 858 * @return int the line that called authentication 859 */ 860 public function getAuthenticationCallerLine () 861 { 862 $this->_ensureAuthenticationCalled(); 863 return $this->_authentication_caller['line']; 864 } 865 866 /** 867 * Answer information about the authentication caller. 868 * 869 * Throws a CAS_OutOfSequenceException if wasAuthenticationCalled() is false 870 * and markAuthenticationCall() didn't happen. 871 * 872 * @return string the method that called authentication 873 */ 874 public function getAuthenticationCallerMethod () 875 { 876 $this->_ensureAuthenticationCalled(); 877 return $this->_authentication_caller['method']; 878 } 879 880 /** @} */ 881 882 // ######################################################################## 883 // CONSTRUCTOR 884 // ######################################################################## 885 /** 886 * @addtogroup internalConfig 887 * @{ 888 */ 889 890 /** 891 * CAS_Client constructor. 892 * 893 * @param string $server_version the version of the CAS server 894 * @param bool $proxy true if the CAS client is a CAS proxy 895 * @param string $server_hostname the hostname of the CAS server 896 * @param int $server_port the port the CAS server is running on 897 * @param string $server_uri the URI the CAS server is responding on 898 * @param bool $changeSessionID Allow phpCAS to change the session_id 899 * (Single Sign Out/handleLogoutRequests 900 * is based on that change) 901 * @param \SessionHandlerInterface $sessionHandler the session handler 902 * 903 * @return self a newly created CAS_Client object 904 */ 905 public function __construct( 906 $server_version, 907 $proxy, 908 $server_hostname, 909 $server_port, 910 $server_uri, 911 $changeSessionID = true, 912 \SessionHandlerInterface $sessionHandler = null 913 ) { 914 // Argument validation 915 if (gettype($server_version) != 'string') 916 throw new CAS_TypeMismatchException($server_version, '$server_version', 'string'); 917 if (gettype($proxy) != 'boolean') 918 throw new CAS_TypeMismatchException($proxy, '$proxy', 'boolean'); 919 if (gettype($server_hostname) != 'string') 920 throw new CAS_TypeMismatchException($server_hostname, '$server_hostname', 'string'); 921 if (gettype($server_port) != 'integer') 922 throw new CAS_TypeMismatchException($server_port, '$server_port', 'integer'); 923 if (gettype($server_uri) != 'string') 924 throw new CAS_TypeMismatchException($server_uri, '$server_uri', 'string'); 925 if (gettype($changeSessionID) != 'boolean') 926 throw new CAS_TypeMismatchException($changeSessionID, '$changeSessionID', 'boolean'); 927 928 if (empty($sessionHandler)) { 929 $sessionHandler = new CAS_Session_PhpSession; 930 } 931 932 phpCAS::traceBegin(); 933 // true : allow to change the session_id(), false session_id won't be 934 // changed and logout won't be handled because of that 935 $this->_setChangeSessionID($changeSessionID); 936 937 $this->setSessionHandler($sessionHandler); 938 939 if (!$this->_isLogoutRequest()) { 940 if (session_id() === "") { 941 // skip Session Handling for logout requests and if don't want it 942 session_start(); 943 phpCAS :: trace("Starting a new session " . session_id()); 944 } 945 // init phpCAS session array 946 if (!isset($_SESSION[static::PHPCAS_SESSION_PREFIX]) 947 || !is_array($_SESSION[static::PHPCAS_SESSION_PREFIX])) { 948 $_SESSION[static::PHPCAS_SESSION_PREFIX] = array(); 949 } 950 } 951 952 // Only for debug purposes 953 if ($this->isSessionAuthenticated()){ 954 phpCAS :: trace("Session is authenticated as: " . $this->getSessionValue('user')); 955 } else { 956 phpCAS :: trace("Session is not authenticated"); 957 } 958 // are we in proxy mode ? 959 $this->_proxy = $proxy; 960 961 // Make cookie handling available. 962 if ($this->isProxy()) { 963 if (!$this->hasSessionValue('service_cookies')) { 964 $this->setSessionValue('service_cookies', array()); 965 } 966 // TODO remove explicit call to $_SESSION 967 $this->_serviceCookieJar = new CAS_CookieJar( 968 $_SESSION[static::PHPCAS_SESSION_PREFIX]['service_cookies'] 969 ); 970 } 971 972 // check version 973 $supportedProtocols = phpCAS::getSupportedProtocols(); 974 if (isset($supportedProtocols[$server_version]) === false) { 975 phpCAS::error( 976 'this version of CAS (`'.$server_version 977 .'\') is not supported by phpCAS '.phpCAS::getVersion() 978 ); 979 } 980 981 if ($server_version === CAS_VERSION_1_0 && $this->isProxy()) { 982 phpCAS::error( 983 'CAS proxies are not supported in CAS '.$server_version 984 ); 985 } 986 987 $this->_server['version'] = $server_version; 988 989 // check hostname 990 if ( empty($server_hostname) 991 || !preg_match('/[\.\d\-a-z]*/', $server_hostname) 992 ) { 993 phpCAS::error('bad CAS server hostname (`'.$server_hostname.'\')'); 994 } 995 $this->_server['hostname'] = $server_hostname; 996 997 // check port 998 if ( $server_port == 0 999 || !is_int($server_port) 1000 ) { 1001 phpCAS::error('bad CAS server port (`'.$server_hostname.'\')'); 1002 } 1003 $this->_server['port'] = $server_port; 1004 1005 // check URI 1006 if ( !preg_match('/[\.\d\-_a-z\/]*/', $server_uri) ) { 1007 phpCAS::error('bad CAS server URI (`'.$server_uri.'\')'); 1008 } 1009 // add leading and trailing `/' and remove doubles 1010 if(strstr($server_uri, '?') === false) $server_uri .= '/'; 1011 $server_uri = preg_replace('/\/\//', '/', '/'.$server_uri); 1012 $this->_server['uri'] = $server_uri; 1013 1014 // set to callback mode if PgtIou and PgtId CGI GET parameters are provided 1015 if ( $this->isProxy() ) { 1016 if(!empty($_GET['pgtIou'])&&!empty($_GET['pgtId'])) { 1017 $this->_setCallbackMode(true); 1018 $this->_setCallbackModeUsingPost(false); 1019 } elseif (!empty($_POST['pgtIou'])&&!empty($_POST['pgtId'])) { 1020 $this->_setCallbackMode(true); 1021 $this->_setCallbackModeUsingPost(true); 1022 } else { 1023 $this->_setCallbackMode(false); 1024 $this->_setCallbackModeUsingPost(false); 1025 } 1026 1027 1028 } 1029 1030 if ( $this->_isCallbackMode() ) { 1031 //callback mode: check that phpCAS is secured 1032 if ( !$this->_isHttps() ) { 1033 phpCAS::error( 1034 'CAS proxies must be secured to use phpCAS; PGT\'s will not be received from the CAS server' 1035 ); 1036 } 1037 } else { 1038 //normal mode: get ticket and remove it from CGI parameters for 1039 // developers 1040 $ticket = (isset($_GET['ticket']) ? $_GET['ticket'] : null); 1041 if (preg_match('/^[SP]T-/', $ticket) ) { 1042 phpCAS::trace('Ticket \''.$ticket.'\' found'); 1043 $this->setTicket($ticket); 1044 unset($_GET['ticket']); 1045 } else if ( !empty($ticket) ) { 1046 //ill-formed ticket, halt 1047 phpCAS::error( 1048 'ill-formed ticket found in the URL (ticket=`' 1049 .htmlentities($ticket).'\')' 1050 ); 1051 } 1052 1053 } 1054 phpCAS::traceEnd(); 1055 } 1056 1057 /** @} */ 1058 1059 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 1060 // XX XX 1061 // XX Session Handling XX 1062 // XX XX 1063 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 1064 1065 /** 1066 * @addtogroup internalConfig 1067 * @{ 1068 */ 1069 1070 /** The session prefix for phpCAS values */ 1071 const PHPCAS_SESSION_PREFIX = 'phpCAS'; 1072 1073 /** 1074 * @var bool A variable to whether phpcas will use its own session handling. Default = true 1075 * @hideinitializer 1076 */ 1077 private $_change_session_id = true; 1078 1079 /** 1080 * @var SessionHandlerInterface 1081 */ 1082 private $_sessionHandler; 1083 1084 /** 1085 * Set a parameter whether to allow phpCAS to change session_id 1086 * 1087 * @param bool $allowed allow phpCAS to change session_id 1088 * 1089 * @return void 1090 */ 1091 private function _setChangeSessionID($allowed) 1092 { 1093 $this->_change_session_id = $allowed; 1094 } 1095 1096 /** 1097 * Get whether phpCAS is allowed to change session_id 1098 * 1099 * @return bool 1100 */ 1101 public function getChangeSessionID() 1102 { 1103 return $this->_change_session_id; 1104 } 1105 1106 /** 1107 * Set the session handler. 1108 * 1109 * @param \SessionHandlerInterface $sessionHandler 1110 * 1111 * @return bool 1112 */ 1113 public function setSessionHandler(\SessionHandlerInterface $sessionHandler) 1114 { 1115 $this->_sessionHandler = $sessionHandler; 1116 if (session_status() !== PHP_SESSION_ACTIVE) { 1117 return session_set_save_handler($this->_sessionHandler, true); 1118 } 1119 return true; 1120 } 1121 1122 /** 1123 * Get a session value using the given key. 1124 * 1125 * @param string $key 1126 * @param mixed $default default value if the key is not set 1127 * 1128 * @return mixed 1129 */ 1130 protected function getSessionValue($key, $default = null) 1131 { 1132 $this->validateSession($key); 1133 1134 if (isset($_SESSION[static::PHPCAS_SESSION_PREFIX][$key])) { 1135 return $_SESSION[static::PHPCAS_SESSION_PREFIX][$key]; 1136 } 1137 1138 return $default; 1139 } 1140 1141 /** 1142 * Determine whether a session value is set or not. 1143 * 1144 * To check if a session value is empty or not please use 1145 * !!(getSessionValue($key)). 1146 * 1147 * @param string $key 1148 * 1149 * @return bool 1150 */ 1151 protected function hasSessionValue($key) 1152 { 1153 $this->validateSession($key); 1154 1155 return isset($_SESSION[static::PHPCAS_SESSION_PREFIX][$key]); 1156 } 1157 1158 /** 1159 * Set a session value using the given key and value. 1160 * 1161 * @param string $key 1162 * @param mixed $value 1163 * 1164 * @return string 1165 */ 1166 protected function setSessionValue($key, $value) 1167 { 1168 $this->validateSession($key); 1169 1170 $_SESSION[static::PHPCAS_SESSION_PREFIX][$key] = $value; 1171 } 1172 1173 /** 1174 * Remove a session value with the given key. 1175 * 1176 * @param string $key 1177 */ 1178 protected function removeSessionValue($key) 1179 { 1180 $this->validateSession($key); 1181 1182 if (isset($_SESSION[static::PHPCAS_SESSION_PREFIX][$key])) { 1183 unset($_SESSION[static::PHPCAS_SESSION_PREFIX][$key]); 1184 return true; 1185 } 1186 1187 return false; 1188 } 1189 1190 /** 1191 * Remove all phpCAS session values. 1192 */ 1193 protected function clearSessionValues() 1194 { 1195 unset($_SESSION[static::PHPCAS_SESSION_PREFIX]); 1196 } 1197 1198 /** 1199 * Ensure $key is a string for session utils input 1200 * 1201 * @param string $key 1202 * 1203 * @return bool 1204 */ 1205 protected function validateSession($key) 1206 { 1207 if (!is_string($key)) { 1208 throw new InvalidArgumentException('Session key must be a string.'); 1209 } 1210 1211 return true; 1212 } 1213 1214 /** 1215 * Renaming the session 1216 * 1217 * @param string $ticket name of the ticket 1218 * 1219 * @return void 1220 */ 1221 protected function _renameSession($ticket) 1222 { 1223 phpCAS::traceBegin(); 1224 if ($this->getChangeSessionID()) { 1225 if (!empty($this->_user)) { 1226 $old_session = $_SESSION; 1227 phpCAS :: trace("Killing session: ". session_id()); 1228 session_destroy(); 1229 // set up a new session, of name based on the ticket 1230 $session_id = $this->_sessionIdForTicket($ticket); 1231 phpCAS :: trace("Starting session: ". $session_id); 1232 session_id($session_id); 1233 session_start(); 1234 phpCAS :: trace("Restoring old session vars"); 1235 $_SESSION = $old_session; 1236 } else { 1237 phpCAS :: trace ( 1238 'Session should only be renamed after successfull authentication' 1239 ); 1240 } 1241 } else { 1242 phpCAS :: trace( 1243 "Skipping session rename since phpCAS is not handling the session." 1244 ); 1245 } 1246 phpCAS::traceEnd(); 1247 } 1248 1249 /** @} */ 1250 1251 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 1252 // XX XX 1253 // XX AUTHENTICATION XX 1254 // XX XX 1255 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 1256 1257 /** 1258 * @addtogroup internalAuthentication 1259 * @{ 1260 */ 1261 1262 /** 1263 * The Authenticated user. Written by CAS_Client::_setUser(), read by 1264 * CAS_Client::getUser(). 1265 * 1266 * @hideinitializer 1267 */ 1268 private $_user = ''; 1269 1270 /** 1271 * This method sets the CAS user's login name. 1272 * 1273 * @param string $user the login name of the authenticated user. 1274 * 1275 * @return void 1276 */ 1277 private function _setUser($user) 1278 { 1279 $this->_user = $user; 1280 } 1281 1282 /** 1283 * This method returns the CAS user's login name. 1284 * 1285 * @return string the login name of the authenticated user 1286 * 1287 * @warning should be called only after CAS_Client::forceAuthentication() or 1288 * CAS_Client::isAuthenticated(), otherwise halt with an error. 1289 */ 1290 public function getUser() 1291 { 1292 // Sequence validation 1293 $this->ensureAuthenticationCallSuccessful(); 1294 1295 return $this->_getUser(); 1296 } 1297 1298 /** 1299 * This method returns the CAS user's login name. 1300 * 1301 * @return string the login name of the authenticated user 1302 * 1303 * @warning should be called only after CAS_Client::forceAuthentication() or 1304 * CAS_Client::isAuthenticated(), otherwise halt with an error. 1305 */ 1306 private function _getUser() 1307 { 1308 // This is likely a duplicate check that could be removed.... 1309 if ( empty($this->_user) ) { 1310 phpCAS::error( 1311 'this method should be used only after '.__CLASS__ 1312 .'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()' 1313 ); 1314 } 1315 return $this->_user; 1316 } 1317 1318 /** 1319 * The Authenticated users attributes. Written by 1320 * CAS_Client::setAttributes(), read by CAS_Client::getAttributes(). 1321 * @attention client applications should use phpCAS::getAttributes(). 1322 * 1323 * @hideinitializer 1324 */ 1325 private $_attributes = array(); 1326 1327 /** 1328 * Set an array of attributes 1329 * 1330 * @param array $attributes a key value array of attributes 1331 * 1332 * @return void 1333 */ 1334 public function setAttributes($attributes) 1335 { 1336 $this->_attributes = $attributes; 1337 } 1338 1339 /** 1340 * Get an key values arry of attributes 1341 * 1342 * @return array of attributes 1343 */ 1344 public function getAttributes() 1345 { 1346 // Sequence validation 1347 $this->ensureAuthenticationCallSuccessful(); 1348 // This is likely a duplicate check that could be removed.... 1349 if ( empty($this->_user) ) { 1350 // if no user is set, there shouldn't be any attributes also... 1351 phpCAS::error( 1352 'this method should be used only after '.__CLASS__ 1353 .'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()' 1354 ); 1355 } 1356 return $this->_attributes; 1357 } 1358 1359 /** 1360 * Check whether attributes are available 1361 * 1362 * @return bool attributes available 1363 */ 1364 public function hasAttributes() 1365 { 1366 // Sequence validation 1367 $this->ensureAuthenticationCallSuccessful(); 1368 1369 return !empty($this->_attributes); 1370 } 1371 /** 1372 * Check whether a specific attribute with a name is available 1373 * 1374 * @param string $key name of attribute 1375 * 1376 * @return bool is attribute available 1377 */ 1378 public function hasAttribute($key) 1379 { 1380 // Sequence validation 1381 $this->ensureAuthenticationCallSuccessful(); 1382 1383 return $this->_hasAttribute($key); 1384 } 1385 1386 /** 1387 * Check whether a specific attribute with a name is available 1388 * 1389 * @param string $key name of attribute 1390 * 1391 * @return bool is attribute available 1392 */ 1393 private function _hasAttribute($key) 1394 { 1395 return (is_array($this->_attributes) 1396 && array_key_exists($key, $this->_attributes)); 1397 } 1398 1399 /** 1400 * Get a specific attribute by name 1401 * 1402 * @param string $key name of attribute 1403 * 1404 * @return string attribute values 1405 */ 1406 public function getAttribute($key) 1407 { 1408 // Sequence validation 1409 $this->ensureAuthenticationCallSuccessful(); 1410 1411 if ($this->_hasAttribute($key)) { 1412 return $this->_attributes[$key]; 1413 } 1414 } 1415 1416 /** 1417 * This method is called to renew the authentication of the user 1418 * If the user is authenticated, renew the connection 1419 * If not, redirect to CAS 1420 * 1421 * @return bool true when the user is authenticated; otherwise halt. 1422 */ 1423 public function renewAuthentication() 1424 { 1425 phpCAS::traceBegin(); 1426 // Either way, the user is authenticated by CAS 1427 $this->removeSessionValue('auth_checked'); 1428 if ( $this->isAuthenticated(true) ) { 1429 phpCAS::trace('user already authenticated'); 1430 $res = true; 1431 } else { 1432 $this->redirectToCas(false, true); 1433 // never reached 1434 $res = false; 1435 } 1436 phpCAS::traceEnd(); 1437 return $res; 1438 } 1439 1440 /** 1441 * This method is called to be sure that the user is authenticated. When not 1442 * authenticated, halt by redirecting to the CAS server; otherwise return true. 1443 * 1444 * @return bool true when the user is authenticated; otherwise halt. 1445 */ 1446 public function forceAuthentication() 1447 { 1448 phpCAS::traceBegin(); 1449 1450 if ( $this->isAuthenticated() ) { 1451 // the user is authenticated, nothing to be done. 1452 phpCAS::trace('no need to authenticate'); 1453 $res = true; 1454 } else { 1455 // the user is not authenticated, redirect to the CAS server 1456 $this->removeSessionValue('auth_checked'); 1457 $this->redirectToCas(false/* no gateway */); 1458 // never reached 1459 $res = false; 1460 } 1461 phpCAS::traceEnd($res); 1462 return $res; 1463 } 1464 1465 /** 1466 * An integer that gives the number of times authentication will be cached 1467 * before rechecked. 1468 * 1469 * @hideinitializer 1470 */ 1471 private $_cache_times_for_auth_recheck = 0; 1472 1473 /** 1474 * Set the number of times authentication will be cached before rechecked. 1475 * 1476 * @param int $n number of times to wait for a recheck 1477 * 1478 * @return void 1479 */ 1480 public function setCacheTimesForAuthRecheck($n) 1481 { 1482 if (gettype($n) != 'integer') 1483 throw new CAS_TypeMismatchException($n, '$n', 'string'); 1484 1485 $this->_cache_times_for_auth_recheck = $n; 1486 } 1487 1488 /** 1489 * This method is called to check whether the user is authenticated or not. 1490 * 1491 * @return bool true when the user is authenticated, false when a previous 1492 * gateway login failed or the function will not return if the user is 1493 * redirected to the cas server for a gateway login attempt 1494 */ 1495 public function checkAuthentication() 1496 { 1497 phpCAS::traceBegin(); 1498 $res = false; // default 1499 if ( $this->isAuthenticated() ) { 1500 phpCAS::trace('user is authenticated'); 1501 /* The 'auth_checked' variable is removed just in case it's set. */ 1502 $this->removeSessionValue('auth_checked'); 1503 $res = true; 1504 } else if ($this->getSessionValue('auth_checked')) { 1505 // the previous request has redirected the client to the CAS server 1506 // with gateway=true 1507 $this->removeSessionValue('auth_checked'); 1508 } else { 1509 // avoid a check against CAS on every request 1510 // we need to write this back to session later 1511 $unauth_count = $this->getSessionValue('unauth_count', -2); 1512 1513 if (($unauth_count != -2 1514 && $this->_cache_times_for_auth_recheck == -1) 1515 || ($unauth_count >= 0 1516 && $unauth_count < $this->_cache_times_for_auth_recheck) 1517 ) { 1518 if ($this->_cache_times_for_auth_recheck != -1) { 1519 $unauth_count++; 1520 phpCAS::trace( 1521 'user is not authenticated (cached for ' 1522 .$unauth_count.' times of ' 1523 .$this->_cache_times_for_auth_recheck.')' 1524 ); 1525 } else { 1526 phpCAS::trace( 1527 'user is not authenticated (cached for until login pressed)' 1528 ); 1529 } 1530 $this->setSessionValue('unauth_count', $unauth_count); 1531 } else { 1532 $this->setSessionValue('unauth_count', 0); 1533 $this->setSessionValue('auth_checked', true); 1534 phpCAS::trace('user is not authenticated (cache reset)'); 1535 $this->redirectToCas(true/* gateway */); 1536 // never reached 1537 } 1538 } 1539 phpCAS::traceEnd($res); 1540 return $res; 1541 } 1542 1543 /** 1544 * This method is called to check if the user is authenticated (previously or by 1545 * tickets given in the URL). 1546 * 1547 * @param bool $renew true to force the authentication with the CAS server 1548 * 1549 * @return bool true when the user is authenticated. Also may redirect to the 1550 * same URL without the ticket. 1551 */ 1552 public function isAuthenticated($renew=false) 1553 { 1554 phpCAS::traceBegin(); 1555 $res = false; 1556 $validate_url = ''; 1557 if ( $this->_wasPreviouslyAuthenticated() ) { 1558 if ($this->hasTicket()) { 1559 // User has a additional ticket but was already authenticated 1560 phpCAS::trace( 1561 'ticket was present and will be discarded, use renewAuthenticate()' 1562 ); 1563 if ($this->_clearTicketsFromUrl) { 1564 phpCAS::trace("Prepare redirect to : ".$this->getURL()); 1565 session_write_close(); 1566 header('Location: '.$this->getURL()); 1567 flush(); 1568 phpCAS::traceExit(); 1569 throw new CAS_GracefullTerminationException(); 1570 } else { 1571 phpCAS::trace( 1572 'Already authenticated, but skipping ticket clearing since setNoClearTicketsFromUrl() was used.' 1573 ); 1574 $res = true; 1575 } 1576 } else { 1577 // the user has already (previously during the session) been 1578 // authenticated, nothing to be done. 1579 phpCAS::trace( 1580 'user was already authenticated, no need to look for tickets' 1581 ); 1582 $res = true; 1583 } 1584 1585 // Mark the auth-check as complete to allow post-authentication 1586 // callbacks to make use of phpCAS::getUser() and similar methods 1587 $this->markAuthenticationCall($res); 1588 } else { 1589 if ($this->hasTicket()) { 1590 switch ($this->getServerVersion()) { 1591 case CAS_VERSION_1_0: 1592 // if a Service Ticket was given, validate it 1593 phpCAS::trace( 1594 'CAS 1.0 ticket `'.$this->getTicket().'\' is present' 1595 ); 1596 $this->validateCAS10( 1597 $validate_url, $text_response, $tree_response, $renew 1598 ); // if it fails, it halts 1599 phpCAS::trace( 1600 'CAS 1.0 ticket `'.$this->getTicket().'\' was validated' 1601 ); 1602 $this->setSessionValue('user', $this->_getUser()); 1603 $res = true; 1604 $logoutTicket = $this->getTicket(); 1605 break; 1606 case CAS_VERSION_2_0: 1607 case CAS_VERSION_3_0: 1608 // if a Proxy Ticket was given, validate it 1609 phpCAS::trace( 1610 'CAS '.$this->getServerVersion().' ticket `'.$this->getTicket().'\' is present' 1611 ); 1612 $this->validateCAS20( 1613 $validate_url, $text_response, $tree_response, $renew 1614 ); // note: if it fails, it halts 1615 phpCAS::trace( 1616 'CAS '.$this->getServerVersion().' ticket `'.$this->getTicket().'\' was validated' 1617 ); 1618 if ( $this->isProxy() ) { 1619 $this->_validatePGT( 1620 $validate_url, $text_response, $tree_response 1621 ); // idem 1622 phpCAS::trace('PGT `'.$this->_getPGT().'\' was validated'); 1623 $this->setSessionValue('pgt', $this->_getPGT()); 1624 } 1625 $this->setSessionValue('user', $this->_getUser()); 1626 if (!empty($this->_attributes)) { 1627 $this->setSessionValue('attributes', $this->_attributes); 1628 } 1629 $proxies = $this->getProxies(); 1630 if (!empty($proxies)) { 1631 $this->setSessionValue('proxies', $this->getProxies()); 1632 } 1633 $res = true; 1634 $logoutTicket = $this->getTicket(); 1635 break; 1636 case SAML_VERSION_1_1: 1637 // if we have a SAML ticket, validate it. 1638 phpCAS::trace( 1639 'SAML 1.1 ticket `'.$this->getTicket().'\' is present' 1640 ); 1641 $this->validateSA( 1642 $validate_url, $text_response, $tree_response, $renew 1643 ); // if it fails, it halts 1644 phpCAS::trace( 1645 'SAML 1.1 ticket `'.$this->getTicket().'\' was validated' 1646 ); 1647 $this->setSessionValue('user', $this->_getUser()); 1648 $this->setSessionValue('attributes', $this->_attributes); 1649 $res = true; 1650 $logoutTicket = $this->getTicket(); 1651 break; 1652 default: 1653 phpCAS::trace('Protocoll error'); 1654 break; 1655 } 1656 } else { 1657 // no ticket given, not authenticated 1658 phpCAS::trace('no ticket found'); 1659 } 1660 1661 // Mark the auth-check as complete to allow post-authentication 1662 // callbacks to make use of phpCAS::getUser() and similar methods 1663 $this->markAuthenticationCall($res); 1664 1665 if ($res) { 1666 // call the post-authenticate callback if registered. 1667 if ($this->_postAuthenticateCallbackFunction) { 1668 $args = $this->_postAuthenticateCallbackArgs; 1669 array_unshift($args, $logoutTicket); 1670 call_user_func_array( 1671 $this->_postAuthenticateCallbackFunction, $args 1672 ); 1673 } 1674 1675 // if called with a ticket parameter, we need to redirect to the 1676 // app without the ticket so that CAS-ification is transparent 1677 // to the browser (for later POSTS) most of the checks and 1678 // errors should have been made now, so we're safe for redirect 1679 // without masking error messages. remove the ticket as a 1680 // security precaution to prevent a ticket in the HTTP_REFERRER 1681 if ($this->_clearTicketsFromUrl) { 1682 phpCAS::trace("Prepare redirect to : ".$this->getURL()); 1683 session_write_close(); 1684 header('Location: '.$this->getURL()); 1685 flush(); 1686 phpCAS::traceExit(); 1687 throw new CAS_GracefullTerminationException(); 1688 } 1689 } 1690 } 1691 phpCAS::traceEnd($res); 1692 return $res; 1693 } 1694 1695 /** 1696 * This method tells if the current session is authenticated. 1697 * 1698 * @return bool true if authenticated based soley on $_SESSION variable 1699 */ 1700 public function isSessionAuthenticated () 1701 { 1702 return !!$this->getSessionValue('user'); 1703 } 1704 1705 /** 1706 * This method tells if the user has already been (previously) authenticated 1707 * by looking into the session variables. 1708 * 1709 * @note This function switches to callback mode when needed. 1710 * 1711 * @return bool true when the user has already been authenticated; false otherwise. 1712 */ 1713 private function _wasPreviouslyAuthenticated() 1714 { 1715 phpCAS::traceBegin(); 1716 1717 if ( $this->_isCallbackMode() ) { 1718 // Rebroadcast the pgtIou and pgtId to all nodes 1719 if ($this->_rebroadcast&&!isset($_POST['rebroadcast'])) { 1720 $this->_rebroadcast(self::PGTIOU); 1721 } 1722 $this->_callback(); 1723 } 1724 1725 $auth = false; 1726 1727 if ( $this->isProxy() ) { 1728 // CAS proxy: username and PGT must be present 1729 if ( $this->isSessionAuthenticated() 1730 && $this->getSessionValue('pgt') 1731 ) { 1732 // authentication already done 1733 $this->_setUser($this->getSessionValue('user')); 1734 if ($this->hasSessionValue('attributes')) { 1735 $this->setAttributes($this->getSessionValue('attributes')); 1736 } 1737 $this->_setPGT($this->getSessionValue('pgt')); 1738 phpCAS::trace( 1739 'user = `'.$this->getSessionValue('user').'\', PGT = `' 1740 .$this->getSessionValue('pgt').'\'' 1741 ); 1742 1743 // Include the list of proxies 1744 if ($this->hasSessionValue('proxies')) { 1745 $this->_setProxies($this->getSessionValue('proxies')); 1746 phpCAS::trace( 1747 'proxies = "' 1748 .implode('", "', $this->getSessionValue('proxies')).'"' 1749 ); 1750 } 1751 1752 $auth = true; 1753 } elseif ( $this->isSessionAuthenticated() 1754 && !$this->getSessionValue('pgt') 1755 ) { 1756 // these two variables should be empty or not empty at the same time 1757 phpCAS::trace( 1758 'username found (`'.$this->getSessionValue('user') 1759 .'\') but PGT is empty' 1760 ); 1761 // unset all tickets to enforce authentication 1762 $this->clearSessionValues(); 1763 $this->setTicket(''); 1764 } elseif ( !$this->isSessionAuthenticated() 1765 && $this->getSessionValue('pgt') 1766 ) { 1767 // these two variables should be empty or not empty at the same time 1768 phpCAS::trace( 1769 'PGT found (`'.$this->getSessionValue('pgt') 1770 .'\') but username is empty' 1771 ); 1772 // unset all tickets to enforce authentication 1773 $this->clearSessionValues(); 1774 $this->setTicket(''); 1775 } else { 1776 phpCAS::trace('neither user nor PGT found'); 1777 } 1778 } else { 1779 // `simple' CAS client (not a proxy): username must be present 1780 if ( $this->isSessionAuthenticated() ) { 1781 // authentication already done 1782 $this->_setUser($this->getSessionValue('user')); 1783 if ($this->hasSessionValue('attributes')) { 1784 $this->setAttributes($this->getSessionValue('attributes')); 1785 } 1786 phpCAS::trace('user = `'.$this->getSessionValue('user').'\''); 1787 1788 // Include the list of proxies 1789 if ($this->hasSessionValue('proxies')) { 1790 $this->_setProxies($this->getSessionValue('proxies')); 1791 phpCAS::trace( 1792 'proxies = "' 1793 .implode('", "', $this->getSessionValue('proxies')).'"' 1794 ); 1795 } 1796 1797 $auth = true; 1798 } else { 1799 phpCAS::trace('no user found'); 1800 } 1801 } 1802 1803 phpCAS::traceEnd($auth); 1804 return $auth; 1805 } 1806 1807 /** 1808 * This method is used to redirect the client to the CAS server. 1809 * It is used by CAS_Client::forceAuthentication() and 1810 * CAS_Client::checkAuthentication(). 1811 * 1812 * @param bool $gateway true to check authentication, false to force it 1813 * @param bool $renew true to force the authentication with the CAS server 1814 * 1815 * @return void 1816 */ 1817 public function redirectToCas($gateway=false,$renew=false) 1818 { 1819 phpCAS::traceBegin(); 1820 $cas_url = $this->getServerLoginURL($gateway, $renew); 1821 session_write_close(); 1822 if (php_sapi_name() === 'cli') { 1823 @header('Location: '.$cas_url); 1824 } else { 1825 header('Location: '.$cas_url); 1826 } 1827 phpCAS::trace("Redirect to : ".$cas_url); 1828 $lang = $this->getLangObj(); 1829 $this->printHTMLHeader($lang->getAuthenticationWanted()); 1830 printf('<p>'. $lang->getShouldHaveBeenRedirected(). '</p>', $cas_url); 1831 $this->printHTMLFooter(); 1832 phpCAS::traceExit(); 1833 throw new CAS_GracefullTerminationException(); 1834 } 1835 1836 1837 /** 1838 * This method is used to logout from CAS. 1839 * 1840 * @param array $params an array that contains the optional url and service 1841 * parameters that will be passed to the CAS server 1842 * 1843 * @return void 1844 */ 1845 public function logout($params) 1846 { 1847 phpCAS::traceBegin(); 1848 $cas_url = $this->getServerLogoutURL(); 1849 $paramSeparator = '?'; 1850 if (isset($params['url'])) { 1851 $cas_url = $cas_url . $paramSeparator . "url=" 1852 . urlencode($params['url']); 1853 $paramSeparator = '&'; 1854 } 1855 if (isset($params['service'])) { 1856 $cas_url = $cas_url . $paramSeparator . "service=" 1857 . urlencode($params['service']); 1858 } 1859 header('Location: '.$cas_url); 1860 phpCAS::trace("Prepare redirect to : ".$cas_url); 1861 1862 phpCAS::trace("Destroying session : ".session_id()); 1863 session_unset(); 1864 session_destroy(); 1865 if (session_status() === PHP_SESSION_NONE) { 1866 phpCAS::trace("Session terminated"); 1867 } else { 1868 phpCAS::error("Session was not terminated"); 1869 phpCAS::trace("Session was not terminated"); 1870 } 1871 $lang = $this->getLangObj(); 1872 $this->printHTMLHeader($lang->getLogout()); 1873 printf('<p>'.$lang->getShouldHaveBeenRedirected(). '</p>', $cas_url); 1874 $this->printHTMLFooter(); 1875 phpCAS::traceExit(); 1876 throw new CAS_GracefullTerminationException(); 1877 } 1878 1879 /** 1880 * Check of the current request is a logout request 1881 * 1882 * @return bool is logout request. 1883 */ 1884 private function _isLogoutRequest() 1885 { 1886 return !empty($_POST['logoutRequest']); 1887 } 1888 1889 /** 1890 * This method handles logout requests. 1891 * 1892 * @param bool $check_client true to check the client bofore handling 1893 * the request, false not to perform any access control. True by default. 1894 * @param array $allowed_clients an array of host names allowed to send 1895 * logout requests. 1896 * 1897 * @return void 1898 */ 1899 public function handleLogoutRequests($check_client=true, $allowed_clients=array()) 1900 { 1901 phpCAS::traceBegin(); 1902 if (!$this->_isLogoutRequest()) { 1903 phpCAS::trace("Not a logout request"); 1904 phpCAS::traceEnd(); 1905 return; 1906 } 1907 if (!$this->getChangeSessionID() 1908 && is_null($this->_signoutCallbackFunction) 1909 ) { 1910 phpCAS::trace( 1911 "phpCAS can't handle logout requests if it is not allowed to change session_id." 1912 ); 1913 } 1914 phpCAS::trace("Logout requested"); 1915 $decoded_logout_rq = urldecode($_POST['logoutRequest']); 1916 phpCAS::trace("SAML REQUEST: ".$decoded_logout_rq); 1917 $allowed = false; 1918 if ($check_client) { 1919 if ($allowed_clients === array()) { 1920 $allowed_clients = array( $this->_getServerHostname() ); 1921 } 1922 $client_ip = $_SERVER['REMOTE_ADDR']; 1923 $client = gethostbyaddr($client_ip); 1924 phpCAS::trace("Client: ".$client."/".$client_ip); 1925 foreach ($allowed_clients as $allowed_client) { 1926 if (($client == $allowed_client) 1927 || ($client_ip == $allowed_client) 1928 ) { 1929 phpCAS::trace( 1930 "Allowed client '".$allowed_client 1931 ."' matches, logout request is allowed" 1932 ); 1933 $allowed = true; 1934 break; 1935 } else { 1936 phpCAS::trace( 1937 "Allowed client '".$allowed_client."' does not match" 1938 ); 1939 } 1940 } 1941 } else { 1942 phpCAS::trace("No access control set"); 1943 $allowed = true; 1944 } 1945 // If Logout command is permitted proceed with the logout 1946 if ($allowed) { 1947 phpCAS::trace("Logout command allowed"); 1948 // Rebroadcast the logout request 1949 if ($this->_rebroadcast && !isset($_POST['rebroadcast'])) { 1950 $this->_rebroadcast(self::LOGOUT); 1951 } 1952 // Extract the ticket from the SAML Request 1953 preg_match( 1954 "|<samlp:SessionIndex>(.*)</samlp:SessionIndex>|", 1955 $decoded_logout_rq, $tick, PREG_OFFSET_CAPTURE, 3 1956 ); 1957 $wrappedSamlSessionIndex = preg_replace( 1958 '|<samlp:SessionIndex>|', '', $tick[0][0] 1959 ); 1960 $ticket2logout = preg_replace( 1961 '|</samlp:SessionIndex>|', '', $wrappedSamlSessionIndex 1962 ); 1963 phpCAS::trace("Ticket to logout: ".$ticket2logout); 1964 1965 // call the post-authenticate callback if registered. 1966 if ($this->_signoutCallbackFunction) { 1967 $args = $this->_signoutCallbackArgs; 1968 array_unshift($args, $ticket2logout); 1969 call_user_func_array($this->_signoutCallbackFunction, $args); 1970 } 1971 1972 // If phpCAS is managing the session_id, destroy session thanks to 1973 // session_id. 1974 if ($this->getChangeSessionID()) { 1975 $session_id = $this->_sessionIdForTicket($ticket2logout); 1976 phpCAS::trace("Session id: ".$session_id); 1977 1978 // destroy a possible application session created before phpcas 1979 if (session_id() !== "") { 1980 session_unset(); 1981 session_destroy(); 1982 } 1983 // fix session ID 1984 session_id($session_id); 1985 $_COOKIE[session_name()]=$session_id; 1986 $_GET[session_name()]=$session_id; 1987 1988 // Overwrite session 1989 session_start(); 1990 session_unset(); 1991 session_destroy(); 1992 phpCAS::trace("Session ". $session_id . " destroyed"); 1993 } 1994 } else { 1995 phpCAS::error("Unauthorized logout request from client '".$client."'"); 1996 phpCAS::trace("Unauthorized logout request from client '".$client."'"); 1997 } 1998 flush(); 1999 phpCAS::traceExit(); 2000 throw new CAS_GracefullTerminationException(); 2001 2002 } 2003 2004 /** @} */ 2005 2006 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 2007 // XX XX 2008 // XX BASIC CLIENT FEATURES (CAS 1.0) XX 2009 // XX XX 2010 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 2011 2012 // ######################################################################## 2013 // ST 2014 // ######################################################################## 2015 /** 2016 * @addtogroup internalBasic 2017 * @{ 2018 */ 2019 2020 /** 2021 * The Ticket provided in the URL of the request if present 2022 * (empty otherwise). Written by CAS_Client::CAS_Client(), read by 2023 * CAS_Client::getTicket() and CAS_Client::_hasPGT(). 2024 * 2025 * @hideinitializer 2026 */ 2027 private $_ticket = ''; 2028 2029 /** 2030 * This method returns the Service Ticket provided in the URL of the request. 2031 * 2032 * @return string service ticket. 2033 */ 2034 public function getTicket() 2035 { 2036 return $this->_ticket; 2037 } 2038 2039 /** 2040 * This method stores the Service Ticket. 2041 * 2042 * @param string $st The Service Ticket. 2043 * 2044 * @return void 2045 */ 2046 public function setTicket($st) 2047 { 2048 $this->_ticket = $st; 2049 } 2050 2051 /** 2052 * This method tells if a Service Ticket was stored. 2053 * 2054 * @return bool if a Service Ticket has been stored. 2055 */ 2056 public function hasTicket() 2057 { 2058 return !empty($this->_ticket); 2059 } 2060 2061 /** @} */ 2062 2063 // ######################################################################## 2064 // ST VALIDATION 2065 // ######################################################################## 2066 /** 2067 * @addtogroup internalBasic 2068 * @{ 2069 */ 2070 2071 /** 2072 * @var string the certificate of the CAS server CA. 2073 * 2074 * @hideinitializer 2075 */ 2076 private $_cas_server_ca_cert = null; 2077 2078 2079 /** 2080 2081 * validate CN of the CAS server certificate 2082 2083 * 2084 2085 * @hideinitializer 2086 2087 */ 2088 2089 private $_cas_server_cn_validate = true; 2090 2091 /** 2092 * Set to true not to validate the CAS server. 2093 * 2094 * @hideinitializer 2095 */ 2096 private $_no_cas_server_validation = false; 2097 2098 2099 /** 2100 * Set the CA certificate of the CAS server. 2101 * 2102 * @param string $cert the PEM certificate file name of the CA that emited 2103 * the cert of the server 2104 * @param bool $validate_cn valiate CN of the CAS server certificate 2105 * 2106 * @return void 2107 */ 2108 public function setCasServerCACert($cert, $validate_cn) 2109 { 2110 // Argument validation 2111 if (gettype($cert) != 'string') { 2112 throw new CAS_TypeMismatchException($cert, '$cert', 'string'); 2113 } 2114 if (gettype($validate_cn) != 'boolean') { 2115 throw new CAS_TypeMismatchException($validate_cn, '$validate_cn', 'boolean'); 2116 } 2117 if (!file_exists($cert)) { 2118 throw new CAS_InvalidArgumentException("Certificate file does not exist " . $this->_requestImplementation); 2119 } 2120 $this->_cas_server_ca_cert = $cert; 2121 $this->_cas_server_cn_validate = $validate_cn; 2122 } 2123 2124 /** 2125 * Set no SSL validation for the CAS server. 2126 * 2127 * @return void 2128 */ 2129 public function setNoCasServerValidation() 2130 { 2131 $this->_no_cas_server_validation = true; 2132 } 2133 2134 /** 2135 * This method is used to validate a CAS 1,0 ticket; halt on failure, and 2136 * sets $validate_url, $text_reponse and $tree_response on success. 2137 * 2138 * @param string &$validate_url reference to the the URL of the request to 2139 * the CAS server. 2140 * @param string &$text_response reference to the response of the CAS 2141 * server, as is (XML text). 2142 * @param string &$tree_response reference to the response of the CAS 2143 * server, as a DOM XML tree. 2144 * @param bool $renew true to force the authentication with the CAS server 2145 * 2146 * @return bool true when successfull and issue a CAS_AuthenticationException 2147 * and false on an error 2148 * @throws CAS_AuthenticationException 2149 */ 2150 public function validateCAS10(&$validate_url,&$text_response,&$tree_response,$renew=false) 2151 { 2152 phpCAS::traceBegin(); 2153 // build the URL to validate the ticket 2154 $validate_url = $this->getServerServiceValidateURL() 2155 .'&ticket='.urlencode($this->getTicket()); 2156 2157 if ( $renew ) { 2158 // pass the renew 2159 $validate_url .= '&renew=true'; 2160 } 2161 2162 // open and read the URL 2163 if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) { 2164 phpCAS::trace( 2165 'could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')' 2166 ); 2167 throw new CAS_AuthenticationException( 2168 $this, 'CAS 1.0 ticket not validated', $validate_url, 2169 true/*$no_response*/ 2170 ); 2171 } 2172 2173 if (preg_match('/^no\n/', $text_response)) { 2174 phpCAS::trace('Ticket has not been validated'); 2175 throw new CAS_AuthenticationException( 2176 $this, 'ST not validated', $validate_url, false/*$no_response*/, 2177 false/*$bad_response*/, $text_response 2178 ); 2179 } else if (!preg_match('/^yes\n/', $text_response)) { 2180 phpCAS::trace('ill-formed response'); 2181 throw new CAS_AuthenticationException( 2182 $this, 'Ticket not validated', $validate_url, 2183 false/*$no_response*/, true/*$bad_response*/, $text_response 2184 ); 2185 } 2186 // ticket has been validated, extract the user name 2187 $arr = preg_split('/\n/', $text_response); 2188 $this->_setUser(trim($arr[1])); 2189 2190 $this->_renameSession($this->getTicket()); 2191 2192 // at this step, ticket has been validated and $this->_user has been set, 2193 phpCAS::traceEnd(true); 2194 return true; 2195 } 2196 2197 /** @} */ 2198 2199 2200 // ######################################################################## 2201 // SAML VALIDATION 2202 // ######################################################################## 2203 /** 2204 * @addtogroup internalSAML 2205 * @{ 2206 */ 2207 2208 /** 2209 * This method is used to validate a SAML TICKET; halt on failure, and sets 2210 * $validate_url, $text_reponse and $tree_response on success. These 2211 * parameters are used later by CAS_Client::_validatePGT() for CAS proxies. 2212 * 2213 * @param string &$validate_url reference to the the URL of the request to 2214 * the CAS server. 2215 * @param string &$text_response reference to the response of the CAS 2216 * server, as is (XML text). 2217 * @param string &$tree_response reference to the response of the CAS 2218 * server, as a DOM XML tree. 2219 * @param bool $renew true to force the authentication with the CAS server 2220 * 2221 * @return bool true when successfull and issue a CAS_AuthenticationException 2222 * and false on an error 2223 * 2224 * @throws CAS_AuthenticationException 2225 */ 2226 public function validateSA(&$validate_url,&$text_response,&$tree_response,$renew=false) 2227 { 2228 phpCAS::traceBegin(); 2229 $result = false; 2230 // build the URL to validate the ticket 2231 $validate_url = $this->getServerSamlValidateURL(); 2232 2233 if ( $renew ) { 2234 // pass the renew 2235 $validate_url .= '&renew=true'; 2236 } 2237 2238 // open and read the URL 2239 if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) { 2240 phpCAS::trace( 2241 'could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')' 2242 ); 2243 throw new CAS_AuthenticationException( 2244 $this, 'SA not validated', $validate_url, true/*$no_response*/ 2245 ); 2246 } 2247 2248 phpCAS::trace('server version: '.$this->getServerVersion()); 2249 2250 // analyze the result depending on the version 2251 switch ($this->getServerVersion()) { 2252 case SAML_VERSION_1_1: 2253 // create new DOMDocument Object 2254 $dom = new DOMDocument(); 2255 // Fix possible whitspace problems 2256 $dom->preserveWhiteSpace = false; 2257 // read the response of the CAS server into a DOM object 2258 if (!($dom->loadXML($text_response))) { 2259 phpCAS::trace('dom->loadXML() failed'); 2260 throw new CAS_AuthenticationException( 2261 $this, 'SA not validated', $validate_url, 2262 false/*$no_response*/, true/*$bad_response*/, 2263 $text_response 2264 ); 2265 } 2266 // read the root node of the XML tree 2267 if (!($tree_response = $dom->documentElement)) { 2268 phpCAS::trace('documentElement() failed'); 2269 throw new CAS_AuthenticationException( 2270 $this, 'SA not validated', $validate_url, 2271 false/*$no_response*/, true/*$bad_response*/, 2272 $text_response 2273 ); 2274 } else if ( $tree_response->localName != 'Envelope' ) { 2275 // insure that tag name is 'Envelope' 2276 phpCAS::trace( 2277 'bad XML root node (should be `Envelope\' instead of `' 2278 .$tree_response->localName.'\'' 2279 ); 2280 throw new CAS_AuthenticationException( 2281 $this, 'SA not validated', $validate_url, 2282 false/*$no_response*/, true/*$bad_response*/, 2283 $text_response 2284 ); 2285 } else if ($tree_response->getElementsByTagName("NameIdentifier")->length != 0) { 2286 // check for the NameIdentifier tag in the SAML response 2287 $success_elements = $tree_response->getElementsByTagName("NameIdentifier"); 2288 phpCAS::trace('NameIdentifier found'); 2289 $user = trim($success_elements->item(0)->nodeValue); 2290 phpCAS::trace('user = `'.$user.'`'); 2291 $this->_setUser($user); 2292 $this->_setSessionAttributes($text_response); 2293 $result = true; 2294 } else { 2295 phpCAS::trace('no <NameIdentifier> tag found in SAML payload'); 2296 throw new CAS_AuthenticationException( 2297 $this, 'SA not validated', $validate_url, 2298 false/*$no_response*/, true/*$bad_response*/, 2299 $text_response 2300 ); 2301 } 2302 } 2303 if ($result) { 2304 $this->_renameSession($this->getTicket()); 2305 } 2306 // at this step, ST has been validated and $this->_user has been set, 2307 phpCAS::traceEnd($result); 2308 return $result; 2309 } 2310 2311 /** 2312 * This method will parse the DOM and pull out the attributes from the SAML 2313 * payload and put them into an array, then put the array into the session. 2314 * 2315 * @param string $text_response the SAML payload. 2316 * 2317 * @return bool true when successfull and false if no attributes a found 2318 */ 2319 private function _setSessionAttributes($text_response) 2320 { 2321 phpCAS::traceBegin(); 2322 2323 $result = false; 2324 2325 $attr_array = array(); 2326 2327 // create new DOMDocument Object 2328 $dom = new DOMDocument(); 2329 // Fix possible whitspace problems 2330 $dom->preserveWhiteSpace = false; 2331 if (($dom->loadXML($text_response))) { 2332 $xPath = new DOMXPath($dom); 2333 $xPath->registerNamespace('samlp', 'urn:oasis:names:tc:SAML:1.0:protocol'); 2334 $xPath->registerNamespace('saml', 'urn:oasis:names:tc:SAML:1.0:assertion'); 2335 $nodelist = $xPath->query("//saml:Attribute"); 2336 2337 if ($nodelist) { 2338 foreach ($nodelist as $node) { 2339 $xres = $xPath->query("saml:AttributeValue", $node); 2340 $name = $node->getAttribute("AttributeName"); 2341 $value_array = array(); 2342 foreach ($xres as $node2) { 2343 $value_array[] = $node2->nodeValue; 2344 } 2345 $attr_array[$name] = $value_array; 2346 } 2347 // UGent addition... 2348 foreach ($attr_array as $attr_key => $attr_value) { 2349 if (count($attr_value) > 1) { 2350 $this->_attributes[$attr_key] = $attr_value; 2351 phpCAS::trace("* " . $attr_key . "=" . print_r($attr_value, true)); 2352 } else { 2353 $this->_attributes[$attr_key] = $attr_value[0]; 2354 phpCAS::trace("* " . $attr_key . "=" . $attr_value[0]); 2355 } 2356 } 2357 $result = true; 2358 } else { 2359 phpCAS::trace("SAML Attributes are empty"); 2360 $result = false; 2361 } 2362 } 2363 phpCAS::traceEnd($result); 2364 return $result; 2365 } 2366 2367 /** @} */ 2368 2369 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 2370 // XX XX 2371 // XX PROXY FEATURES (CAS 2.0) XX 2372 // XX XX 2373 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 2374 2375 // ######################################################################## 2376 // PROXYING 2377 // ######################################################################## 2378 /** 2379 * @addtogroup internalProxy 2380 * @{ 2381 */ 2382 2383 /** 2384 * @var bool is the client a proxy 2385 * A boolean telling if the client is a CAS proxy or not. Written by 2386 * CAS_Client::CAS_Client(), read by CAS_Client::isProxy(). 2387 */ 2388 private $_proxy; 2389 2390 /** 2391 * @var CAS_CookieJar Handler for managing service cookies. 2392 */ 2393 private $_serviceCookieJar; 2394 2395 /** 2396 * Tells if a CAS client is a CAS proxy or not 2397 * 2398 * @return bool true when the CAS client is a CAS proxy, false otherwise 2399 */ 2400 public function isProxy() 2401 { 2402 return $this->_proxy; 2403 } 2404 2405 2406 /** @} */ 2407 // ######################################################################## 2408 // PGT 2409 // ######################################################################## 2410 /** 2411 * @addtogroup internalProxy 2412 * @{ 2413 */ 2414 2415 /** 2416 * the Proxy Grnting Ticket given by the CAS server (empty otherwise). 2417 * Written by CAS_Client::_setPGT(), read by CAS_Client::_getPGT() and 2418 * CAS_Client::_hasPGT(). 2419 * 2420 * @hideinitializer 2421 */ 2422 private $_pgt = ''; 2423 2424 /** 2425 * This method returns the Proxy Granting Ticket given by the CAS server. 2426 * 2427 * @return string the Proxy Granting Ticket. 2428 */ 2429 private function _getPGT() 2430 { 2431 return $this->_pgt; 2432 } 2433 2434 /** 2435 * This method stores the Proxy Granting Ticket. 2436 * 2437 * @param string $pgt The Proxy Granting Ticket. 2438 * 2439 * @return void 2440 */ 2441 private function _setPGT($pgt) 2442 { 2443 $this->_pgt = $pgt; 2444 } 2445 2446 /** 2447 * This method tells if a Proxy Granting Ticket was stored. 2448 * 2449 * @return bool true if a Proxy Granting Ticket has been stored. 2450 */ 2451 private function _hasPGT() 2452 { 2453 return !empty($this->_pgt); 2454 } 2455 2456 /** @} */ 2457 2458 // ######################################################################## 2459 // CALLBACK MODE 2460 // ######################################################################## 2461 /** 2462 * @addtogroup internalCallback 2463 * @{ 2464 */ 2465 /** 2466 * each PHP script using phpCAS in proxy mode is its own callback to get the 2467 * PGT back from the CAS server. callback_mode is detected by the constructor 2468 * thanks to the GET parameters. 2469 */ 2470 2471 /** 2472 * @var bool a boolean to know if the CAS client is running in callback mode. Written by 2473 * CAS_Client::setCallBackMode(), read by CAS_Client::_isCallbackMode(). 2474 * 2475 * @hideinitializer 2476 */ 2477 private $_callback_mode = false; 2478 2479 /** 2480 * This method sets/unsets callback mode. 2481 * 2482 * @param bool $callback_mode true to set callback mode, false otherwise. 2483 * 2484 * @return void 2485 */ 2486 private function _setCallbackMode($callback_mode) 2487 { 2488 $this->_callback_mode = $callback_mode; 2489 } 2490 2491 /** 2492 * This method returns true when the CAS client is running in callback mode, 2493 * false otherwise. 2494 * 2495 * @return bool A boolean. 2496 */ 2497 private function _isCallbackMode() 2498 { 2499 return $this->_callback_mode; 2500 } 2501 2502 /** 2503 * @var bool a boolean to know if the CAS client is using POST parameters when in callback mode. 2504 * Written by CAS_Client::_setCallbackModeUsingPost(), read by CAS_Client::_isCallbackModeUsingPost(). 2505 * 2506 * @hideinitializer 2507 */ 2508 private $_callback_mode_using_post = false; 2509 2510 /** 2511 * This method sets/unsets usage of POST parameters in callback mode (default/false is GET parameters) 2512 * 2513 * @param bool $callback_mode_using_post true to use POST, false to use GET (default). 2514 * 2515 * @return void 2516 */ 2517 private function _setCallbackModeUsingPost($callback_mode_using_post) 2518 { 2519 $this->_callback_mode_using_post = $callback_mode_using_post; 2520 } 2521 2522 /** 2523 * This method returns true when the callback mode is using POST, false otherwise. 2524 * 2525 * @return bool A boolean. 2526 */ 2527 private function _isCallbackModeUsingPost() 2528 { 2529 return $this->_callback_mode_using_post; 2530 } 2531 2532 /** 2533 * the URL that should be used for the PGT callback (in fact the URL of the 2534 * current request without any CGI parameter). Written and read by 2535 * CAS_Client::_getCallbackURL(). 2536 * 2537 * @hideinitializer 2538 */ 2539 private $_callback_url = ''; 2540 2541 /** 2542 * This method returns the URL that should be used for the PGT callback (in 2543 * fact the URL of the current request without any CGI parameter, except if 2544 * phpCAS::setFixedCallbackURL() was used). 2545 * 2546 * @return string The callback URL 2547 */ 2548 private function _getCallbackURL() 2549 { 2550 // the URL is built when needed only 2551 if ( empty($this->_callback_url) ) { 2552 // remove the ticket if present in the URL 2553 $final_uri = 'https://'; 2554 $final_uri .= $this->_getClientUrl(); 2555 $request_uri = $_SERVER['REQUEST_URI']; 2556 $request_uri = preg_replace('/\?.*$/', '', $request_uri); 2557 $final_uri .= $request_uri; 2558 $this->_callback_url = $final_uri; 2559 } 2560 return $this->_callback_url; 2561 } 2562 2563 /** 2564 * This method sets the callback url. 2565 * 2566 * @param string $url url to set callback 2567 * 2568 * @return string the callback url 2569 */ 2570 public function setCallbackURL($url) 2571 { 2572 // Sequence validation 2573 $this->ensureIsProxy(); 2574 // Argument Validation 2575 if (gettype($url) != 'string') 2576 throw new CAS_TypeMismatchException($url, '$url', 'string'); 2577 2578 return $this->_callback_url = $url; 2579 } 2580 2581 /** 2582 * This method is called by CAS_Client::CAS_Client() when running in callback 2583 * mode. It stores the PGT and its PGT Iou, prints its output and halts. 2584 * 2585 * @return void 2586 */ 2587 private function _callback() 2588 { 2589 phpCAS::traceBegin(); 2590 if ($this->_isCallbackModeUsingPost()) { 2591 $pgtId = $_POST['pgtId']; 2592 $pgtIou = $_POST['pgtIou']; 2593 } else { 2594 $pgtId = $_GET['pgtId']; 2595 $pgtIou = $_GET['pgtIou']; 2596 } 2597 if (preg_match('/^PGTIOU-[\.\-\w]+$/', $pgtIou)) { 2598 if (preg_match('/^[PT]GT-[\.\-\w]+$/', $pgtId)) { 2599 phpCAS::trace('Storing PGT `'.$pgtId.'\' (id=`'.$pgtIou.'\')'); 2600 $this->_storePGT($pgtId, $pgtIou); 2601 if ($this->isXmlResponse()) { 2602 echo '<?xml version="1.0" encoding="UTF-8"?>' . "\r\n"; 2603 echo '<proxySuccess xmlns="http://www.yale.edu/tp/cas" />'; 2604 phpCAS::traceExit("XML response sent"); 2605 } else { 2606 $this->printHTMLHeader('phpCAS callback'); 2607 echo '<p>Storing PGT `'.$pgtId.'\' (id=`'.$pgtIou.'\').</p>'; 2608 $this->printHTMLFooter(); 2609 phpCAS::traceExit("HTML response sent"); 2610 } 2611 phpCAS::traceExit("Successfull Callback"); 2612 } else { 2613 phpCAS::error('PGT format invalid' . $pgtId); 2614 phpCAS::traceExit('PGT format invalid' . $pgtId); 2615 } 2616 } else { 2617 phpCAS::error('PGTiou format invalid' . $pgtIou); 2618 phpCAS::traceExit('PGTiou format invalid' . $pgtIou); 2619 } 2620 2621 // Flush the buffer to prevent from sending anything other then a 200 2622 // Success Status back to the CAS Server. The Exception would normally 2623 // report as a 500 error. 2624 flush(); 2625 throw new CAS_GracefullTerminationException(); 2626 } 2627 2628 /** 2629 * Check if application/xml or text/xml is pressent in HTTP_ACCEPT header values 2630 * when return value is complex and contains attached q parameters. 2631 * Example: HTTP_ACCEPT = text/html,application/xhtml+xml,application/xml;q=0.9 2632 * @return bool 2633 */ 2634 private function isXmlResponse() 2635 { 2636 if (!array_key_exists('HTTP_ACCEPT', $_SERVER)) { 2637 return false; 2638 } 2639 if (strpos($_SERVER['HTTP_ACCEPT'], 'application/xml') === false && strpos($_SERVER['HTTP_ACCEPT'], 'text/xml') === false) { 2640 return false; 2641 } 2642 2643 return true; 2644 } 2645 2646 /** @} */ 2647 2648 // ######################################################################## 2649 // PGT STORAGE 2650 // ######################################################################## 2651 /** 2652 * @addtogroup internalPGTStorage 2653 * @{ 2654 */ 2655 2656 /** 2657 * @var CAS_PGTStorage_AbstractStorage 2658 * an instance of a class inheriting of PGTStorage, used to deal with PGT 2659 * storage. Created by CAS_Client::setPGTStorageFile(), used 2660 * by CAS_Client::setPGTStorageFile() and CAS_Client::_initPGTStorage(). 2661 * 2662 * @hideinitializer 2663 */ 2664 private $_pgt_storage = null; 2665 2666 /** 2667 * This method is used to initialize the storage of PGT's. 2668 * Halts on error. 2669 * 2670 * @return void 2671 */ 2672 private function _initPGTStorage() 2673 { 2674 // if no SetPGTStorageXxx() has been used, default to file 2675 if ( !is_object($this->_pgt_storage) ) { 2676 $this->setPGTStorageFile(); 2677 } 2678 2679 // initializes the storage 2680 $this->_pgt_storage->init(); 2681 } 2682 2683 /** 2684 * This method stores a PGT. Halts on error. 2685 * 2686 * @param string $pgt the PGT to store 2687 * @param string $pgt_iou its corresponding Iou 2688 * 2689 * @return void 2690 */ 2691 private function _storePGT($pgt,$pgt_iou) 2692 { 2693 // ensure that storage is initialized 2694 $this->_initPGTStorage(); 2695 // writes the PGT 2696 $this->_pgt_storage->write($pgt, $pgt_iou); 2697 } 2698 2699 /** 2700 * This method reads a PGT from its Iou and deletes the corresponding 2701 * storage entry. 2702 * 2703 * @param string $pgt_iou the PGT Iou 2704 * 2705 * @return string mul The PGT corresponding to the Iou, false when not found. 2706 */ 2707 private function _loadPGT($pgt_iou) 2708 { 2709 // ensure that storage is initialized 2710 $this->_initPGTStorage(); 2711 // read the PGT 2712 return $this->_pgt_storage->read($pgt_iou); 2713 } 2714 2715 /** 2716 * This method can be used to set a custom PGT storage object. 2717 * 2718 * @param CAS_PGTStorage_AbstractStorage $storage a PGT storage object that 2719 * inherits from the CAS_PGTStorage_AbstractStorage class 2720 * 2721 * @return void 2722 */ 2723 public function setPGTStorage($storage) 2724 { 2725 // Sequence validation 2726 $this->ensureIsProxy(); 2727 2728 // check that the storage has not already been set 2729 if ( is_object($this->_pgt_storage) ) { 2730 phpCAS::error('PGT storage already defined'); 2731 } 2732 2733 // check to make sure a valid storage object was specified 2734 if ( !($storage instanceof CAS_PGTStorage_AbstractStorage) ) 2735 throw new CAS_TypeMismatchException($storage, '$storage', 'CAS_PGTStorage_AbstractStorage object'); 2736 2737 // store the PGTStorage object 2738 $this->_pgt_storage = $storage; 2739 } 2740 2741 /** 2742 * This method is used to tell phpCAS to store the response of the 2743 * CAS server to PGT requests in a database. 2744 * 2745 * @param string|PDO $dsn_or_pdo a dsn string to use for creating a PDO 2746 * object or a PDO object 2747 * @param string $username the username to use when connecting to the 2748 * database 2749 * @param string $password the password to use when connecting to the 2750 * database 2751 * @param string $table the table to use for storing and retrieving 2752 * PGTs 2753 * @param string $driver_options any driver options to use when connecting 2754 * to the database 2755 * 2756 * @return void 2757 */ 2758 public function setPGTStorageDb( 2759 $dsn_or_pdo, $username='', $password='', $table='', $driver_options=null 2760 ) { 2761 // Sequence validation 2762 $this->ensureIsProxy(); 2763 2764 // Argument validation 2765 if (!(is_object($dsn_or_pdo) && $dsn_or_pdo instanceof PDO) && !is_string($dsn_or_pdo)) 2766 throw new CAS_TypeMismatchException($dsn_or_pdo, '$dsn_or_pdo', 'string or PDO object'); 2767 if (gettype($username) != 'string') 2768 throw new CAS_TypeMismatchException($username, '$username', 'string'); 2769 if (gettype($password) != 'string') 2770 throw new CAS_TypeMismatchException($password, '$password', 'string'); 2771 if (gettype($table) != 'string') 2772 throw new CAS_TypeMismatchException($table, '$password', 'string'); 2773 2774 // create the storage object 2775 $this->setPGTStorage( 2776 new CAS_PGTStorage_Db( 2777 $this, $dsn_or_pdo, $username, $password, $table, $driver_options 2778 ) 2779 ); 2780 } 2781 2782 /** 2783 * This method is used to tell phpCAS to store the response of the 2784 * CAS server to PGT requests onto the filesystem. 2785 * 2786 * @param string $path the path where the PGT's should be stored 2787 * 2788 * @return void 2789 */ 2790 public function setPGTStorageFile($path='') 2791 { 2792 // Sequence validation 2793 $this->ensureIsProxy(); 2794 2795 // Argument validation 2796 if (gettype($path) != 'string') 2797 throw new CAS_TypeMismatchException($path, '$path', 'string'); 2798 2799 // create the storage object 2800 $this->setPGTStorage(new CAS_PGTStorage_File($this, $path)); 2801 } 2802 2803 2804 // ######################################################################## 2805 // PGT VALIDATION 2806 // ######################################################################## 2807 /** 2808 * This method is used to validate a PGT; halt on failure. 2809 * 2810 * @param string &$validate_url the URL of the request to the CAS server. 2811 * @param string $text_response the response of the CAS server, as is 2812 * (XML text); result of 2813 * CAS_Client::validateCAS10() or 2814 * CAS_Client::validateCAS20(). 2815 * @param DOMElement $tree_response the response of the CAS server, as a DOM XML 2816 * tree; result of CAS_Client::validateCAS10() or CAS_Client::validateCAS20(). 2817 * 2818 * @return bool true when successfull and issue a CAS_AuthenticationException 2819 * and false on an error 2820 * 2821 * @throws CAS_AuthenticationException 2822 */ 2823 private function _validatePGT(&$validate_url,$text_response,$tree_response) 2824 { 2825 phpCAS::traceBegin(); 2826 if ( $tree_response->getElementsByTagName("proxyGrantingTicket")->length == 0) { 2827 phpCAS::trace('<proxyGrantingTicket> not found'); 2828 // authentication succeded, but no PGT Iou was transmitted 2829 throw new CAS_AuthenticationException( 2830 $this, 'Ticket validated but no PGT Iou transmitted', 2831 $validate_url, false/*$no_response*/, false/*$bad_response*/, 2832 $text_response 2833 ); 2834 } else { 2835 // PGT Iou transmitted, extract it 2836 $pgt_iou = trim( 2837 $tree_response->getElementsByTagName("proxyGrantingTicket")->item(0)->nodeValue 2838 ); 2839 if (preg_match('/^PGTIOU-[\.\-\w]+$/', $pgt_iou)) { 2840 $pgt = $this->_loadPGT($pgt_iou); 2841 if ( $pgt == false ) { 2842 phpCAS::trace('could not load PGT'); 2843 throw new CAS_AuthenticationException( 2844 $this, 2845 'PGT Iou was transmitted but PGT could not be retrieved', 2846 $validate_url, false/*$no_response*/, 2847 false/*$bad_response*/, $text_response 2848 ); 2849 } 2850 $this->_setPGT($pgt); 2851 } else { 2852 phpCAS::trace('PGTiou format error'); 2853 throw new CAS_AuthenticationException( 2854 $this, 'PGT Iou was transmitted but has wrong format', 2855 $validate_url, false/*$no_response*/, false/*$bad_response*/, 2856 $text_response 2857 ); 2858 } 2859 } 2860 phpCAS::traceEnd(true); 2861 return true; 2862 } 2863 2864 // ######################################################################## 2865 // PGT VALIDATION 2866 // ######################################################################## 2867 2868 /** 2869 * This method is used to retrieve PT's from the CAS server thanks to a PGT. 2870 * 2871 * @param string $target_service the service to ask for with the PT. 2872 * @param int &$err_code an error code (PHPCAS_SERVICE_OK on success). 2873 * @param string &$err_msg an error message (empty on success). 2874 * 2875 * @return string|false a Proxy Ticket, or false on error. 2876 */ 2877 public function retrievePT($target_service,&$err_code,&$err_msg) 2878 { 2879 // Argument validation 2880 if (gettype($target_service) != 'string') 2881 throw new CAS_TypeMismatchException($target_service, '$target_service', 'string'); 2882 2883 phpCAS::traceBegin(); 2884 2885 // by default, $err_msg is set empty and $pt to true. On error, $pt is 2886 // set to false and $err_msg to an error message. At the end, if $pt is false 2887 // and $error_msg is still empty, it is set to 'invalid response' (the most 2888 // commonly encountered error). 2889 $err_msg = ''; 2890 2891 // build the URL to retrieve the PT 2892 $cas_url = $this->getServerProxyURL().'?targetService=' 2893 .urlencode($target_service).'&pgt='.$this->_getPGT(); 2894 2895 // open and read the URL 2896 if ( !$this->_readURL($cas_url, $headers, $cas_response, $err_msg) ) { 2897 phpCAS::trace( 2898 'could not open URL \''.$cas_url.'\' to validate ('.$err_msg.')' 2899 ); 2900 $err_code = PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE; 2901 $err_msg = 'could not retrieve PT (no response from the CAS server)'; 2902 phpCAS::traceEnd(false); 2903 return false; 2904 } 2905 2906 $bad_response = false; 2907 2908 // create new DOMDocument object 2909 $dom = new DOMDocument(); 2910 // Fix possible whitspace problems 2911 $dom->preserveWhiteSpace = false; 2912 // read the response of the CAS server into a DOM object 2913 if ( !($dom->loadXML($cas_response))) { 2914 phpCAS::trace('dom->loadXML() failed'); 2915 // read failed 2916 $bad_response = true; 2917 } 2918 2919 if ( !$bad_response ) { 2920 // read the root node of the XML tree 2921 if ( !($root = $dom->documentElement) ) { 2922 phpCAS::trace('documentElement failed'); 2923 // read failed 2924 $bad_response = true; 2925 } 2926 } 2927 2928 if ( !$bad_response ) { 2929 // insure that tag name is 'serviceResponse' 2930 if ( $root->localName != 'serviceResponse' ) { 2931 phpCAS::trace('localName failed'); 2932 // bad root node 2933 $bad_response = true; 2934 } 2935 } 2936 2937 if ( !$bad_response ) { 2938 // look for a proxySuccess tag 2939 if ( $root->getElementsByTagName("proxySuccess")->length != 0) { 2940 $proxy_success_list = $root->getElementsByTagName("proxySuccess"); 2941 2942 // authentication succeded, look for a proxyTicket tag 2943 if ( $proxy_success_list->item(0)->getElementsByTagName("proxyTicket")->length != 0) { 2944 $err_code = PHPCAS_SERVICE_OK; 2945 $err_msg = ''; 2946 $pt = trim( 2947 $proxy_success_list->item(0)->getElementsByTagName("proxyTicket")->item(0)->nodeValue 2948 ); 2949 phpCAS::trace('original PT: '.trim($pt)); 2950 phpCAS::traceEnd($pt); 2951 return $pt; 2952 } else { 2953 phpCAS::trace('<proxySuccess> was found, but not <proxyTicket>'); 2954 } 2955 } else if ($root->getElementsByTagName("proxyFailure")->length != 0) { 2956 // look for a proxyFailure tag 2957 $proxy_failure_list = $root->getElementsByTagName("proxyFailure"); 2958 2959 // authentication failed, extract the error 2960 $err_code = PHPCAS_SERVICE_PT_FAILURE; 2961 $err_msg = 'PT retrieving failed (code=`' 2962 .$proxy_failure_list->item(0)->getAttribute('code') 2963 .'\', message=`' 2964 .trim($proxy_failure_list->item(0)->nodeValue) 2965 .'\')'; 2966 phpCAS::traceEnd(false); 2967 return false; 2968 } else { 2969 phpCAS::trace('neither <proxySuccess> nor <proxyFailure> found'); 2970 } 2971 } 2972 2973 // at this step, we are sure that the response of the CAS server was 2974 // illformed 2975 $err_code = PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE; 2976 $err_msg = 'Invalid response from the CAS server (response=`' 2977 .$cas_response.'\')'; 2978 2979 phpCAS::traceEnd(false); 2980 return false; 2981 } 2982 2983 /** @} */ 2984 2985 // ######################################################################## 2986 // READ CAS SERVER ANSWERS 2987 // ######################################################################## 2988 2989 /** 2990 * @addtogroup internalMisc 2991 * @{ 2992 */ 2993 2994 /** 2995 * This method is used to acces a remote URL. 2996 * 2997 * @param string $url the URL to access. 2998 * @param string &$headers an array containing the HTTP header lines of the 2999 * response (an empty array on failure). 3000 * @param string &$body the body of the response, as a string (empty on 3001 * failure). 3002 * @param string &$err_msg an error message, filled on failure. 3003 * 3004 * @return bool true on success, false otherwise (in this later case, $err_msg 3005 * contains an error message). 3006 */ 3007 private function _readURL($url, &$headers, &$body, &$err_msg) 3008 { 3009 phpCAS::traceBegin(); 3010 $className = $this->_requestImplementation; 3011 $request = new $className(); 3012 3013 if (count($this->_curl_options)) { 3014 $request->setCurlOptions($this->_curl_options); 3015 } 3016 3017 $request->setUrl($url); 3018 3019 if (empty($this->_cas_server_ca_cert) && !$this->_no_cas_server_validation) { 3020 phpCAS::error( 3021 'one of the methods phpCAS::setCasServerCACert() or phpCAS::setNoCasServerValidation() must be called.' 3022 ); 3023 } 3024 if ($this->_cas_server_ca_cert != '') { 3025 $request->setSslCaCert( 3026 $this->_cas_server_ca_cert, $this->_cas_server_cn_validate 3027 ); 3028 } 3029 3030 // add extra stuff if SAML 3031 if ($this->getServerVersion() == SAML_VERSION_1_1) { 3032 $request->addHeader("soapaction: http://www.oasis-open.org/committees/security"); 3033 $request->addHeader("cache-control: no-cache"); 3034 $request->addHeader("pragma: no-cache"); 3035 $request->addHeader("accept: text/xml"); 3036 $request->addHeader("connection: keep-alive"); 3037 $request->addHeader("content-type: text/xml"); 3038 $request->makePost(); 3039 $request->setPostBody($this->_buildSAMLPayload()); 3040 } 3041 3042 if ($request->send()) { 3043 $headers = $request->getResponseHeaders(); 3044 $body = $request->getResponseBody(); 3045 $err_msg = ''; 3046 phpCAS::traceEnd(true); 3047 return true; 3048 } else { 3049 $headers = ''; 3050 $body = ''; 3051 $err_msg = $request->getErrorMessage(); 3052 phpCAS::traceEnd(false); 3053 return false; 3054 } 3055 } 3056 3057 /** 3058 * This method is used to build the SAML POST body sent to /samlValidate URL. 3059 * 3060 * @return string the SOAP-encased SAMLP artifact (the ticket). 3061 */ 3062 private function _buildSAMLPayload() 3063 { 3064 phpCAS::traceBegin(); 3065 3066 //get the ticket 3067 $sa = urlencode($this->getTicket()); 3068 3069 $body = SAML_SOAP_ENV.SAML_SOAP_BODY.SAMLP_REQUEST 3070 .SAML_ASSERTION_ARTIFACT.$sa.SAML_ASSERTION_ARTIFACT_CLOSE 3071 .SAMLP_REQUEST_CLOSE.SAML_SOAP_BODY_CLOSE.SAML_SOAP_ENV_CLOSE; 3072 3073 phpCAS::traceEnd($body); 3074 return ($body); 3075 } 3076 3077 /** @} **/ 3078 3079 // ######################################################################## 3080 // ACCESS TO EXTERNAL SERVICES 3081 // ######################################################################## 3082 3083 /** 3084 * @addtogroup internalProxyServices 3085 * @{ 3086 */ 3087 3088 3089 /** 3090 * Answer a proxy-authenticated service handler. 3091 * 3092 * @param string $type The service type. One of: 3093 * PHPCAS_PROXIED_SERVICE_HTTP_GET, PHPCAS_PROXIED_SERVICE_HTTP_POST, 3094 * PHPCAS_PROXIED_SERVICE_IMAP 3095 * 3096 * @return CAS_ProxiedService 3097 * @throws InvalidArgumentException If the service type is unknown. 3098 */ 3099 public function getProxiedService ($type) 3100 { 3101 // Sequence validation 3102 $this->ensureIsProxy(); 3103 $this->ensureAuthenticationCallSuccessful(); 3104 3105 // Argument validation 3106 if (gettype($type) != 'string') 3107 throw new CAS_TypeMismatchException($type, '$type', 'string'); 3108 3109 switch ($type) { 3110 case PHPCAS_PROXIED_SERVICE_HTTP_GET: 3111 case PHPCAS_PROXIED_SERVICE_HTTP_POST: 3112 $requestClass = $this->_requestImplementation; 3113 $request = new $requestClass(); 3114 if (count($this->_curl_options)) { 3115 $request->setCurlOptions($this->_curl_options); 3116 } 3117 $proxiedService = new $type($request, $this->_serviceCookieJar); 3118 if ($proxiedService instanceof CAS_ProxiedService_Testable) { 3119 $proxiedService->setCasClient($this); 3120 } 3121 return $proxiedService; 3122 case PHPCAS_PROXIED_SERVICE_IMAP; 3123 $proxiedService = new CAS_ProxiedService_Imap($this->_getUser()); 3124 if ($proxiedService instanceof CAS_ProxiedService_Testable) { 3125 $proxiedService->setCasClient($this); 3126 } 3127 return $proxiedService; 3128 default: 3129 throw new CAS_InvalidArgumentException( 3130 "Unknown proxied-service type, $type." 3131 ); 3132 } 3133 } 3134 3135 /** 3136 * Initialize a proxied-service handler with the proxy-ticket it should use. 3137 * 3138 * @param CAS_ProxiedService $proxiedService service handler 3139 * 3140 * @return void 3141 * 3142 * @throws CAS_ProxyTicketException If there is a proxy-ticket failure. 3143 * The code of the Exception will be one of: 3144 * PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE 3145 * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE 3146 * PHPCAS_SERVICE_PT_FAILURE 3147 * @throws CAS_ProxiedService_Exception If there is a failure getting the 3148 * url from the proxied service. 3149 */ 3150 public function initializeProxiedService (CAS_ProxiedService $proxiedService) 3151 { 3152 // Sequence validation 3153 $this->ensureIsProxy(); 3154 $this->ensureAuthenticationCallSuccessful(); 3155 3156 $url = $proxiedService->getServiceUrl(); 3157 if (!is_string($url)) { 3158 throw new CAS_ProxiedService_Exception( 3159 "Proxied Service ".get_class($proxiedService) 3160 ."->getServiceUrl() should have returned a string, returned a " 3161 .gettype($url)." instead." 3162 ); 3163 } 3164 $pt = $this->retrievePT($url, $err_code, $err_msg); 3165 if (!$pt) { 3166 throw new CAS_ProxyTicketException($err_msg, $err_code); 3167 } 3168 $proxiedService->setProxyTicket($pt); 3169 } 3170 3171 /** 3172 * This method is used to access an HTTP[S] service. 3173 * 3174 * @param string $url the service to access. 3175 * @param int &$err_code an error code Possible values are 3176 * PHPCAS_SERVICE_OK (on success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE, 3177 * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, PHPCAS_SERVICE_PT_FAILURE, 3178 * PHPCAS_SERVICE_NOT_AVAILABLE. 3179 * @param string &$output the output of the service (also used to give an error 3180 * message on failure). 3181 * 3182 * @return bool true on success, false otherwise (in this later case, $err_code 3183 * gives the reason why it failed and $output contains an error message). 3184 */ 3185 public function serviceWeb($url,&$err_code,&$output) 3186 { 3187 // Sequence validation 3188 $this->ensureIsProxy(); 3189 $this->ensureAuthenticationCallSuccessful(); 3190 3191 // Argument validation 3192 if (gettype($url) != 'string') 3193 throw new CAS_TypeMismatchException($url, '$url', 'string'); 3194 3195 try { 3196 $service = $this->getProxiedService(PHPCAS_PROXIED_SERVICE_HTTP_GET); 3197 $service->setUrl($url); 3198 $service->send(); 3199 $output = $service->getResponseBody(); 3200 $err_code = PHPCAS_SERVICE_OK; 3201 return true; 3202 } catch (CAS_ProxyTicketException $e) { 3203 $err_code = $e->getCode(); 3204 $output = $e->getMessage(); 3205 return false; 3206 } catch (CAS_ProxiedService_Exception $e) { 3207 $lang = $this->getLangObj(); 3208 $output = sprintf( 3209 $lang->getServiceUnavailable(), $url, $e->getMessage() 3210 ); 3211 $err_code = PHPCAS_SERVICE_NOT_AVAILABLE; 3212 return false; 3213 } 3214 } 3215 3216 /** 3217 * This method is used to access an IMAP/POP3/NNTP service. 3218 * 3219 * @param string $url a string giving the URL of the service, including 3220 * the mailing box for IMAP URLs, as accepted by imap_open(). 3221 * @param string $serviceUrl a string giving for CAS retrieve Proxy ticket 3222 * @param string $flags options given to imap_open(). 3223 * @param int &$err_code an error code Possible values are 3224 * PHPCAS_SERVICE_OK (on success), PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE, 3225 * PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE, PHPCAS_SERVICE_PT_FAILURE, 3226 * PHPCAS_SERVICE_NOT_AVAILABLE. 3227 * @param string &$err_msg an error message on failure 3228 * @param string &$pt the Proxy Ticket (PT) retrieved from the CAS 3229 * server to access the URL on success, false on error). 3230 * 3231 * @return object|false an IMAP stream on success, false otherwise (in this later 3232 * case, $err_code gives the reason why it failed and $err_msg contains an 3233 * error message). 3234 */ 3235 public function serviceMail($url,$serviceUrl,$flags,&$err_code,&$err_msg,&$pt) 3236 { 3237 // Sequence validation 3238 $this->ensureIsProxy(); 3239 $this->ensureAuthenticationCallSuccessful(); 3240 3241 // Argument validation 3242 if (gettype($url) != 'string') 3243 throw new CAS_TypeMismatchException($url, '$url', 'string'); 3244 if (gettype($serviceUrl) != 'string') 3245 throw new CAS_TypeMismatchException($serviceUrl, '$serviceUrl', 'string'); 3246 if (gettype($flags) != 'integer') 3247 throw new CAS_TypeMismatchException($flags, '$flags', 'string'); 3248 3249 try { 3250 $service = $this->getProxiedService(PHPCAS_PROXIED_SERVICE_IMAP); 3251 $service->setServiceUrl($serviceUrl); 3252 $service->setMailbox($url); 3253 $service->setOptions($flags); 3254 3255 $stream = $service->open(); 3256 $err_code = PHPCAS_SERVICE_OK; 3257 $pt = $service->getImapProxyTicket(); 3258 return $stream; 3259 } catch (CAS_ProxyTicketException $e) { 3260 $err_msg = $e->getMessage(); 3261 $err_code = $e->getCode(); 3262 $pt = false; 3263 return false; 3264 } catch (CAS_ProxiedService_Exception $e) { 3265 $lang = $this->getLangObj(); 3266 $err_msg = sprintf( 3267 $lang->getServiceUnavailable(), 3268 $url, 3269 $e->getMessage() 3270 ); 3271 $err_code = PHPCAS_SERVICE_NOT_AVAILABLE; 3272 $pt = false; 3273 return false; 3274 } 3275 } 3276 3277 /** @} **/ 3278 3279 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 3280 // XX XX 3281 // XX PROXIED CLIENT FEATURES (CAS 2.0) XX 3282 // XX XX 3283 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 3284 3285 // ######################################################################## 3286 // PT 3287 // ######################################################################## 3288 /** 3289 * @addtogroup internalService 3290 * @{ 3291 */ 3292 3293 /** 3294 * This array will store a list of proxies in front of this application. This 3295 * property will only be populated if this script is being proxied rather than 3296 * accessed directly. 3297 * 3298 * It is set in CAS_Client::validateCAS20() and can be read by 3299 * CAS_Client::getProxies() 3300 * 3301 * @access private 3302 */ 3303 private $_proxies = array(); 3304 3305 /** 3306 * Answer an array of proxies that are sitting in front of this application. 3307 * 3308 * This method will only return a non-empty array if we have received and 3309 * validated a Proxy Ticket. 3310 * 3311 * @return array 3312 * @access public 3313 */ 3314 public function getProxies() 3315 { 3316 return $this->_proxies; 3317 } 3318 3319 /** 3320 * Set the Proxy array, probably from persistant storage. 3321 * 3322 * @param array $proxies An array of proxies 3323 * 3324 * @return void 3325 * @access private 3326 */ 3327 private function _setProxies($proxies) 3328 { 3329 $this->_proxies = $proxies; 3330 if (!empty($proxies)) { 3331 // For proxy-authenticated requests people are not viewing the URL 3332 // directly since the client is another application making a 3333 // web-service call. 3334 // Because of this, stripping the ticket from the URL is unnecessary 3335 // and causes another web-service request to be performed. Additionally, 3336 // if session handling on either the client or the server malfunctions 3337 // then the subsequent request will not complete successfully. 3338 $this->setNoClearTicketsFromUrl(); 3339 } 3340 } 3341 3342 /** 3343 * A container of patterns to be allowed as proxies in front of the cas client. 3344 * 3345 * @var CAS_ProxyChain_AllowedList 3346 */ 3347 private $_allowed_proxy_chains; 3348 3349 /** 3350 * Answer the CAS_ProxyChain_AllowedList object for this client. 3351 * 3352 * @return CAS_ProxyChain_AllowedList 3353 */ 3354 public function getAllowedProxyChains () 3355 { 3356 if (empty($this->_allowed_proxy_chains)) { 3357 $this->_allowed_proxy_chains = new CAS_ProxyChain_AllowedList(); 3358 } 3359 return $this->_allowed_proxy_chains; 3360 } 3361 3362 /** @} */ 3363 // ######################################################################## 3364 // PT VALIDATION 3365 // ######################################################################## 3366 /** 3367 * @addtogroup internalProxied 3368 * @{ 3369 */ 3370 3371 /** 3372 * This method is used to validate a cas 2.0 ST or PT; halt on failure 3373 * Used for all CAS 2.0 validations 3374 * 3375 * @param string &$validate_url the url of the reponse 3376 * @param string &$text_response the text of the repsones 3377 * @param DOMElement &$tree_response the domxml tree of the respones 3378 * @param bool $renew true to force the authentication with the CAS server 3379 * 3380 * @return bool true when successfull and issue a CAS_AuthenticationException 3381 * and false on an error 3382 * 3383 * @throws CAS_AuthenticationException 3384 */ 3385 public function validateCAS20(&$validate_url,&$text_response,&$tree_response, $renew=false) 3386 { 3387 phpCAS::traceBegin(); 3388 phpCAS::trace($text_response); 3389 // build the URL to validate the ticket 3390 if ($this->getAllowedProxyChains()->isProxyingAllowed()) { 3391 $validate_url = $this->getServerProxyValidateURL().'&ticket=' 3392 .urlencode($this->getTicket()); 3393 } else { 3394 $validate_url = $this->getServerServiceValidateURL().'&ticket=' 3395 .urlencode($this->getTicket()); 3396 } 3397 3398 if ( $this->isProxy() ) { 3399 // pass the callback url for CAS proxies 3400 $validate_url .= '&pgtUrl='.urlencode($this->_getCallbackURL()); 3401 } 3402 3403 if ( $renew ) { 3404 // pass the renew 3405 $validate_url .= '&renew=true'; 3406 } 3407 3408 // open and read the URL 3409 if ( !$this->_readURL($validate_url, $headers, $text_response, $err_msg) ) { 3410 phpCAS::trace( 3411 'could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')' 3412 ); 3413 throw new CAS_AuthenticationException( 3414 $this, 'Ticket not validated', $validate_url, 3415 true/*$no_response*/ 3416 ); 3417 } 3418 3419 // create new DOMDocument object 3420 $dom = new DOMDocument(); 3421 // Fix possible whitspace problems 3422 $dom->preserveWhiteSpace = false; 3423 // CAS servers should only return data in utf-8 3424 $dom->encoding = "utf-8"; 3425 // read the response of the CAS server into a DOMDocument object 3426 if ( !($dom->loadXML($text_response))) { 3427 // read failed 3428 throw new CAS_AuthenticationException( 3429 $this, 'Ticket not validated', $validate_url, 3430 false/*$no_response*/, true/*$bad_response*/, $text_response 3431 ); 3432 } else if ( !($tree_response = $dom->documentElement) ) { 3433 // read the root node of the XML tree 3434 // read failed 3435 throw new CAS_AuthenticationException( 3436 $this, 'Ticket not validated', $validate_url, 3437 false/*$no_response*/, true/*$bad_response*/, $text_response 3438 ); 3439 } else if ($tree_response->localName != 'serviceResponse') { 3440 // insure that tag name is 'serviceResponse' 3441 // bad root node 3442 throw new CAS_AuthenticationException( 3443 $this, 'Ticket not validated', $validate_url, 3444 false/*$no_response*/, true/*$bad_response*/, $text_response 3445 ); 3446 } else if ( $tree_response->getElementsByTagName("authenticationFailure")->length != 0) { 3447 // authentication failed, extract the error code and message and throw exception 3448 $auth_fail_list = $tree_response 3449 ->getElementsByTagName("authenticationFailure"); 3450 throw new CAS_AuthenticationException( 3451 $this, 'Ticket not validated', $validate_url, 3452 false/*$no_response*/, false/*$bad_response*/, 3453 $text_response, 3454 $auth_fail_list->item(0)->getAttribute('code')/*$err_code*/, 3455 trim($auth_fail_list->item(0)->nodeValue)/*$err_msg*/ 3456 ); 3457 } else if ($tree_response->getElementsByTagName("authenticationSuccess")->length != 0) { 3458 // authentication succeded, extract the user name 3459 $success_elements = $tree_response 3460 ->getElementsByTagName("authenticationSuccess"); 3461 if ( $success_elements->item(0)->getElementsByTagName("user")->length == 0) { 3462 // no user specified => error 3463 throw new CAS_AuthenticationException( 3464 $this, 'Ticket not validated', $validate_url, 3465 false/*$no_response*/, true/*$bad_response*/, $text_response 3466 ); 3467 } else { 3468 $this->_setUser( 3469 trim( 3470 $success_elements->item(0)->getElementsByTagName("user")->item(0)->nodeValue 3471 ) 3472 ); 3473 $this->_readExtraAttributesCas20($success_elements); 3474 // Store the proxies we are sitting behind for authorization checking 3475 $proxyList = array(); 3476 if ( sizeof($arr = $success_elements->item(0)->getElementsByTagName("proxy")) > 0) { 3477 foreach ($arr as $proxyElem) { 3478 phpCAS::trace("Found Proxy: ".$proxyElem->nodeValue); 3479 $proxyList[] = trim($proxyElem->nodeValue); 3480 } 3481 $this->_setProxies($proxyList); 3482 phpCAS::trace("Storing Proxy List"); 3483 } 3484 // Check if the proxies in front of us are allowed 3485 if (!$this->getAllowedProxyChains()->isProxyListAllowed($proxyList)) { 3486 throw new CAS_AuthenticationException( 3487 $this, 'Proxy not allowed', $validate_url, 3488 false/*$no_response*/, true/*$bad_response*/, 3489 $text_response 3490 ); 3491 } else { 3492 $result = true; 3493 } 3494 } 3495 } else { 3496 throw new CAS_AuthenticationException( 3497 $this, 'Ticket not validated', $validate_url, 3498 false/*$no_response*/, true/*$bad_response*/, 3499 $text_response 3500 ); 3501 } 3502 3503 $this->_renameSession($this->getTicket()); 3504 3505 // at this step, Ticket has been validated and $this->_user has been set, 3506 3507 phpCAS::traceEnd($result); 3508 return $result; 3509 } 3510 3511 /** 3512 * This method recursively parses the attribute XML. 3513 * It also collapses name-value pairs into a single 3514 * array entry. It parses all common formats of 3515 * attributes and well formed XML files. 3516 * 3517 * @param string $root the DOM root element to be parsed 3518 * @param string $namespace namespace of the elements 3519 * 3520 * @return an array of the parsed XML elements 3521 * 3522 * Formats tested: 3523 * 3524 * "Jasig Style" Attributes: 3525 * 3526 * <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> 3527 * <cas:authenticationSuccess> 3528 * <cas:user>jsmith</cas:user> 3529 * <cas:attributes> 3530 * <cas:attraStyle>RubyCAS</cas:attraStyle> 3531 * <cas:surname>Smith</cas:surname> 3532 * <cas:givenName>John</cas:givenName> 3533 * <cas:memberOf>CN=Staff,OU=Groups,DC=example,DC=edu</cas:memberOf> 3534 * <cas:memberOf>CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu</cas:memberOf> 3535 * </cas:attributes> 3536 * <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket> 3537 * </cas:authenticationSuccess> 3538 * </cas:serviceResponse> 3539 * 3540 * "Jasig Style" Attributes (longer version): 3541 * 3542 * <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> 3543 * <cas:authenticationSuccess> 3544 * <cas:user>jsmith</cas:user> 3545 * <cas:attributes> 3546 * <cas:attribute> 3547 * <cas:name>surname</cas:name> 3548 * <cas:value>Smith</cas:value> 3549 * </cas:attribute> 3550 * <cas:attribute> 3551 * <cas:name>givenName</cas:name> 3552 * <cas:value>John</cas:value> 3553 * </cas:attribute> 3554 * <cas:attribute> 3555 * <cas:name>memberOf</cas:name> 3556 * <cas:value>['CN=Staff,OU=Groups,DC=example,DC=edu', 'CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu']</cas:value> 3557 * </cas:attribute> 3558 * </cas:attributes> 3559 * <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket> 3560 * </cas:authenticationSuccess> 3561 * </cas:serviceResponse> 3562 * 3563 * "RubyCAS Style" attributes 3564 * 3565 * <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> 3566 * <cas:authenticationSuccess> 3567 * <cas:user>jsmith</cas:user> 3568 * 3569 * <cas:attraStyle>RubyCAS</cas:attraStyle> 3570 * <cas:surname>Smith</cas:surname> 3571 * <cas:givenName>John</cas:givenName> 3572 * <cas:memberOf>CN=Staff,OU=Groups,DC=example,DC=edu</cas:memberOf> 3573 * <cas:memberOf>CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu</cas:memberOf> 3574 * 3575 * <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket> 3576 * </cas:authenticationSuccess> 3577 * </cas:serviceResponse> 3578 * 3579 * "Name-Value" attributes. 3580 * 3581 * Attribute format from these mailing list thread: 3582 * http://jasig.275507.n4.nabble.com/CAS-attributes-and-how-they-appear-in-the-CAS-response-td264272.html 3583 * Note: This is a less widely used format, but in use by at least two institutions. 3584 * 3585 * <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> 3586 * <cas:authenticationSuccess> 3587 * <cas:user>jsmith</cas:user> 3588 * 3589 * <cas:attribute name='attraStyle' value='Name-Value' /> 3590 * <cas:attribute name='surname' value='Smith' /> 3591 * <cas:attribute name='givenName' value='John' /> 3592 * <cas:attribute name='memberOf' value='CN=Staff,OU=Groups,DC=example,DC=edu' /> 3593 * <cas:attribute name='memberOf' value='CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu' /> 3594 * 3595 * <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket> 3596 * </cas:authenticationSuccess> 3597 * </cas:serviceResponse> 3598 * 3599 * result: 3600 * 3601 * Array ( 3602 * [surname] => Smith 3603 * [givenName] => John 3604 * [memberOf] => Array ( 3605 * [0] => CN=Staff, OU=Groups, DC=example, DC=edu 3606 * [1] => CN=Spanish Department, OU=Departments, OU=Groups, DC=example, DC=edu 3607 * ) 3608 * ) 3609 */ 3610 private function _xml_to_array($root, $namespace = "cas") 3611 { 3612 $result = array(); 3613 if ($root->hasAttributes()) { 3614 $attrs = $root->attributes; 3615 $pair = array(); 3616 foreach ($attrs as $attr) { 3617 if ($attr->name === "name") { 3618 $pair['name'] = $attr->value; 3619 } elseif ($attr->name === "value") { 3620 $pair['value'] = $attr->value; 3621 } else { 3622 $result[$attr->name] = $attr->value; 3623 } 3624 if (array_key_exists('name', $pair) && array_key_exists('value', $pair)) { 3625 $result[$pair['name']] = $pair['value']; 3626 } 3627 } 3628 } 3629 if ($root->hasChildNodes()) { 3630 $children = $root->childNodes; 3631 if ($children->length == 1) { 3632 $child = $children->item(0); 3633 if ($child->nodeType == XML_TEXT_NODE) { 3634 $result['_value'] = $child->nodeValue; 3635 return (count($result) == 1) ? $result['_value'] : $result; 3636 } 3637 } 3638 $groups = array(); 3639 foreach ($children as $child) { 3640 $child_nodeName = str_ireplace($namespace . ":", "", $child->nodeName); 3641 if (in_array($child_nodeName, array("user", "proxies", "proxyGrantingTicket"))) { 3642 continue; 3643 } 3644 if (!isset($result[$child_nodeName])) { 3645 $res = $this->_xml_to_array($child, $namespace); 3646 if (!empty($res)) { 3647 $result[$child_nodeName] = $this->_xml_to_array($child, $namespace); 3648 } 3649 } else { 3650 if (!isset($groups[$child_nodeName])) { 3651 $result[$child_nodeName] = array($result[$child_nodeName]); 3652 $groups[$child_nodeName] = 1; 3653 } 3654 $result[$child_nodeName][] = $this->_xml_to_array($child, $namespace); 3655 } 3656 } 3657 } 3658 return $result; 3659 } 3660 3661 /** 3662 * This method parses a "JSON-like array" of strings 3663 * into an array of strings 3664 * 3665 * @param string $json_value the json-like string: 3666 * e.g.: 3667 * ['CN=Staff,OU=Groups,DC=example,DC=edu', 'CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu'] 3668 * 3669 * @return array of strings Description 3670 * e.g.: 3671 * Array ( 3672 * [0] => CN=Staff,OU=Groups,DC=example,DC=edu 3673 * [1] => CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu 3674 * ) 3675 */ 3676 private function _parse_json_like_array_value($json_value) 3677 { 3678 $parts = explode(",", trim($json_value, "[]")); 3679 $out = array(); 3680 $quote = ''; 3681 foreach ($parts as $part) { 3682 $part = trim($part); 3683 if ($quote === '') { 3684 $value = ""; 3685 if ($this->_startsWith($part, '\'')) { 3686 $quote = '\''; 3687 } elseif ($this->_startsWith($part, '"')) { 3688 $quote = '"'; 3689 } else { 3690 $out[] = $part; 3691 } 3692 $part = ltrim($part, $quote); 3693 } 3694 if ($quote !== '') { 3695 $value .= $part; 3696 if ($this->_endsWith($part, $quote)) { 3697 $out[] = rtrim($value, $quote); 3698 $quote = ''; 3699 } else { 3700 $value .= ", "; 3701 }; 3702 } 3703 } 3704 return $out; 3705 } 3706 3707 /** 3708 * This method recursively removes unneccessary hirarchy levels in array-trees. 3709 * into an array of strings 3710 * 3711 * @param array $arr the array to flatten 3712 * e.g.: 3713 * Array ( 3714 * [attributes] => Array ( 3715 * [attribute] => Array ( 3716 * [0] => Array ( 3717 * [name] => surname 3718 * [value] => Smith 3719 * ) 3720 * [1] => Array ( 3721 * [name] => givenName 3722 * [value] => John 3723 * ) 3724 * [2] => Array ( 3725 * [name] => memberOf 3726 * [value] => ['CN=Staff,OU=Groups,DC=example,DC=edu', 'CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu'] 3727 * ) 3728 * ) 3729 * ) 3730 * ) 3731 * 3732 * @return array the flattened array 3733 * e.g.: 3734 * Array ( 3735 * [attribute] => Array ( 3736 * [surname] => Smith 3737 * [givenName] => John 3738 * [memberOf] => Array ( 3739 * [0] => CN=Staff, OU=Groups, DC=example, DC=edu 3740 * [1] => CN=Spanish Department, OU=Departments, OU=Groups, DC=example, DC=edu 3741 * ) 3742 * ) 3743 * ) 3744 */ 3745 private function _flatten_array($arr) 3746 { 3747 if (!is_array($arr)) { 3748 if ($this->_startsWith($arr, '[') && $this->_endsWith($arr, ']')) { 3749 return $this->_parse_json_like_array_value($arr); 3750 } else { 3751 return $arr; 3752 } 3753 } 3754 $out = array(); 3755 foreach ($arr as $key => $val) { 3756 if (!is_array($val)) { 3757 $out[$key] = $val; 3758 } else { 3759 switch (count($val)) { 3760 case 1 : { 3761 $key = key($val); 3762 if (array_key_exists($key, $out)) { 3763 $value = $out[$key]; 3764 if (!is_array($value)) { 3765 $out[$key] = array(); 3766 $out[$key][] = $value; 3767 } 3768 $out[$key][] = $this->_flatten_array($val[$key]); 3769 } else { 3770 $out[$key] = $this->_flatten_array($val[$key]); 3771 }; 3772 break; 3773 }; 3774 case 2 : { 3775 if (array_key_exists("name", $val) && array_key_exists("value", $val)) { 3776 $key = $val['name']; 3777 if (array_key_exists($key, $out)) { 3778 $value = $out[$key]; 3779 if (!is_array($value)) { 3780 $out[$key] = array(); 3781 $out[$key][] = $value; 3782 } 3783 $out[$key][] = $this->_flatten_array($val['value']); 3784 } else { 3785 $out[$key] = $this->_flatten_array($val['value']); 3786 }; 3787 } else { 3788 $out[$key] = $this->_flatten_array($val); 3789 } 3790 break; 3791 }; 3792 default: { 3793 $out[$key] = $this->_flatten_array($val); 3794 } 3795 } 3796 } 3797 } 3798 return $out; 3799 } 3800 3801 /** 3802 * This method will parse the DOM and pull out the attributes from the XML 3803 * payload and put them into an array, then put the array into the session. 3804 * 3805 * @param DOMNodeList $success_elements payload of the response 3806 * 3807 * @return bool true when successfull, halt otherwise by calling 3808 * CAS_Client::_authError(). 3809 */ 3810 private function _readExtraAttributesCas20($success_elements) 3811 { 3812 phpCAS::traceBegin(); 3813 3814 $extra_attributes = array(); 3815 if ($this->_casAttributeParserCallbackFunction !== null 3816 && is_callable($this->_casAttributeParserCallbackFunction) 3817 ) { 3818 array_unshift($this->_casAttributeParserCallbackArgs, $success_elements->item(0)); 3819 phpCAS :: trace("Calling attritubeParser callback"); 3820 $extra_attributes = call_user_func_array( 3821 $this->_casAttributeParserCallbackFunction, 3822 $this->_casAttributeParserCallbackArgs 3823 ); 3824 } else { 3825 phpCAS :: trace("Parse extra attributes: "); 3826 $attributes = $this->_xml_to_array($success_elements->item(0)); 3827 phpCAS :: trace(print_r($attributes,true). "\nFLATTEN Array: "); 3828 $extra_attributes = $this->_flatten_array($attributes); 3829 phpCAS :: trace(print_r($extra_attributes, true)."\nFILTER : "); 3830 if (array_key_exists("attribute", $extra_attributes)) { 3831 $extra_attributes = $extra_attributes["attribute"]; 3832 } elseif (array_key_exists("attributes", $extra_attributes)) { 3833 $extra_attributes = $extra_attributes["attributes"]; 3834 }; 3835 phpCAS :: trace(print_r($extra_attributes, true)."return"); 3836 } 3837 $this->setAttributes($extra_attributes); 3838 phpCAS::traceEnd(); 3839 return true; 3840 } 3841 3842 /** 3843 * Add an attribute value to an array of attributes. 3844 * 3845 * @param array &$attributeArray reference to array 3846 * @param string $name name of attribute 3847 * @param string $value value of attribute 3848 * 3849 * @return void 3850 */ 3851 private function _addAttributeToArray(array &$attributeArray, $name, $value) 3852 { 3853 // If multiple attributes exist, add as an array value 3854 if (isset($attributeArray[$name])) { 3855 // Initialize the array with the existing value 3856 if (!is_array($attributeArray[$name])) { 3857 $existingValue = $attributeArray[$name]; 3858 $attributeArray[$name] = array($existingValue); 3859 } 3860 3861 $attributeArray[$name][] = trim($value); 3862 } else { 3863 $attributeArray[$name] = trim($value); 3864 } 3865 } 3866 3867 /** @} */ 3868 3869 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 3870 // XX XX 3871 // XX MISC XX 3872 // XX XX 3873 // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 3874 3875 /** 3876 * @addtogroup internalMisc 3877 * @{ 3878 */ 3879 3880 // ######################################################################## 3881 // URL 3882 // ######################################################################## 3883 /** 3884 * the URL of the current request (without any ticket CGI parameter). Written 3885 * and read by CAS_Client::getURL(). 3886 * 3887 * @hideinitializer 3888 */ 3889 private $_url = ''; 3890 3891 3892 /** 3893 * This method sets the URL of the current request 3894 * 3895 * @param string $url url to set for service 3896 * 3897 * @return void 3898 */ 3899 public function setURL($url) 3900 { 3901 // Argument Validation 3902 if (gettype($url) != 'string') 3903 throw new CAS_TypeMismatchException($url, '$url', 'string'); 3904 3905 $this->_url = $url; 3906 } 3907 3908 /** 3909 * This method returns the URL of the current request (without any ticket 3910 * CGI parameter). 3911 * 3912 * @return string The URL 3913 */ 3914 public function getURL() 3915 { 3916 phpCAS::traceBegin(); 3917 // the URL is built when needed only 3918 if ( empty($this->_url) ) { 3919 // remove the ticket if present in the URL 3920 $final_uri = ($this->_isHttps()) ? 'https' : 'http'; 3921 $final_uri .= '://'; 3922 3923 $final_uri .= $this->_getClientUrl(); 3924 $request_uri = explode('?', $_SERVER['REQUEST_URI'], 2); 3925 $final_uri .= $request_uri[0]; 3926 3927 if (isset($request_uri[1]) && $request_uri[1]) { 3928 $query_string= $this->_removeParameterFromQueryString('ticket', $request_uri[1]); 3929 3930 // If the query string still has anything left, 3931 // append it to the final URI 3932 if ($query_string !== '') { 3933 $final_uri .= "?$query_string"; 3934 } 3935 } 3936 3937 phpCAS::trace("Final URI: $final_uri"); 3938 $this->setURL($final_uri); 3939 } 3940 phpCAS::traceEnd($this->_url); 3941 return $this->_url; 3942 } 3943 3944 /** 3945 * This method sets the base URL of the CAS server. 3946 * 3947 * @param string $url the base URL 3948 * 3949 * @return string base url 3950 */ 3951 public function setBaseURL($url) 3952 { 3953 // Argument Validation 3954 if (gettype($url) != 'string') 3955 throw new CAS_TypeMismatchException($url, '$url', 'string'); 3956 3957 return $this->_server['base_url'] = $url; 3958 } 3959 3960 3961 /** 3962 * Try to figure out the phpCAS client URL with possible Proxys / Ports etc. 3963 * 3964 * @return string Server URL with domain:port 3965 */ 3966 private function _getClientUrl() 3967 { 3968 if (!empty($_SERVER['HTTP_X_FORWARDED_HOST'])) { 3969 // explode the host list separated by comma and use the first host 3970 $hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_HOST']); 3971 // see rfc7239#5.3 and rfc7230#2.7.1: port is in HTTP_X_FORWARDED_HOST if non default 3972 return $hosts[0]; 3973 } else if (!empty($_SERVER['HTTP_X_FORWARDED_SERVER'])) { 3974 $server_url = $_SERVER['HTTP_X_FORWARDED_SERVER']; 3975 } else { 3976 if (empty($_SERVER['SERVER_NAME'])) { 3977 $server_url = $_SERVER['HTTP_HOST']; 3978 } else { 3979 $server_url = $_SERVER['SERVER_NAME']; 3980 } 3981 } 3982 if (!strpos($server_url, ':')) { 3983 if (empty($_SERVER['HTTP_X_FORWARDED_PORT'])) { 3984 $server_port = $_SERVER['SERVER_PORT']; 3985 } else { 3986 $ports = explode(',', $_SERVER['HTTP_X_FORWARDED_PORT']); 3987 $server_port = $ports[0]; 3988 } 3989 3990 if ( ($this->_isHttps() && $server_port!=443) 3991 || (!$this->_isHttps() && $server_port!=80) 3992 ) { 3993 $server_url .= ':'; 3994 $server_url .= $server_port; 3995 } 3996 } 3997 return $server_url; 3998 } 3999 4000 /** 4001 * This method checks to see if the request is secured via HTTPS 4002 * 4003 * @return bool true if https, false otherwise 4004 */ 4005 private function _isHttps() 4006 { 4007 if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) { 4008 return ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'); 4009 } elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTOCOL'])) { 4010 return ($_SERVER['HTTP_X_FORWARDED_PROTOCOL'] === 'https'); 4011 } elseif ( isset($_SERVER['HTTPS']) 4012 && !empty($_SERVER['HTTPS']) 4013 && strcasecmp($_SERVER['HTTPS'], 'off') !== 0 4014 ) { 4015 return true; 4016 } 4017 return false; 4018 4019 } 4020 4021 /** 4022 * Removes a parameter from a query string 4023 * 4024 * @param string $parameterName name of parameter 4025 * @param string $queryString query string 4026 * 4027 * @return string new query string 4028 * 4029 * @link http://stackoverflow.com/questions/1842681/regular-expression-to-remove-one-parameter-from-query-string 4030 */ 4031 private function _removeParameterFromQueryString($parameterName, $queryString) 4032 { 4033 $parameterName = preg_quote($parameterName); 4034 return preg_replace( 4035 "/&$parameterName(=[^&]*)?|^$parameterName(=[^&]*)?&?/", 4036 '', $queryString 4037 ); 4038 } 4039 4040 /** 4041 * This method is used to append query parameters to an url. Since the url 4042 * might already contain parameter it has to be detected and to build a proper 4043 * URL 4044 * 4045 * @param string $url base url to add the query params to 4046 * @param string $query params in query form with & separated 4047 * 4048 * @return string url with query params 4049 */ 4050 private function _buildQueryUrl($url, $query) 4051 { 4052 $url .= (strstr($url, '?') === false) ? '?' : '&'; 4053 $url .= $query; 4054 return $url; 4055 } 4056 4057 /** 4058 * This method tests if a string starts with a given character. 4059 * 4060 * @param string $text text to test 4061 * @param string $char character to test for 4062 * 4063 * @return bool true if the $text starts with $char 4064 */ 4065 private function _startsWith($text, $char) 4066 { 4067 return (strpos($text, $char) === 0); 4068 } 4069 4070 /** 4071 * This method tests if a string ends with a given character 4072 * 4073 * @param string $text text to test 4074 * @param string $char character to test for 4075 * 4076 * @return bool true if the $text ends with $char 4077 */ 4078 private function _endsWith($text, $char) 4079 { 4080 return (strpos(strrev($text), $char) === 0); 4081 } 4082 4083 /** 4084 * Answer a valid session-id given a CAS ticket. 4085 * 4086 * The output must be deterministic to allow single-log-out when presented with 4087 * the ticket to log-out. 4088 * 4089 * 4090 * @param string $ticket name of the ticket 4091 * 4092 * @return string 4093 */ 4094 private function _sessionIdForTicket($ticket) 4095 { 4096 // Hash the ticket to ensure that the value meets the PHP 7.1 requirement 4097 // that session-ids have a length between 22 and 256 characters. 4098 return hash('sha256', $this->_sessionIdSalt . $ticket); 4099 } 4100 4101 /** 4102 * Set a salt/seed for the session-id hash to make it harder to guess. 4103 * 4104 * @var string $_sessionIdSalt 4105 */ 4106 private $_sessionIdSalt = ''; 4107 4108 /** 4109 * Set a salt/seed for the session-id hash to make it harder to guess. 4110 * 4111 * @param string $salt 4112 * 4113 * @return void 4114 */ 4115 public function setSessionIdSalt($salt) { 4116 $this->_sessionIdSalt = (string)$salt; 4117 } 4118 4119 // ######################################################################## 4120 // AUTHENTICATION ERROR HANDLING 4121 // ######################################################################## 4122 /** 4123 * This method is used to print the HTML output when the user was not 4124 * authenticated. 4125 * 4126 * @param string $failure the failure that occured 4127 * @param string $cas_url the URL the CAS server was asked for 4128 * @param bool $no_response the response from the CAS server (other 4129 * parameters are ignored if true) 4130 * @param bool $bad_response bad response from the CAS server ($err_code 4131 * and $err_msg ignored if true) 4132 * @param string $cas_response the response of the CAS server 4133 * @param int $err_code the error code given by the CAS server 4134 * @param string $err_msg the error message given by the CAS server 4135 * 4136 * @return void 4137 */ 4138 private function _authError( 4139 $failure, 4140 $cas_url, 4141 $no_response=false, 4142 $bad_response=false, 4143 $cas_response='', 4144 $err_code=-1, 4145 $err_msg='' 4146 ) { 4147 phpCAS::traceBegin(); 4148 $lang = $this->getLangObj(); 4149 $this->printHTMLHeader($lang->getAuthenticationFailed()); 4150 printf( 4151 $lang->getYouWereNotAuthenticated(), htmlentities($this->getURL()), 4152 isset($_SERVER['SERVER_ADMIN']) ? $_SERVER['SERVER_ADMIN']:'' 4153 ); 4154 phpCAS::trace('CAS URL: '.$cas_url); 4155 phpCAS::trace('Authentication failure: '.$failure); 4156 if ( $no_response ) { 4157 phpCAS::trace('Reason: no response from the CAS server'); 4158 } else { 4159 if ( $bad_response ) { 4160 phpCAS::trace('Reason: bad response from the CAS server'); 4161 } else { 4162 switch ($this->getServerVersion()) { 4163 case CAS_VERSION_1_0: 4164 phpCAS::trace('Reason: CAS error'); 4165 break; 4166 case CAS_VERSION_2_0: 4167 case CAS_VERSION_3_0: 4168 if ( $err_code === -1 ) { 4169 phpCAS::trace('Reason: no CAS error'); 4170 } else { 4171 phpCAS::trace( 4172 'Reason: ['.$err_code.'] CAS error: '.$err_msg 4173 ); 4174 } 4175 break; 4176 } 4177 } 4178 phpCAS::trace('CAS response: '.$cas_response); 4179 } 4180 $this->printHTMLFooter(); 4181 phpCAS::traceExit(); 4182 throw new CAS_GracefullTerminationException(); 4183 } 4184 4185 // ######################################################################## 4186 // PGTIOU/PGTID and logoutRequest rebroadcasting 4187 // ######################################################################## 4188 4189 /** 4190 * Boolean of whether to rebroadcast pgtIou/pgtId and logoutRequest, and 4191 * array of the nodes. 4192 */ 4193 private $_rebroadcast = false; 4194 private $_rebroadcast_nodes = array(); 4195 4196 /** 4197 * Constants used for determining rebroadcast node type. 4198 */ 4199 const HOSTNAME = 0; 4200 const IP = 1; 4201 4202 /** 4203 * Determine the node type from the URL. 4204 * 4205 * @param String $nodeURL The node URL. 4206 * 4207 * @return int hostname 4208 * 4209 */ 4210 private function _getNodeType($nodeURL) 4211 { 4212 phpCAS::traceBegin(); 4213 if (preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $nodeURL)) { 4214 phpCAS::traceEnd(self::IP); 4215 return self::IP; 4216 } else { 4217 phpCAS::traceEnd(self::HOSTNAME); 4218 return self::HOSTNAME; 4219 } 4220 } 4221 4222 /** 4223 * Store the rebroadcast node for pgtIou/pgtId and logout requests. 4224 * 4225 * @param string $rebroadcastNodeUrl The rebroadcast node URL. 4226 * 4227 * @return void 4228 */ 4229 public function addRebroadcastNode($rebroadcastNodeUrl) 4230 { 4231 // Argument validation 4232 if ( !(bool)preg_match("/^(http|https):\/\/([A-Z0-9][A-Z0-9_-]*(?:\.[A-Z0-9][A-Z0-9_-]*)+):?(\d+)?\/?/i", $rebroadcastNodeUrl)) 4233 throw new CAS_TypeMismatchException($rebroadcastNodeUrl, '$rebroadcastNodeUrl', 'url'); 4234 4235 // Store the rebroadcast node and set flag 4236 $this->_rebroadcast = true; 4237 $this->_rebroadcast_nodes[] = $rebroadcastNodeUrl; 4238 } 4239 4240 /** 4241 * An array to store extra rebroadcast curl options. 4242 */ 4243 private $_rebroadcast_headers = array(); 4244 4245 /** 4246 * This method is used to add header parameters when rebroadcasting 4247 * pgtIou/pgtId or logoutRequest. 4248 * 4249 * @param string $header Header to send when rebroadcasting. 4250 * 4251 * @return void 4252 */ 4253 public function addRebroadcastHeader($header) 4254 { 4255 if (gettype($header) != 'string') 4256 throw new CAS_TypeMismatchException($header, '$header', 'string'); 4257 4258 $this->_rebroadcast_headers[] = $header; 4259 } 4260 4261 /** 4262 * Constants used for determining rebroadcast type (logout or pgtIou/pgtId). 4263 */ 4264 const LOGOUT = 0; 4265 const PGTIOU = 1; 4266 4267 /** 4268 * This method rebroadcasts logout/pgtIou requests. Can be LOGOUT,PGTIOU 4269 * 4270 * @param int $type type of rebroadcasting. 4271 * 4272 * @return void 4273 */ 4274 private function _rebroadcast($type) 4275 { 4276 phpCAS::traceBegin(); 4277 4278 $rebroadcast_curl_options = array( 4279 CURLOPT_FAILONERROR => 1, 4280 CURLOPT_FOLLOWLOCATION => 1, 4281 CURLOPT_RETURNTRANSFER => 1, 4282 CURLOPT_CONNECTTIMEOUT => 1, 4283 CURLOPT_TIMEOUT => 4); 4284 4285 // Try to determine the IP address of the server 4286 if (!empty($_SERVER['SERVER_ADDR'])) { 4287 $ip = $_SERVER['SERVER_ADDR']; 4288 } else if (!empty($_SERVER['LOCAL_ADDR'])) { 4289 // IIS 7 4290 $ip = $_SERVER['LOCAL_ADDR']; 4291 } 4292 // Try to determine the DNS name of the server 4293 if (!empty($ip)) { 4294 $dns = gethostbyaddr($ip); 4295 } 4296 $multiClassName = 'CAS_Request_CurlMultiRequest'; 4297 $multiRequest = new $multiClassName(); 4298 4299 for ($i = 0; $i < sizeof($this->_rebroadcast_nodes); $i++) { 4300 if ((($this->_getNodeType($this->_rebroadcast_nodes[$i]) == self::HOSTNAME) && !empty($dns) && (stripos($this->_rebroadcast_nodes[$i], $dns) === false)) 4301 || (($this->_getNodeType($this->_rebroadcast_nodes[$i]) == self::IP) && !empty($ip) && (stripos($this->_rebroadcast_nodes[$i], $ip) === false)) 4302 ) { 4303 phpCAS::trace( 4304 'Rebroadcast target URL: '.$this->_rebroadcast_nodes[$i] 4305 .$_SERVER['REQUEST_URI'] 4306 ); 4307 $className = $this->_requestImplementation; 4308 $request = new $className(); 4309 4310 $url = $this->_rebroadcast_nodes[$i].$_SERVER['REQUEST_URI']; 4311 $request->setUrl($url); 4312 4313 if (count($this->_rebroadcast_headers)) { 4314 $request->addHeaders($this->_rebroadcast_headers); 4315 } 4316 4317 $request->makePost(); 4318 if ($type == self::LOGOUT) { 4319 // Logout request 4320 $request->setPostBody( 4321 'rebroadcast=false&logoutRequest='.$_POST['logoutRequest'] 4322 ); 4323 } else if ($type == self::PGTIOU) { 4324 // pgtIou/pgtId rebroadcast 4325 $request->setPostBody('rebroadcast=false'); 4326 } 4327 4328 $request->setCurlOptions($rebroadcast_curl_options); 4329 4330 $multiRequest->addRequest($request); 4331 } else { 4332 phpCAS::trace( 4333 'Rebroadcast not sent to self: ' 4334 .$this->_rebroadcast_nodes[$i].' == '.(!empty($ip)?$ip:'') 4335 .'/'.(!empty($dns)?$dns:'') 4336 ); 4337 } 4338 } 4339 // We need at least 1 request 4340 if ($multiRequest->getNumRequests() > 0) { 4341 $multiRequest->send(); 4342 } 4343 phpCAS::traceEnd(); 4344 } 4345 4346 /** @} */ 4347} 4348