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