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