login.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. /**
  4. * OpenID login method
  5. *
  6. * The OpenID login method relies on authentication servers providing a public
  7. * URL that can confirm the identity of a person, thus avoiding the spread
  8. * use of password transmissions over non-secure lines (for Dokeos, it is a
  9. * good way of avoiding password theft)
  10. * @package chamilo.auth.openid
  11. */
  12. /**
  13. * Initialisation
  14. */
  15. require_once api_get_path(CONFIGURATION_PATH) . 'auth.conf.php';
  16. require_once 'openid.lib.php';
  17. require_once 'xrds.lib.php';
  18. function openid_form() {
  19. //get_lang('OpenIdAuthentication')
  20. $form = new FormValidator('openid_login', 'post', null, null, array('class' => 'form-vertical form_login'));
  21. $form -> addElement('text', 'openid_url', array(get_lang('OpenIDURL'), Display::url(get_lang('OpenIDWhatIs'), 'main/auth/openid/whatis.php')), array('class' => 'openid_input'));
  22. $form -> addElement('button', 'submit', get_lang('Login'));
  23. return $form->return_form();
  24. /*
  25. return '<label for="openid_url">'.get_lang('OpenIDURL').' <a href="main/auth/openid/whatis.php" title="'.get_lang('OpenIDWhatIs').'">'.Display::return_icon('info3.gif',get_lang('Info')).'</a></label>
  26. <input type="text" id="openid_url" name="openid_url" style="background: url(main/img/openid_small_logo.png) no-repeat; background-color: #fff; background-position: 0 50%; padding-left:18px;" value="http://"></input>
  27. * <input type="submit" name="openid_login" value="'.get_lang('Enter').'" /><br /><br /></form></div>';
  28. *
  29. */
  30. }
  31. /**
  32. * The initial step of OpenID authentication responsible for the following:
  33. * - Perform discovery on the claimed OpenID.
  34. * - If possible, create an association with the Provider's endpoint.
  35. * - Create the authentication request.
  36. * - Perform the appropriate redirect.
  37. *
  38. * @param $claimed_id The OpenID to authenticate
  39. * @param $return_to The endpoint to return to from the OpenID Provider
  40. */
  41. function openid_begin($claimed_id, $return_to = '', $form_values = array()) {
  42. $claimed_id = _openid_normalize($claimed_id);
  43. $services = openid_discovery($claimed_id);
  44. if (count($services) == 0) {
  45. echo 'Sorry, that is not a valid OpenID. Please ensure you have spelled your ID correctly.';
  46. return;
  47. }
  48. $op_endpoint = $services[0]['uri'];
  49. // Store the discovered endpoint in the session (so we don't have to rediscover).
  50. $_SESSION['openid_op_endpoint'] = $op_endpoint;
  51. // Store the claimed_id in the session (for handling delegation).
  52. $_SESSION['openid_claimed_id'] = $claimed_id;
  53. // Store the login form values so we can pass them to
  54. // user_exteral_login later.
  55. $_SESSION['openid_user_login_values'] = $form_values;
  56. // If bcmath is present, then create an association
  57. $assoc_handle = '';
  58. if (function_exists('bcadd')) {
  59. $assoc_handle = openid_association($op_endpoint);
  60. }
  61. // Now that there is an association created, move on
  62. // to request authentication from the IdP
  63. $identity = (!empty($services[0]['delegate'])) ? $services[0]['delegate'] : $claimed_id;
  64. if (isset($services[0]['types']) && is_array($services[0]['types']) && in_array(OPENID_NS_2_0 . '/server', $services[0]['types'])) {
  65. $identity = 'http://openid.net/identifier_select/2.0';
  66. }
  67. $authn_request = openid_authentication_request($claimed_id, $identity, $return_to, $assoc_handle, $services[0]['version']);
  68. if ($services[0]['version'] == 2) {
  69. echo openid_redirect($op_endpoint, $authn_request);
  70. } else {
  71. echo openid_redirect_http($op_endpoint, $authn_request);
  72. }
  73. }
  74. /**
  75. * Completes OpenID authentication by validating returned data from the OpenID
  76. * Provider.
  77. *
  78. * @param $response Array of returned from the OpenID provider (typically $_REQUEST).
  79. *
  80. * @return $response Response values for further processing with
  81. * $response['status'] set to one of 'success', 'failed' or 'cancel'.
  82. */
  83. function openid_complete($response) {
  84. // Default to failed response
  85. $response['status'] = 'failed';
  86. if (isset($_SESSION['openid_op_endpoint']) && isset($_SESSION['openid_claimed_id'])) {
  87. _openid_fix_post($response);
  88. $op_endpoint = $_SESSION['openid_op_endpoint'];
  89. $claimed_id = $_SESSION['openid_claimed_id'];
  90. unset($_SESSION['openid_op_endpoint']);
  91. unset($_SESSION['openid_claimed_id']);
  92. if (isset($response['openid.mode'])) {
  93. if ($response['openid.mode'] == 'cancel') {
  94. $response['status'] = 'cancel';
  95. } else {
  96. if (openid_verify_assertion($op_endpoint, $response)) {
  97. $response['openid.identity'] = $claimed_id;
  98. $response['status'] = 'success';
  99. }
  100. }
  101. }
  102. }
  103. return $response;
  104. }
  105. /**
  106. * Perform discovery on a claimed ID to determine the OpenID provider endpoint.
  107. *
  108. * @param $claimed_id The OpenID URL to perform discovery on.
  109. *
  110. * @return Array of services discovered (including OpenID version, endpoint
  111. * URI, etc).
  112. */
  113. function openid_discovery($claimed_id) {
  114. $services = array();
  115. $xrds_url = $claimed_id;
  116. if (_openid_is_xri($claimed_id)) {
  117. $xrds_url = 'http://xri.net/' . $claimed_id;
  118. }
  119. $url = @parse_url($xrds_url);
  120. if ($url['scheme'] == 'http' || $url['scheme'] == 'https') {
  121. // For regular URLs, try Yadis resolution first, then HTML-based discovery
  122. $headers = array('Accept' => 'application/xrds+xml');
  123. //TODO
  124. $result = openid_http_request($xrds_url, $headers);
  125. if (!isset($result->error)) {
  126. if (isset($result->headers['Content-Type']) && preg_match("/application\/xrds\+xml/", $result->headers['Content-Type'])) {
  127. // Parse XML document to find URL
  128. $services = xrds_parse($result->data);
  129. } else {
  130. $xrds_url = NULL;
  131. if (isset($result->headers['X-XRDS-Location'])) {
  132. $xrds_url = $result->headers['X-XRDS-Location'];
  133. } else {
  134. // Look for meta http-equiv link in HTML head
  135. $xrds_url = _openid_meta_httpequiv('X-XRDS-Location', $result->data);
  136. }
  137. if (!empty($xrds_url)) {
  138. $headers = array('Accept' => 'application/xrds+xml');
  139. //TODO
  140. $xrds_result = openid_http_request($xrds_url, $headers);
  141. if (!isset($xrds_result->error)) {
  142. $services = xrds_parse($xrds_result->data);
  143. }
  144. }
  145. }
  146. // Check for HTML delegation
  147. if (count($services) == 0) {
  148. // Look for 2.0 links
  149. $uri = _openid_link_href('openid2.provider', $result->data);
  150. $delegate = _openid_link_href('openid2.local_id', $result->data);
  151. $version = 2;
  152. // 1.0 links
  153. if (empty($uri)) {
  154. $uri = _openid_link_href('openid.server', $result->data);
  155. $delegate = _openid_link_href('openid.delegate', $result->data);
  156. $version = 1;
  157. }
  158. if (!empty($uri)) {
  159. $services[] = array('uri' => $uri, 'delegate' => $delegate, 'version' => $version);
  160. }
  161. }
  162. }
  163. }
  164. return $services;
  165. }
  166. /**
  167. * Attempt to create a shared secret with the OpenID Provider.
  168. *
  169. * @param $op_endpoint URL of the OpenID Provider endpoint.
  170. *
  171. * @return $assoc_handle The association handle.
  172. */
  173. function openid_association($op_endpoint) {
  174. //@todo Remove Old Associations:
  175. $openid_association = Database::get_main_table(TABLE_MAIN_OPENID_ASSOCIATION);
  176. $sql = "DELETE FROM $openid_association WHERE created + expires_in < '" . api_get_utc_datetime() . "'";
  177. Database::query($sql);
  178. // Check to see if we have an association for this IdP already
  179. $op_endpoint = Database::escape_string($op_endpoint);
  180. $sql = "SELECT assoc_handle FROM $openid_association WHERE idp_endpoint_uri = '$op_endpoint'";
  181. $assoc_handle = Database::query($sql);
  182. if (Database::num_rows($assoc_handle) <= 1) {
  183. $mod = OPENID_DH_DEFAULT_MOD;
  184. $gen = OPENID_DH_DEFAULT_GEN;
  185. $r = _openid_dh_rand($mod);
  186. $private = bcadd($r, 1);
  187. $public = bcpowmod($gen, $private, $mod);
  188. // If there is no existing association, then request one
  189. $assoc_request = openid_association_request($public);
  190. $assoc_message = _openid_encode_message(_openid_create_message($assoc_request));
  191. $assoc_headers = array('Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8');
  192. //TODO
  193. $assoc_result = openid_http_request($op_endpoint, $assoc_headers, 'POST', $assoc_message);
  194. if (isset($assoc_result->error)) {
  195. return FALSE;
  196. }
  197. $assoc_response = _openid_parse_message($assoc_result->data);
  198. if (isset($assoc_response['mode']) && $assoc_response['mode'] == 'error') {
  199. return FALSE;
  200. }
  201. if ($assoc_response['session_type'] == 'DH-SHA1') {
  202. $spub = _openid_dh_base64_to_long($assoc_response['dh_server_public']);
  203. $enc_mac_key = base64_decode($assoc_response['enc_mac_key']);
  204. $shared = bcpowmod($spub, $private, $mod);
  205. $assoc_response['mac_key'] = base64_encode(_openid_dh_xorsecret($shared, $enc_mac_key));
  206. }
  207. //TODO
  208. $openid_association = Database::get_main_table(TABLE_MAIN_OPENID_ASSOCIATION);
  209. Database::query(sprintf("INSERT INTO $openid_association (idp_endpoint_uri, session_type, assoc_handle, assoc_type, expires_in, mac_key, created) VALUES('%s', '%s', '%s', '%s', %d, '%s', %d)", $op_endpoint, $assoc_response['session_type'], $assoc_response['assoc_handle'], $assoc_response['assoc_type'], $assoc_response['expires_in'], $assoc_response['mac_key'], api_get_utc_datetime()));
  210. $assoc_handle = $assoc_response['assoc_handle'];
  211. }
  212. return $assoc_handle;
  213. }
  214. /**
  215. * ?
  216. */
  217. function openid_association_request($public) {
  218. $request = array(
  219. 'openid.ns' => OPENID_NS_2_0,
  220. 'openid.mode' => 'associate',
  221. 'openid.session_type' => 'DH-SHA1',
  222. 'openid.assoc_type' => 'HMAC-SHA1'
  223. );
  224. if ($request['openid.session_type'] == 'DH-SHA1' || $request['openid.session_type'] == 'DH-SHA256') {
  225. $cpub = _openid_dh_long_to_base64($public);
  226. $request['openid.dh_consumer_public'] = $cpub;
  227. }
  228. return $request;
  229. }
  230. /**
  231. *
  232. */
  233. function openid_authentication_request($claimed_id, $identity, $return_to = '', $assoc_handle = '', $version = 2) {
  234. $realm = ($return_to) ? $return_to : api_get_self();
  235. $ns = ($version == 2) ? OPENID_NS_2_0 : OPENID_NS_1_0;
  236. $request = array(
  237. 'openid.ns' => $ns,
  238. 'openid.mode' => 'checkid_setup',
  239. 'openid.identity' => $identity,
  240. 'openid.claimed_id' => $claimed_id,
  241. 'openid.assoc_handle' => $assoc_handle,
  242. 'openid.return_to' => $return_to,
  243. );
  244. if ($version == 2) {
  245. $request['openid.realm'] = $realm;
  246. } else {
  247. $request['openid.trust_root'] = $realm;
  248. }
  249. // Simple Registration - we don't ask lastname and firstname because the only
  250. // available similar data is "fullname" and we would have to guess where to split
  251. $request['openid.sreg.required'] = 'nickname,email';
  252. $request['openid.ns.sreg'] = "http://openid.net/extensions/sreg/1.1";
  253. //$request = array_merge($request, module_invoke_all('openid', 'request', $request));
  254. //$request = array_merge($request);
  255. return $request;
  256. }
  257. /**
  258. * Attempt to verify the response received from the OpenID Provider.
  259. *
  260. * @param $op_endpoint The OpenID Provider URL.
  261. * @param $response Array of repsonse values from the provider.
  262. *
  263. * @return boolean
  264. */
  265. function openid_verify_assertion($op_endpoint, $response) {
  266. $valid = FALSE;
  267. //TODO
  268. $openid_association = Database::get_main_table(TABLE_MAIN_OPENID_ASSOCIATION);
  269. $sql = sprintf("SELECT * FROM $openid_association WHERE assoc_handle = '%s'", $response['openid.assoc_handle']);
  270. $res = Database::query($sql);
  271. $association = Database::fetch_object($res);
  272. if ($association && isset($association->session_type)) {
  273. $keys_to_sign = explode(',', $response['openid.signed']);
  274. $self_sig = _openid_signature($association, $response, $keys_to_sign);
  275. if ($self_sig == $response['openid.sig']) {
  276. $valid = TRUE;
  277. } else {
  278. $valid = FALSE;
  279. }
  280. } else {
  281. $request = $response;
  282. $request['openid.mode'] = 'check_authentication';
  283. $message = _openid_create_message($request);
  284. $headers = array('Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8');
  285. $result = openid_http_request($op_endpoint, $headers, 'POST', _openid_encode_message($message));
  286. if (!isset($result->error)) {
  287. $response = _openid_parse_message($result->data);
  288. if (strtolower(trim($response['is_valid'])) == 'true') {
  289. $valid = TRUE;
  290. } else {
  291. $valid = FALSE;
  292. }
  293. }
  294. }
  295. return $valid;
  296. }
  297. /**
  298. * Make a HTTP request - This function has been copied straight over from Drupal 6 code (drupal_http_request)
  299. */
  300. function openid_http_request($url, $headers = array(), $method = 'GET', $data = NULL, $retry = 3) {
  301. $result = new stdClass();
  302. // Parse the URL and make sure we can handle the schema.
  303. $uri = parse_url($url);
  304. switch ($uri['scheme']) {
  305. case 'http':
  306. $port = isset($uri['port']) ? $uri['port'] : 80;
  307. $host = $uri['host'] . ($port != 80 ? ':' . $port : '');
  308. $fp = @fsockopen($uri['host'], $port, $errno, $errstr, 15);
  309. break;
  310. case 'https':
  311. // Note: Only works for PHP 4.3 compiled with OpenSSL.
  312. $port = isset($uri['port']) ? $uri['port'] : 443;
  313. $host = $uri['host'] . ($port != 443 ? ':' . $port : '');
  314. $fp = @fsockopen('ssl://' . $uri['host'], $port, $errno, $errstr, 20);
  315. break;
  316. default:
  317. $result->error = 'invalid schema ' . $uri['scheme'];
  318. return $result;
  319. }
  320. // Make sure the socket opened properly.
  321. if (!$fp) {
  322. // When a network error occurs, we make sure that it is a negative number so
  323. // it can clash with the HTTP status codes.
  324. $result->code = -$errno;
  325. $result->error = trim($errstr);
  326. return $result;
  327. }
  328. // Construct the path to act on.
  329. $path = isset($uri['path']) ? $uri['path'] : '/';
  330. if (isset($uri['query'])) {
  331. $path .= '?' . $uri['query'];
  332. }
  333. // Create HTTP request.
  334. $defaults = array(
  335. // RFC 2616: "non-standard ports MUST, default ports MAY be included".
  336. // We don't add the port to prevent from breaking rewrite rules checking the
  337. // host that do not take into account the port number.
  338. 'Host' => "Host: $host",
  339. 'User-Agent' => 'User-Agent: Chamilo (+http://www.chamilo.org/)',
  340. 'Content-Length' => 'Content-Length: ' . strlen($data)
  341. );
  342. // If the server url has a user then attempt to use basic authentication
  343. if (isset($uri['user'])) {
  344. $defaults['Authorization'] = 'Authorization: Basic ' . base64_encode($uri['user'] . (!empty($uri['pass']) ? ":" . $uri['pass'] : ''));
  345. }
  346. foreach ($headers as $header => $value) {
  347. $defaults[$header] = $header . ': ' . $value;
  348. }
  349. $request = $method . ' ' . $path . " HTTP/1.0\r\n";
  350. $request .= implode("\r\n", $defaults);
  351. $request .= "\r\n\r\n";
  352. if ($data) {
  353. $request .= $data . "\r\n";
  354. }
  355. $result->request = $request;
  356. fwrite($fp, $request);
  357. // Fetch response.
  358. $response = '';
  359. while (!feof($fp) && $chunk = fread($fp, 1024)) {
  360. $response .= $chunk;
  361. }
  362. fclose($fp);
  363. // Parse response.
  364. list($split, $result->data) = explode("\r\n\r\n", $response, 2);
  365. $split = preg_split("/\r\n|\n|\r/", $split);
  366. list($protocol, $code, $text) = explode(' ', trim(array_shift($split)), 3);
  367. $result->headers = array();
  368. // Parse headers.
  369. while ($line = trim(array_shift($split))) {
  370. list($header, $value) = explode(':', $line, 2);
  371. if (isset($result->headers[$header]) && $header == 'Set-Cookie') {
  372. // RFC 2109: the Set-Cookie response header comprises the token Set-
  373. // Cookie:, followed by a comma-separated list of one or more cookies.
  374. $result->headers[$header] .= ',' . trim($value);
  375. } else {
  376. $result->headers[$header] = trim($value);
  377. }
  378. }
  379. $responses = array(
  380. 100 => 'Continue', 101 => 'Switching Protocols',
  381. 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content',
  382. 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect',
  383. 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed',
  384. 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported'
  385. );
  386. // RFC 2616 states that all unknown HTTP codes must be treated the same as the
  387. // base code in their class.
  388. if (!isset($responses[$code])) {
  389. $code = floor($code / 100) * 100;
  390. }
  391. switch ($code) {
  392. case 200: // OK
  393. case 304: // Not modified
  394. break;
  395. case 301: // Moved permanently
  396. case 302: // Moved temporarily
  397. case 307: // Moved temporarily
  398. $location = $result->headers['Location'];
  399. if ($retry) {
  400. $result = openid_http_request($result->headers['Location'], $headers, $method, $data, --$retry);
  401. $result->redirect_code = $result->code;
  402. }
  403. $result->redirect_url = $location;
  404. break;
  405. default:
  406. $result->error = $text;
  407. }
  408. $result->code = $code;
  409. return $result;
  410. }