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