PageRenderTime 38ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/auth/cas/CAS/CAS/Client.php

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