Discover.php 17 KB

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