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