Discover.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. <?php
  2. /**
  3. * The OpenID and Yadis discovery implementation for OpenID 1.2.
  4. */
  5. require_once "Auth/OpenID.php";
  6. require_once "Auth/OpenID/Parse.php";
  7. require_once "Auth/OpenID/Message.php";
  8. require_once "Auth/Yadis/XRIRes.php";
  9. require_once "Auth/Yadis/Yadis.php";
  10. // XML namespace value
  11. define('Auth_OpenID_XMLNS_1_0', 'http://openid.net/xmlns/1.0');
  12. // Yadis service types
  13. define('Auth_OpenID_TYPE_1_2', 'http://openid.net/signon/1.2');
  14. define('Auth_OpenID_TYPE_1_1', 'http://openid.net/signon/1.1');
  15. define('Auth_OpenID_TYPE_1_0', 'http://openid.net/signon/1.0');
  16. define('Auth_OpenID_TYPE_2_0_IDP', 'http://specs.openid.net/auth/2.0/server');
  17. define('Auth_OpenID_TYPE_2_0', 'http://specs.openid.net/auth/2.0/signon');
  18. define('Auth_OpenID_RP_RETURN_TO_URL_TYPE',
  19. 'http://specs.openid.net/auth/2.0/return_to');
  20. function Auth_OpenID_getOpenIDTypeURIs()
  21. {
  22. return array(Auth_OpenID_TYPE_2_0_IDP,
  23. Auth_OpenID_TYPE_2_0,
  24. Auth_OpenID_TYPE_1_2,
  25. Auth_OpenID_TYPE_1_1,
  26. Auth_OpenID_TYPE_1_0);
  27. }
  28. function Auth_OpenID_getOpenIDConsumerTypeURIs()
  29. {
  30. return array(Auth_OpenID_RP_RETURN_TO_URL_TYPE);
  31. }
  32. /*
  33. * Provides a user-readable interpretation of a type uri.
  34. * Useful for error messages.
  35. */
  36. function Auth_OpenID_getOpenIDTypeName($type_uri) {
  37. switch ($type_uri) {
  38. case Auth_OpenID_TYPE_2_0_IDP:
  39. return 'OpenID 2.0 IDP';
  40. case Auth_OpenID_TYPE_2_0:
  41. return 'OpenID 2.0';
  42. case Auth_OpenID_TYPE_1_2:
  43. return 'OpenID 1.2';
  44. case Auth_OpenID_TYPE_1_1:
  45. return 'OpenID 1.1';
  46. case Auth_OpenID_TYPE_1_0:
  47. return 'OpenID 1.0';
  48. case Auth_OpenID_RP_RETURN_TO_URL_TYPE:
  49. return 'OpenID relying party';
  50. }
  51. }
  52. /**
  53. * Object representing an OpenID service endpoint.
  54. */
  55. class Auth_OpenID_ServiceEndpoint {
  56. function Auth_OpenID_ServiceEndpoint()
  57. {
  58. $this->claimed_id = null;
  59. $this->server_url = null;
  60. $this->type_uris = array();
  61. $this->local_id = null;
  62. $this->canonicalID = null;
  63. $this->used_yadis = false; // whether this came from an XRDS
  64. $this->display_identifier = null;
  65. }
  66. function getDisplayIdentifier()
  67. {
  68. if ($this->display_identifier) {
  69. return $this->display_identifier;
  70. }
  71. if (! $this->claimed_id) {
  72. return $this->claimed_id;
  73. }
  74. $parsed = parse_url($this->claimed_id);
  75. $scheme = $parsed['scheme'];
  76. $host = $parsed['host'];
  77. $path = $parsed['path'];
  78. if (array_key_exists('query', $parsed)) {
  79. $query = $parsed['query'];
  80. $no_frag = "$scheme://$host$path?$query";
  81. } else {
  82. $no_frag = "$scheme://$host$path";
  83. }
  84. return $no_frag;
  85. }
  86. function usesExtension($extension_uri)
  87. {
  88. return in_array($extension_uri, $this->type_uris);
  89. }
  90. function preferredNamespace()
  91. {
  92. if (in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris) ||
  93. in_array(Auth_OpenID_TYPE_2_0, $this->type_uris)) {
  94. return Auth_OpenID_OPENID2_NS;
  95. } else {
  96. return Auth_OpenID_OPENID1_NS;
  97. }
  98. }
  99. /*
  100. * Query this endpoint to see if it has any of the given type
  101. * URIs. This is useful for implementing other endpoint classes
  102. * that e.g. need to check for the presence of multiple versions
  103. * of a single protocol.
  104. *
  105. * @param $type_uris The URIs that you wish to check
  106. *
  107. * @return all types that are in both in type_uris and
  108. * $this->type_uris
  109. */
  110. function matchTypes($type_uris)
  111. {
  112. $result = array();
  113. foreach ($type_uris as $test_uri) {
  114. if ($this->supportsType($test_uri)) {
  115. $result[] = $test_uri;
  116. }
  117. }
  118. return $result;
  119. }
  120. function supportsType($type_uri)
  121. {
  122. // Does this endpoint support this type?
  123. return ((in_array($type_uri, $this->type_uris)) ||
  124. (($type_uri == Auth_OpenID_TYPE_2_0) &&
  125. $this->isOPIdentifier()));
  126. }
  127. function compatibilityMode()
  128. {
  129. return $this->preferredNamespace() != Auth_OpenID_OPENID2_NS;
  130. }
  131. function isOPIdentifier()
  132. {
  133. return in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris);
  134. }
  135. static function fromOPEndpointURL($op_endpoint_url)
  136. {
  137. // Construct an OP-Identifier OpenIDServiceEndpoint object for
  138. // a given OP Endpoint URL
  139. $obj = new Auth_OpenID_ServiceEndpoint();
  140. $obj->server_url = $op_endpoint_url;
  141. $obj->type_uris = array(Auth_OpenID_TYPE_2_0_IDP);
  142. return $obj;
  143. }
  144. function parseService($yadis_url, $uri, $type_uris, $service_element)
  145. {
  146. // Set the state of this object based on the contents of the
  147. // service element. Return true if successful, false if not
  148. // (if findOPLocalIdentifier returns false).
  149. $this->type_uris = $type_uris;
  150. $this->server_url = $uri;
  151. $this->used_yadis = true;
  152. if (!$this->isOPIdentifier()) {
  153. $this->claimed_id = $yadis_url;
  154. $this->local_id = Auth_OpenID_findOPLocalIdentifier(
  155. $service_element,
  156. $this->type_uris);
  157. if ($this->local_id === false) {
  158. return false;
  159. }
  160. }
  161. return true;
  162. }
  163. function getLocalID()
  164. {
  165. // Return the identifier that should be sent as the
  166. // openid.identity_url parameter to the server.
  167. if ($this->local_id === null && $this->canonicalID === null) {
  168. return $this->claimed_id;
  169. } else {
  170. if ($this->local_id) {
  171. return $this->local_id;
  172. } else {
  173. return $this->canonicalID;
  174. }
  175. }
  176. }
  177. /*
  178. * Parse the given document as XRDS looking for OpenID consumer services.
  179. *
  180. * @return array of Auth_OpenID_ServiceEndpoint or null if the
  181. * document cannot be parsed.
  182. */
  183. function consumerFromXRDS($uri, $xrds_text)
  184. {
  185. $xrds =& Auth_Yadis_XRDS::parseXRDS($xrds_text);
  186. if ($xrds) {
  187. $yadis_services =
  188. $xrds->services(array('filter_MatchesAnyOpenIDConsumerType'));
  189. return Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services);
  190. }
  191. return null;
  192. }
  193. /*
  194. * Parse the given document as XRDS looking for OpenID services.
  195. *
  196. * @return array of Auth_OpenID_ServiceEndpoint or null if the
  197. * document cannot be parsed.
  198. */
  199. static function fromXRDS($uri, $xrds_text)
  200. {
  201. $xrds = Auth_Yadis_XRDS::parseXRDS($xrds_text);
  202. if ($xrds) {
  203. $yadis_services =
  204. $xrds->services(array('filter_MatchesAnyOpenIDType'));
  205. return Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services);
  206. }
  207. return null;
  208. }
  209. /*
  210. * Create endpoints from a DiscoveryResult.
  211. *
  212. * @param discoveryResult Auth_Yadis_DiscoveryResult
  213. * @return array of Auth_OpenID_ServiceEndpoint or null if
  214. * endpoints cannot be created.
  215. */
  216. static function fromDiscoveryResult($discoveryResult)
  217. {
  218. if ($discoveryResult->isXRDS()) {
  219. return Auth_OpenID_ServiceEndpoint::fromXRDS(
  220. $discoveryResult->normalized_uri,
  221. $discoveryResult->response_text);
  222. } else {
  223. return Auth_OpenID_ServiceEndpoint::fromHTML(
  224. $discoveryResult->normalized_uri,
  225. $discoveryResult->response_text);
  226. }
  227. }
  228. static function fromHTML($uri, $html)
  229. {
  230. $discovery_types = array(
  231. array(Auth_OpenID_TYPE_2_0,
  232. 'openid2.provider', 'openid2.local_id'),
  233. array(Auth_OpenID_TYPE_1_1,
  234. 'openid.server', 'openid.delegate')
  235. );
  236. $services = array();
  237. foreach ($discovery_types as $triple) {
  238. list($type_uri, $server_rel, $delegate_rel) = $triple;
  239. $urls = Auth_OpenID_legacy_discover($html, $server_rel,
  240. $delegate_rel);
  241. if ($urls === false) {
  242. continue;
  243. }
  244. list($delegate_url, $server_url) = $urls;
  245. $service = new Auth_OpenID_ServiceEndpoint();
  246. $service->claimed_id = $uri;
  247. $service->local_id = $delegate_url;
  248. $service->server_url = $server_url;
  249. $service->type_uris = array($type_uri);
  250. $services[] = $service;
  251. }
  252. return $services;
  253. }
  254. function copy()
  255. {
  256. $x = new Auth_OpenID_ServiceEndpoint();
  257. $x->claimed_id = $this->claimed_id;
  258. $x->server_url = $this->server_url;
  259. $x->type_uris = $this->type_uris;
  260. $x->local_id = $this->local_id;
  261. $x->canonicalID = $this->canonicalID;
  262. $x->used_yadis = $this->used_yadis;
  263. return $x;
  264. }
  265. }
  266. function Auth_OpenID_findOPLocalIdentifier($service, $type_uris)
  267. {
  268. // Extract a openid:Delegate value from a Yadis Service element.
  269. // If no delegate is found, returns null. Returns false on
  270. // discovery failure (when multiple delegate/localID tags have
  271. // different values).
  272. $service->parser->registerNamespace('openid',
  273. Auth_OpenID_XMLNS_1_0);
  274. $service->parser->registerNamespace('xrd',
  275. Auth_Yadis_XMLNS_XRD_2_0);
  276. $parser = $service->parser;
  277. $permitted_tags = array();
  278. if (in_array(Auth_OpenID_TYPE_1_1, $type_uris) ||
  279. in_array(Auth_OpenID_TYPE_1_0, $type_uris)) {
  280. $permitted_tags[] = 'openid:Delegate';
  281. }
  282. if (in_array(Auth_OpenID_TYPE_2_0, $type_uris)) {
  283. $permitted_tags[] = 'xrd:LocalID';
  284. }
  285. $local_id = null;
  286. foreach ($permitted_tags as $tag_name) {
  287. $tags = $service->getElements($tag_name);
  288. foreach ($tags as $tag) {
  289. $content = $parser->content($tag);
  290. if ($local_id === null) {
  291. $local_id = $content;
  292. } else if ($local_id != $content) {
  293. return false;
  294. }
  295. }
  296. }
  297. return $local_id;
  298. }
  299. function filter_MatchesAnyOpenIDType($service)
  300. {
  301. $uris = $service->getTypes();
  302. foreach ($uris as $uri) {
  303. if (in_array($uri, Auth_OpenID_getOpenIDTypeURIs())) {
  304. return true;
  305. }
  306. }
  307. return false;
  308. }
  309. function filter_MatchesAnyOpenIDConsumerType(&$service)
  310. {
  311. $uris = $service->getTypes();
  312. foreach ($uris as $uri) {
  313. if (in_array($uri, Auth_OpenID_getOpenIDConsumerTypeURIs())) {
  314. return true;
  315. }
  316. }
  317. return false;
  318. }
  319. function Auth_OpenID_bestMatchingService($service, $preferred_types)
  320. {
  321. // Return the index of the first matching type, or something
  322. // higher if no type matches.
  323. //
  324. // This provides an ordering in which service elements that
  325. // contain a type that comes earlier in the preferred types list
  326. // come before service elements that come later. If a service
  327. // element has more than one type, the most preferred one wins.
  328. foreach ($preferred_types as $index => $typ) {
  329. if (in_array($typ, $service->type_uris)) {
  330. return $index;
  331. }
  332. }
  333. return count($preferred_types);
  334. }
  335. function Auth_OpenID_arrangeByType($service_list, $preferred_types)
  336. {
  337. // Rearrange service_list in a new list so services are ordered by
  338. // types listed in preferred_types. Return the new list.
  339. // Build a list with the service elements in tuples whose
  340. // comparison will prefer the one with the best matching service
  341. $prio_services = array();
  342. foreach ($service_list as $index => $service) {
  343. $prio_services[] = array(Auth_OpenID_bestMatchingService($service,
  344. $preferred_types),
  345. $index, $service);
  346. }
  347. sort($prio_services);
  348. // Now that the services are sorted by priority, remove the sort
  349. // keys from the list.
  350. foreach ($prio_services as $index => $s) {
  351. $prio_services[$index] = $prio_services[$index][2];
  352. }
  353. return $prio_services;
  354. }
  355. // Extract OP Identifier services. If none found, return the rest,
  356. // sorted with most preferred first according to
  357. // OpenIDServiceEndpoint.openid_type_uris.
  358. //
  359. // openid_services is a list of OpenIDServiceEndpoint objects.
  360. //
  361. // Returns a list of OpenIDServiceEndpoint objects."""
  362. function Auth_OpenID_getOPOrUserServices($openid_services)
  363. {
  364. $op_services = Auth_OpenID_arrangeByType($openid_services,
  365. array(Auth_OpenID_TYPE_2_0_IDP));
  366. $openid_services = Auth_OpenID_arrangeByType($openid_services,
  367. Auth_OpenID_getOpenIDTypeURIs());
  368. if ($op_services) {
  369. return $op_services;
  370. } else {
  371. return $openid_services;
  372. }
  373. }
  374. function Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services)
  375. {
  376. $s = array();
  377. if (!$yadis_services) {
  378. return $s;
  379. }
  380. foreach ($yadis_services as $service) {
  381. $type_uris = $service->getTypes();
  382. $uris = $service->getURIs();
  383. // If any Type URIs match and there is an endpoint URI
  384. // specified, then this is an OpenID endpoint
  385. if ($type_uris &&
  386. $uris) {
  387. foreach ($uris as $service_uri) {
  388. $openid_endpoint = new Auth_OpenID_ServiceEndpoint();
  389. if ($openid_endpoint->parseService($uri,
  390. $service_uri,
  391. $type_uris,
  392. $service)) {
  393. $s[] = $openid_endpoint;
  394. }
  395. }
  396. }
  397. }
  398. return $s;
  399. }
  400. function Auth_OpenID_discoverWithYadis($uri, $fetcher,
  401. $endpoint_filter='Auth_OpenID_getOPOrUserServices',
  402. $discover_function=null)
  403. {
  404. // Discover OpenID services for a URI. Tries Yadis and falls back
  405. // on old-style <link rel='...'> discovery if Yadis fails.
  406. // Might raise a yadis.discover.DiscoveryFailure if no document
  407. // came back for that URI at all. I don't think falling back to
  408. // OpenID 1.0 discovery on the same URL will help, so don't bother
  409. // to catch it.
  410. if ($discover_function === null) {
  411. $discover_function = array('Auth_Yadis_Yadis', 'discover');
  412. }
  413. $openid_services = array();
  414. $response = call_user_func_array($discover_function,
  415. array($uri, $fetcher));
  416. $yadis_url = $response->normalized_uri;
  417. $yadis_services = array();
  418. if ($response->isFailure() && !$response->isXRDS()) {
  419. return array($uri, array());
  420. }
  421. $openid_services = Auth_OpenID_ServiceEndpoint::fromXRDS(
  422. $yadis_url,
  423. $response->response_text);
  424. if (!$openid_services) {
  425. if ($response->isXRDS()) {
  426. return Auth_OpenID_discoverWithoutYadis($uri,
  427. $fetcher);
  428. }
  429. // Try to parse the response as HTML to get OpenID 1.0/1.1
  430. // <link rel="...">
  431. $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML(
  432. $yadis_url,
  433. $response->response_text);
  434. }
  435. $openid_services = call_user_func_array($endpoint_filter,
  436. array($openid_services));
  437. return array($yadis_url, $openid_services);
  438. }
  439. function Auth_OpenID_discoverURI($uri, $fetcher)
  440. {
  441. $uri = Auth_OpenID::normalizeUrl($uri);
  442. return Auth_OpenID_discoverWithYadis($uri, $fetcher);
  443. }
  444. function Auth_OpenID_discoverWithoutYadis($uri, $fetcher)
  445. {
  446. $http_resp = @$fetcher->get($uri);
  447. if ($http_resp->status != 200 and $http_resp->status != 206) {
  448. return array($uri, array());
  449. }
  450. $identity_url = $http_resp->final_url;
  451. // Try to parse the response as HTML to get OpenID 1.0/1.1 <link
  452. // rel="...">
  453. $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML(
  454. $identity_url,
  455. $http_resp->body);
  456. return array($identity_url, $openid_services);
  457. }
  458. function Auth_OpenID_discoverXRI($iname, $fetcher)
  459. {
  460. $resolver = new Auth_Yadis_ProxyResolver($fetcher);
  461. list($canonicalID, $yadis_services) =
  462. $resolver->query($iname,
  463. Auth_OpenID_getOpenIDTypeURIs(),
  464. array('filter_MatchesAnyOpenIDType'));
  465. $openid_services = Auth_OpenID_makeOpenIDEndpoints($iname,
  466. $yadis_services);
  467. $openid_services = Auth_OpenID_getOPOrUserServices($openid_services);
  468. for ($i = 0; $i < count($openid_services); $i++) {
  469. $openid_services[$i]->canonicalID = $canonicalID;
  470. $openid_services[$i]->claimed_id = $canonicalID;
  471. $openid_services[$i]->display_identifier = $iname;
  472. }
  473. // FIXME: returned xri should probably be in some normal form
  474. return array($iname, $openid_services);
  475. }
  476. function Auth_OpenID_discover($uri, $fetcher)
  477. {
  478. // If the fetcher (i.e., PHP) doesn't support SSL, we can't do
  479. // discovery on an HTTPS URL.
  480. if ($fetcher->isHTTPS($uri) && !$fetcher->supportsSSL()) {
  481. return array($uri, array());
  482. }
  483. if (Auth_Yadis_identifierScheme($uri) == 'XRI') {
  484. $result = Auth_OpenID_discoverXRI($uri, $fetcher);
  485. } else {
  486. $result = Auth_OpenID_discoverURI($uri, $fetcher);
  487. }
  488. // If the fetcher doesn't support SSL, we can't interact with
  489. // HTTPS server URLs; remove those endpoints from the list.
  490. if (!$fetcher->supportsSSL()) {
  491. $http_endpoints = array();
  492. list($new_uri, $endpoints) = $result;
  493. foreach ($endpoints as $e) {
  494. if (!$fetcher->isHTTPS($e->server_url)) {
  495. $http_endpoints[] = $e;
  496. }
  497. }
  498. $result = array($new_uri, $http_endpoints);
  499. }
  500. return $result;
  501. }