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