openid.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  1. <?php
  2. /**
  3. * This class provides a simple interface for OpenID (1.1 and 2.0) authentication.
  4. * Supports Yadis discovery.
  5. * The authentication process is stateless/dumb.
  6. *
  7. * Usage:
  8. * Sign-on with OpenID is a two step process:
  9. * Step one is authentication with the provider:
  10. * <code>
  11. * $openid = new LightOpenID('my-host.example.org');
  12. * $openid->identity = 'ID supplied by user';
  13. * header('Location: ' . $openid->authUrl());
  14. * </code>
  15. * The provider then sends various parameters via GET, one of them is openid_mode.
  16. * Step two is verification:
  17. * <code>
  18. * $openid = new LightOpenID('my-host.example.org');
  19. * if ($openid->mode) {
  20. * echo $openid->validate() ? 'Logged in.' : 'Failed';
  21. * }
  22. * </code>
  23. *
  24. * Change the 'my-host.example.org' to your domain name. Do NOT use $_SERVER['HTTP_HOST']
  25. * for that, unless you know what you are doing.
  26. *
  27. * Optionally, you can set $returnUrl and $realm (or $trustRoot, which is an alias).
  28. * The default values for those are:
  29. * $openid->realm = (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'];
  30. * $openid->returnUrl = $openid->realm . $_SERVER['REQUEST_URI'];
  31. * If you don't know their meaning, refer to any openid tutorial, or specification. Or just guess.
  32. *
  33. * AX and SREG extensions are supported.
  34. * To use them, specify $openid->required and/or $openid->optional before calling $openid->authUrl().
  35. * These are arrays, with values being AX schema paths (the 'path' part of the URL).
  36. * For example:
  37. * $openid->required = array('namePerson/friendly', 'contact/email');
  38. * $openid->optional = array('namePerson/first');
  39. * If the server supports only SREG or OpenID 1.1, these are automaticaly
  40. * mapped to SREG names, so that user doesn't have to know anything about the server.
  41. *
  42. * To get the values, use $openid->getAttributes().
  43. *
  44. *
  45. * The library requires PHP >= 5.1.2 with curl or http/https stream wrappers enabled.
  46. * @author Mewp
  47. * @copyright Copyright (c) 2010, Mewp
  48. * @license http://www.opensource.org/licenses/mit-license.php MIT
  49. */
  50. class LightOpenID
  51. {
  52. public $returnUrl
  53. , $required = array()
  54. , $optional = array()
  55. , $verify_peer = null
  56. , $capath = null
  57. , $cainfo = null
  58. , $data;
  59. private $identity, $claimed_id;
  60. protected $server, $version, $trustRoot, $aliases, $identifier_select = false
  61. , $ax = false, $sreg = false, $setup_url = null, $headers = array();
  62. static protected $ax_to_sreg = array(
  63. 'namePerson/friendly' => 'nickname',
  64. 'contact/email' => 'email',
  65. 'namePerson' => 'fullname',
  66. 'birthDate' => 'dob',
  67. 'person/gender' => 'gender',
  68. 'contact/postalCode/home' => 'postcode',
  69. 'contact/country/home' => 'country',
  70. 'pref/language' => 'language',
  71. 'pref/timezone' => 'timezone',
  72. );
  73. function __construct($host)
  74. {
  75. $this->trustRoot = (strpos($host, '://') ? $host : 'http://' . $host);
  76. if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')
  77. || (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
  78. && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')
  79. ) {
  80. $this->trustRoot = (strpos($host, '://') ? $host : 'https://' . $host);
  81. }
  82. if(($host_end = strpos($this->trustRoot, '/', 8)) !== false) {
  83. $this->trustRoot = substr($this->trustRoot, 0, $host_end);
  84. }
  85. $uri = rtrim(preg_replace('#((?<=\?)|&)openid\.[^&]+#', '', $_SERVER['REQUEST_URI']), '?');
  86. $this->returnUrl = $this->trustRoot . $uri;
  87. $this->data = ($_SERVER['REQUEST_METHOD'] === 'POST') ? $_POST : $_GET;
  88. if(!function_exists('curl_init') && !in_array('https', stream_get_wrappers())) {
  89. throw new ErrorException('You must have either https wrappers or curl enabled.');
  90. }
  91. }
  92. function __set($name, $value)
  93. {
  94. switch ($name) {
  95. case 'identity':
  96. if (strlen($value = trim((String) $value))) {
  97. if (preg_match('#^xri:/*#i', $value, $m)) {
  98. $value = substr($value, strlen($m[0]));
  99. } elseif (!preg_match('/^(?:[=@+\$!\(]|https?:)/i', $value)) {
  100. $value = "http://$value";
  101. }
  102. if (preg_match('#^https?://[^/]+$#i', $value, $m)) {
  103. $value .= '/';
  104. }
  105. }
  106. $this->$name = $this->claimed_id = $value;
  107. break;
  108. case 'trustRoot':
  109. case 'realm':
  110. $this->trustRoot = trim($value);
  111. }
  112. }
  113. function __get($name)
  114. {
  115. switch ($name) {
  116. case 'identity':
  117. # We return claimed_id instead of identity,
  118. # because the developer should see the claimed identifier,
  119. # i.e. what he set as identity, not the op-local identifier (which is what we verify)
  120. return $this->claimed_id;
  121. case 'trustRoot':
  122. case 'realm':
  123. return $this->trustRoot;
  124. case 'mode':
  125. return empty($this->data['openid_mode']) ? null : $this->data['openid_mode'];
  126. }
  127. }
  128. /**
  129. * Checks if the server specified in the url exists.
  130. *
  131. * @param $url url to check
  132. * @return true, if the server exists; false otherwise
  133. */
  134. function hostExists($url)
  135. {
  136. if (strpos($url, '/') === false) {
  137. $server = $url;
  138. } else {
  139. $server = @parse_url($url, PHP_URL_HOST);
  140. }
  141. if (!$server) {
  142. return false;
  143. }
  144. return !!gethostbynamel($server);
  145. }
  146. protected function request_curl($url, $method='GET', $params=array(), $update_claimed_id)
  147. {
  148. $params = http_build_query($params, '', '&');
  149. $curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : ''));
  150. curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
  151. curl_setopt($curl, CURLOPT_HEADER, false);
  152. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
  153. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  154. curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/xrds+xml, */*'));
  155. if($this->verify_peer !== null) {
  156. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verify_peer);
  157. if($this->capath) {
  158. curl_setopt($curl, CURLOPT_CAPATH, $this->capath);
  159. }
  160. if($this->cainfo) {
  161. curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo);
  162. }
  163. }
  164. if ($method == 'POST') {
  165. curl_setopt($curl, CURLOPT_POST, true);
  166. curl_setopt($curl, CURLOPT_POSTFIELDS, $params);
  167. } elseif ($method == 'HEAD') {
  168. curl_setopt($curl, CURLOPT_HEADER, true);
  169. curl_setopt($curl, CURLOPT_NOBODY, true);
  170. } else {
  171. curl_setopt($curl, CURLOPT_HEADER, true);
  172. curl_setopt($curl, CURLOPT_HTTPGET, true);
  173. }
  174. $response = curl_exec($curl);
  175. if($method == 'HEAD' && curl_getinfo($curl, CURLINFO_HTTP_CODE) == 405) {
  176. curl_setopt($curl, CURLOPT_HTTPGET, true);
  177. $response = curl_exec($curl);
  178. $response = substr($response, 0, strpos($response, "\r\n\r\n"));
  179. }
  180. if($method == 'HEAD' || $method == 'GET') {
  181. $header_response = $response;
  182. # If it's a GET request, we want to only parse the header part.
  183. if($method == 'GET') {
  184. $header_response = substr($response, 0, strpos($response, "\r\n\r\n"));
  185. }
  186. $headers = array();
  187. foreach(explode("\n", $header_response) as $header) {
  188. $pos = strpos($header,':');
  189. if ($pos !== false) {
  190. $name = strtolower(trim(substr($header, 0, $pos)));
  191. $headers[$name] = trim(substr($header, $pos+1));
  192. }
  193. }
  194. if($update_claimed_id) {
  195. # Updating claimed_id in case of redirections.
  196. $effective_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
  197. if($effective_url != $url) {
  198. $this->identity = $this->claimed_id = $effective_url;
  199. }
  200. }
  201. if($method == 'HEAD') {
  202. return $headers;
  203. } else {
  204. $this->headers = $headers;
  205. }
  206. }
  207. if (curl_errno($curl)) {
  208. throw new ErrorException(curl_error($curl), curl_errno($curl));
  209. }
  210. return $response;
  211. }
  212. protected function parse_header_array($array, $update_claimed_id)
  213. {
  214. $headers = array();
  215. foreach($array as $header) {
  216. $pos = strpos($header,':');
  217. if ($pos !== false) {
  218. $name = strtolower(trim(substr($header, 0, $pos)));
  219. $headers[$name] = trim(substr($header, $pos+1));
  220. # Following possible redirections. The point is just to have
  221. # claimed_id change with them, because the redirections
  222. # are followed automatically.
  223. # We ignore redirections with relative paths.
  224. # If any known provider uses them, file a bug report.
  225. if($name == 'location' && $update_claimed_id) {
  226. if(strpos($headers[$name], 'http') === 0) {
  227. $this->identity = $this->claimed_id = $headers[$name];
  228. } elseif($headers[$name][0] == '/') {
  229. $parsed_url = parse_url($this->claimed_id);
  230. $this->identity =
  231. $this->claimed_id = $parsed_url['scheme'] . '://'
  232. . $parsed_url['host']
  233. . $headers[$name];
  234. }
  235. }
  236. }
  237. }
  238. return $headers;
  239. }
  240. protected function request_streams($url, $method='GET', $params=array(), $update_claimed_id)
  241. {
  242. if(!$this->hostExists($url)) {
  243. throw new ErrorException("Could not connect to $url.", 404);
  244. }
  245. $params = http_build_query($params, '', '&');
  246. switch($method) {
  247. case 'GET':
  248. $opts = array(
  249. 'http' => array(
  250. 'method' => 'GET',
  251. 'header' => 'Accept: application/xrds+xml, */*',
  252. 'ignore_errors' => true,
  253. ), 'ssl' => array(
  254. 'CN_match' => parse_url($url, PHP_URL_HOST),
  255. ),
  256. );
  257. $url = $url . ($params ? '?' . $params : '');
  258. break;
  259. case 'POST':
  260. $opts = array(
  261. 'http' => array(
  262. 'method' => 'POST',
  263. 'header' => 'Content-type: application/x-www-form-urlencoded',
  264. 'content' => $params,
  265. 'ignore_errors' => true,
  266. ), 'ssl' => array(
  267. 'CN_match' => parse_url($url, PHP_URL_HOST),
  268. ),
  269. );
  270. break;
  271. case 'HEAD':
  272. # We want to send a HEAD request,
  273. # but since get_headers doesn't accept $context parameter,
  274. # we have to change the defaults.
  275. $default = stream_context_get_options(stream_context_get_default());
  276. stream_context_get_default(
  277. array(
  278. 'http' => array(
  279. 'method' => 'HEAD',
  280. 'header' => 'Accept: application/xrds+xml, */*',
  281. 'ignore_errors' => true,
  282. ), 'ssl' => array(
  283. 'CN_match' => parse_url($url, PHP_URL_HOST),
  284. ),
  285. )
  286. );
  287. $url = $url . ($params ? '?' . $params : '');
  288. $headers = get_headers ($url);
  289. if(!$headers) {
  290. return array();
  291. }
  292. if(intval(substr($headers[0], strlen('HTTP/1.1 '))) == 405) {
  293. # The server doesn't support HEAD, so let's emulate it with
  294. # a GET.
  295. $args = func_get_args();
  296. $args[1] = 'GET';
  297. call_user_func_array(array($this, 'request_streams'), $args);
  298. return $this->headers;
  299. }
  300. $headers = $this->parse_header_array($headers, $update_claimed_id);
  301. # And restore them.
  302. stream_context_get_default($default);
  303. return $headers;
  304. }
  305. if($this->verify_peer) {
  306. $opts['ssl'] += array(
  307. 'verify_peer' => true,
  308. 'capath' => $this->capath,
  309. 'cafile' => $this->cainfo,
  310. );
  311. }
  312. $context = stream_context_create ($opts);
  313. $data = file_get_contents($url, false, $context);
  314. # This is a hack for providers who don't support HEAD requests.
  315. # It just creates the headers array for the last request in $this->headers.
  316. if(isset($http_response_header)) {
  317. $this->headers = $this->parse_header_array($http_response_header, $update_claimed_id);
  318. }
  319. return file_get_contents($url, false, $context);
  320. }
  321. protected function request($url, $method='GET', $params=array(), $update_claimed_id=false)
  322. {
  323. if (function_exists('curl_init')
  324. && (!in_array('https', stream_get_wrappers()) || !ini_get('safe_mode') && !ini_get('open_basedir'))
  325. ) {
  326. return $this->request_curl($url, $method, $params, $update_claimed_id);
  327. }
  328. return $this->request_streams($url, $method, $params, $update_claimed_id);
  329. }
  330. protected function build_url($url, $parts)
  331. {
  332. if (isset($url['query'], $parts['query'])) {
  333. $parts['query'] = $url['query'] . '&' . $parts['query'];
  334. }
  335. $url = $parts + $url;
  336. $url = $url['scheme'] . '://'
  337. . (empty($url['username'])?''
  338. :(empty($url['password'])? "{$url['username']}@"
  339. :"{$url['username']}:{$url['password']}@"))
  340. . $url['host']
  341. . (empty($url['port'])?'':":{$url['port']}")
  342. . (empty($url['path'])?'':$url['path'])
  343. . (empty($url['query'])?'':"?{$url['query']}")
  344. . (empty($url['fragment'])?'':"#{$url['fragment']}");
  345. return $url;
  346. }
  347. /**
  348. * Helper function used to scan for <meta>/<link> tags and extract information
  349. * from them
  350. */
  351. protected function htmlTag($content, $tag, $attrName, $attrValue, $valueName)
  352. {
  353. preg_match_all("#<{$tag}[^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*$valueName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1);
  354. preg_match_all("#<{$tag}[^>]*$valueName=['\"](.+?)['\"][^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*/?>#i", $content, $matches2);
  355. $result = array_merge($matches1[1], $matches2[1]);
  356. return empty($result)?false:$result[0];
  357. }
  358. /**
  359. * Performs Yadis and HTML discovery. Normally not used.
  360. * @param $url Identity URL.
  361. * @return String OP Endpoint (i.e. OpenID provider address).
  362. * @throws ErrorException
  363. */
  364. function discover($url)
  365. {
  366. if (!$url) throw new ErrorException('No identity supplied.');
  367. # Use xri.net proxy to resolve i-name identities
  368. if (!preg_match('#^https?:#', $url)) {
  369. $url = "https://xri.net/$url";
  370. }
  371. # We save the original url in case of Yadis discovery failure.
  372. # It can happen when we'll be lead to an XRDS document
  373. # which does not have any OpenID2 services.
  374. $originalUrl = $url;
  375. # A flag to disable yadis discovery in case of failure in headers.
  376. $yadis = true;
  377. # We'll jump a maximum of 5 times, to avoid endless redirections.
  378. for ($i = 0; $i < 5; $i ++) {
  379. if ($yadis) {
  380. $headers = $this->request($url, 'HEAD', array(), true);
  381. $next = false;
  382. if (isset($headers['x-xrds-location'])) {
  383. $url = $this->build_url(parse_url($url), parse_url(trim($headers['x-xrds-location'])));
  384. $next = true;
  385. }
  386. if (isset($headers['content-type'])
  387. && (strpos($headers['content-type'], 'application/xrds+xml') !== false
  388. || strpos($headers['content-type'], 'text/xml') !== false)
  389. ) {
  390. # Apparently, some providers return XRDS documents as text/html.
  391. # While it is against the spec, allowing this here shouldn't break
  392. # compatibility with anything.
  393. # ---
  394. # Found an XRDS document, now let's find the server, and optionally delegate.
  395. $content = $this->request($url, 'GET');
  396. preg_match_all('#<Service.*?>(.*?)</Service>#s', $content, $m);
  397. foreach($m[1] as $content) {
  398. $content = ' ' . $content; # The space is added, so that strpos doesn't return 0.
  399. # OpenID 2
  400. $ns = preg_quote('http://specs.openid.net/auth/2.0/', '#');
  401. if(preg_match('#<Type>\s*'.$ns.'(server|signon)\s*</Type>#s', $content, $type)) {
  402. if ($type[1] == 'server') $this->identifier_select = true;
  403. preg_match('#<URI.*?>(.*)</URI>#', $content, $server);
  404. preg_match('#<(Local|Canonical)ID>(.*)</\1ID>#', $content, $delegate);
  405. if (empty($server)) {
  406. return false;
  407. }
  408. # Does the server advertise support for either AX or SREG?
  409. $this->ax = (bool) strpos($content, '<Type>http://openid.net/srv/ax/1.0</Type>');
  410. $this->sreg = strpos($content, '<Type>http://openid.net/sreg/1.0</Type>')
  411. || strpos($content, '<Type>http://openid.net/extensions/sreg/1.1</Type>');
  412. $server = $server[1];
  413. if (isset($delegate[2])) $this->identity = trim($delegate[2]);
  414. $this->version = 2;
  415. $this->server = $server;
  416. return $server;
  417. }
  418. # OpenID 1.1
  419. $ns = preg_quote('http://openid.net/signon/1.1', '#');
  420. if (preg_match('#<Type>\s*'.$ns.'\s*</Type>#s', $content)) {
  421. preg_match('#<URI.*?>(.*)</URI>#', $content, $server);
  422. preg_match('#<.*?Delegate>(.*)</.*?Delegate>#', $content, $delegate);
  423. if (empty($server)) {
  424. return false;
  425. }
  426. # AX can be used only with OpenID 2.0, so checking only SREG
  427. $this->sreg = strpos($content, '<Type>http://openid.net/sreg/1.0</Type>')
  428. || strpos($content, '<Type>http://openid.net/extensions/sreg/1.1</Type>');
  429. $server = $server[1];
  430. if (isset($delegate[1])) $this->identity = $delegate[1];
  431. $this->version = 1;
  432. $this->server = $server;
  433. return $server;
  434. }
  435. }
  436. $next = true;
  437. $yadis = false;
  438. $url = $originalUrl;
  439. $content = null;
  440. break;
  441. }
  442. if ($next) continue;
  443. # There are no relevant information in headers, so we search the body.
  444. $content = $this->request($url, 'GET', array(), true);
  445. if (isset($this->headers['x-xrds-location'])) {
  446. $url = $this->build_url(parse_url($url), parse_url(trim($this->headers['x-xrds-location'])));
  447. continue;
  448. }
  449. $location = $this->htmlTag($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'content');
  450. if ($location) {
  451. $url = $this->build_url(parse_url($url), parse_url($location));
  452. continue;
  453. }
  454. }
  455. if (!$content) $content = $this->request($url, 'GET');
  456. # At this point, the YADIS Discovery has failed, so we'll switch
  457. # to openid2 HTML discovery, then fallback to openid 1.1 discovery.
  458. $server = $this->htmlTag($content, 'link', 'rel', 'openid2.provider', 'href');
  459. $delegate = $this->htmlTag($content, 'link', 'rel', 'openid2.local_id', 'href');
  460. $this->version = 2;
  461. if (!$server) {
  462. # The same with openid 1.1
  463. $server = $this->htmlTag($content, 'link', 'rel', 'openid.server', 'href');
  464. $delegate = $this->htmlTag($content, 'link', 'rel', 'openid.delegate', 'href');
  465. $this->version = 1;
  466. }
  467. if ($server) {
  468. # We found an OpenID2 OP Endpoint
  469. if ($delegate) {
  470. # We have also found an OP-Local ID.
  471. $this->identity = $delegate;
  472. }
  473. $this->server = $server;
  474. return $server;
  475. }
  476. throw new ErrorException("No OpenID Server found at $url", 404);
  477. }
  478. throw new ErrorException('Endless redirection!', 500);
  479. }
  480. protected function sregParams()
  481. {
  482. $params = array();
  483. # We always use SREG 1.1, even if the server is advertising only support for 1.0.
  484. # That's because it's fully backwards compatibile with 1.0, and some providers
  485. # advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com
  486. $params['openid.ns.sreg'] = 'http://openid.net/extensions/sreg/1.1';
  487. if ($this->required) {
  488. $params['openid.sreg.required'] = array();
  489. foreach ($this->required as $required) {
  490. if (!isset(self::$ax_to_sreg[$required])) continue;
  491. $params['openid.sreg.required'][] = self::$ax_to_sreg[$required];
  492. }
  493. $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']);
  494. }
  495. if ($this->optional) {
  496. $params['openid.sreg.optional'] = array();
  497. foreach ($this->optional as $optional) {
  498. if (!isset(self::$ax_to_sreg[$optional])) continue;
  499. $params['openid.sreg.optional'][] = self::$ax_to_sreg[$optional];
  500. }
  501. $params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']);
  502. }
  503. return $params;
  504. }
  505. protected function axParams()
  506. {
  507. $params = array();
  508. if ($this->required || $this->optional) {
  509. $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0';
  510. $params['openid.ax.mode'] = 'fetch_request';
  511. $this->aliases = array();
  512. $counts = array();
  513. $required = array();
  514. $optional = array();
  515. foreach (array('required','optional') as $type) {
  516. foreach ($this->$type as $alias => $field) {
  517. if (is_int($alias)) $alias = strtr($field, '/', '_');
  518. $this->aliases[$alias] = 'http://axschema.org/' . $field;
  519. if (empty($counts[$alias])) $counts[$alias] = 0;
  520. $counts[$alias] += 1;
  521. ${$type}[] = $alias;
  522. }
  523. }
  524. foreach ($this->aliases as $alias => $ns) {
  525. $params['openid.ax.type.' . $alias] = $ns;
  526. }
  527. foreach ($counts as $alias => $count) {
  528. if ($count == 1) continue;
  529. $params['openid.ax.count.' . $alias] = $count;
  530. }
  531. # Don't send empty ax.requied and ax.if_available.
  532. # Google and possibly other providers refuse to support ax when one of these is empty.
  533. if($required) {
  534. $params['openid.ax.required'] = implode(',', $required);
  535. }
  536. if($optional) {
  537. $params['openid.ax.if_available'] = implode(',', $optional);
  538. }
  539. }
  540. return $params;
  541. }
  542. protected function authUrl_v1($immediate)
  543. {
  544. $returnUrl = $this->returnUrl;
  545. # If we have an openid.delegate that is different from our claimed id,
  546. # we need to somehow preserve the claimed id between requests.
  547. # The simplest way is to just send it along with the return_to url.
  548. if($this->identity != $this->claimed_id) {
  549. $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->claimed_id;
  550. }
  551. $params = array(
  552. 'openid.return_to' => $returnUrl,
  553. 'openid.mode' => $immediate ? 'checkid_immediate' : 'checkid_setup',
  554. 'openid.identity' => $this->identity,
  555. 'openid.trust_root' => $this->trustRoot,
  556. ) + $this->sregParams();
  557. return $this->build_url(parse_url($this->server)
  558. , array('query' => http_build_query($params, '', '&')));
  559. }
  560. protected function authUrl_v2($immediate)
  561. {
  562. $params = array(
  563. 'openid.ns' => 'http://specs.openid.net/auth/2.0',
  564. 'openid.mode' => $immediate ? 'checkid_immediate' : 'checkid_setup',
  565. 'openid.return_to' => $this->returnUrl,
  566. 'openid.realm' => $this->trustRoot,
  567. );
  568. if ($this->ax) {
  569. $params += $this->axParams();
  570. }
  571. if ($this->sreg) {
  572. $params += $this->sregParams();
  573. }
  574. if (!$this->ax && !$this->sreg) {
  575. # If OP doesn't advertise either SREG, nor AX, let's send them both
  576. # in worst case we don't get anything in return.
  577. $params += $this->axParams() + $this->sregParams();
  578. }
  579. if ($this->identifier_select) {
  580. $params['openid.identity'] = $params['openid.claimed_id']
  581. = 'http://specs.openid.net/auth/2.0/identifier_select';
  582. } else {
  583. $params['openid.identity'] = $this->identity;
  584. $params['openid.claimed_id'] = $this->claimed_id;
  585. }
  586. return $this->build_url(parse_url($this->server)
  587. , array('query' => http_build_query($params, '', '&')));
  588. }
  589. /**
  590. * Returns authentication url. Usually, you want to redirect your user to it.
  591. * @return String The authentication url.
  592. * @param String $select_identifier Whether to request OP to select identity for an user in OpenID 2. Does not affect OpenID 1.
  593. * @throws ErrorException
  594. */
  595. function authUrl($immediate = false)
  596. {
  597. if ($this->setup_url && !$immediate) return $this->setup_url;
  598. if (!$this->server) $this->discover($this->identity);
  599. if ($this->version == 2) {
  600. return $this->authUrl_v2($immediate);
  601. }
  602. return $this->authUrl_v1($immediate);
  603. }
  604. /**
  605. * Performs OpenID verification with the OP.
  606. * @return Bool Whether the verification was successful.
  607. * @throws ErrorException
  608. */
  609. function validate()
  610. {
  611. # If the request was using immediate mode, a failure may be reported
  612. # by presenting user_setup_url (for 1.1) or reporting
  613. # mode 'setup_needed' (for 2.0). Also catching all modes other than
  614. # id_res, in order to avoid throwing errors.
  615. if(isset($this->data['openid_user_setup_url'])) {
  616. $this->setup_url = $this->data['openid_user_setup_url'];
  617. return false;
  618. }
  619. if($this->mode != 'id_res') {
  620. return false;
  621. }
  622. $this->claimed_id = isset($this->data['openid_claimed_id'])?$this->data['openid_claimed_id']:$this->data['openid_identity'];
  623. $params = array(
  624. 'openid.assoc_handle' => $this->data['openid_assoc_handle'],
  625. 'openid.signed' => $this->data['openid_signed'],
  626. 'openid.sig' => $this->data['openid_sig'],
  627. );
  628. if (isset($this->data['openid_ns'])) {
  629. # We're dealing with an OpenID 2.0 server, so let's set an ns
  630. # Even though we should know location of the endpoint,
  631. # we still need to verify it by discovery, so $server is not set here
  632. $params['openid.ns'] = 'http://specs.openid.net/auth/2.0';
  633. } elseif (isset($this->data['openid_claimed_id'])
  634. && $this->data['openid_claimed_id'] != $this->data['openid_identity']
  635. ) {
  636. # If it's an OpenID 1 provider, and we've got claimed_id,
  637. # we have to append it to the returnUrl, like authUrl_v1 does.
  638. $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?')
  639. . 'openid.claimed_id=' . $this->claimed_id;
  640. }
  641. if ($this->data['openid_return_to'] != $this->returnUrl) {
  642. # The return_to url must match the url of current request.
  643. # I'm assuing that noone will set the returnUrl to something that doesn't make sense.
  644. return false;
  645. }
  646. $server = $this->discover($this->claimed_id);
  647. foreach (explode(',', $this->data['openid_signed']) as $item) {
  648. # Checking whether magic_quotes_gpc is turned on, because
  649. # the function may fail if it is. For example, when fetching
  650. # AX namePerson, it might containg an apostrophe, which will be escaped.
  651. # In such case, validation would fail, since we'd send different data than OP
  652. # wants to verify. stripslashes() should solve that problem, but we can't
  653. # use it when magic_quotes is off.
  654. $value = $this->data['openid_' . str_replace('.','_',$item)];
  655. $params['openid.' . $item] = function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc() ? stripslashes($value) : $value;
  656. }
  657. $params['openid.mode'] = 'check_authentication';
  658. $response = $this->request($server, 'POST', $params);
  659. return preg_match('/is_valid\s*:\s*true/i', $response);
  660. }
  661. protected function getAxAttributes()
  662. {
  663. $alias = null;
  664. if (isset($this->data['openid_ns_ax'])
  665. && $this->data['openid_ns_ax'] != 'http://openid.net/srv/ax/1.0'
  666. ) { # It's the most likely case, so we'll check it before
  667. $alias = 'ax';
  668. } else {
  669. # 'ax' prefix is either undefined, or points to another extension,
  670. # so we search for another prefix
  671. foreach ($this->data as $key => $val) {
  672. if (substr($key, 0, strlen('openid_ns_')) == 'openid_ns_'
  673. && $val == 'http://openid.net/srv/ax/1.0'
  674. ) {
  675. $alias = substr($key, strlen('openid_ns_'));
  676. break;
  677. }
  678. }
  679. }
  680. if (!$alias) {
  681. # An alias for AX schema has not been found,
  682. # so there is no AX data in the OP's response
  683. return array();
  684. }
  685. $attributes = array();
  686. foreach (explode(',', $this->data['openid_signed']) as $key) {
  687. $keyMatch = $alias . '.value.';
  688. if (substr($key, 0, strlen($keyMatch)) != $keyMatch) {
  689. continue;
  690. }
  691. $key = substr($key, strlen($keyMatch));
  692. if (!isset($this->data['openid_' . $alias . '_type_' . $key])) {
  693. # OP is breaking the spec by returning a field without
  694. # associated ns. This shouldn't happen, but it's better
  695. # to check, than cause an E_NOTICE.
  696. continue;
  697. }
  698. $value = $this->data['openid_' . $alias . '_value_' . $key];
  699. $key = substr($this->data['openid_' . $alias . '_type_' . $key],
  700. strlen('http://axschema.org/'));
  701. $attributes[$key] = $value;
  702. }
  703. return $attributes;
  704. }
  705. protected function getSregAttributes()
  706. {
  707. $attributes = array();
  708. $sreg_to_ax = array_flip(self::$ax_to_sreg);
  709. foreach (explode(',', $this->data['openid_signed']) as $key) {
  710. $keyMatch = 'sreg.';
  711. if (substr($key, 0, strlen($keyMatch)) != $keyMatch) {
  712. continue;
  713. }
  714. $key = substr($key, strlen($keyMatch));
  715. if (!isset($sreg_to_ax[$key])) {
  716. # The field name isn't part of the SREG spec, so we ignore it.
  717. continue;
  718. }
  719. $attributes[$sreg_to_ax[$key]] = $this->data['openid_sreg_' . $key];
  720. }
  721. return $attributes;
  722. }
  723. /**
  724. * Gets AX/SREG attributes provided by OP. should be used only after successful validaton.
  725. * Note that it does not guarantee that any of the required/optional parameters will be present,
  726. * or that there will be no other attributes besides those specified.
  727. * In other words. OP may provide whatever information it wants to.
  728. * * SREG names will be mapped to AX names.
  729. * * @return Array Array of attributes with keys being the AX schema names, e.g. 'contact/email'
  730. * @see http://www.axschema.org/types/
  731. */
  732. function getAttributes()
  733. {
  734. if (isset($this->data['openid_ns'])
  735. && $this->data['openid_ns'] == 'http://specs.openid.net/auth/2.0'
  736. ) { # OpenID 2.0
  737. # We search for both AX and SREG attributes, with AX taking precedence.
  738. return $this->getAxAttributes() + $this->getSregAttributes();
  739. }
  740. return $this->getSregAttributes();
  741. }
  742. }