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