Index: modules/openid/openid.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/openid/openid.inc,v retrieving revision 1.26 diff -u -9 -p -r1.26 openid.inc --- modules/openid/openid.inc 6 Mar 2010 06:39:00 -0000 1.26 +++ modules/openid/openid.inc 21 Mar 2010 20:47:47 -0000 @@ -55,18 +55,28 @@ define('OPENID_NS_1_0', 'http://openid.n */ define('OPENID_NS_SREG', 'http://openid.net/extensions/sreg/1.1'); /** * OpenID Attribute Exchange extension. */ define('OPENID_NS_AX', 'http://openid.net/srv/ax/1.0'); /** + * Extensible Resource Descriptor documents. + */ +define('OPENID_NS_XRD', 'xri://$xrd*($v*2.0)'); + +/** + * OpenID IDP for Google hosted domains. + */ +define('OPENID_NS_GOOGLE', 'http://namespace.google.com/openid/xmlns'); + +/** * Performs an HTTP 302 redirect (for the 1.x protocol). */ function openid_redirect_http($url, $message) { $query = array(); foreach ($message as $key => $val) { $query[] = $key . '=' . urlencode($val); } $sep = (strpos($url, '?') === FALSE) ? '?' : '&'; @@ -103,18 +113,61 @@ function openid_redirect_form($form, &$f '#prefix' => '', '#value' => t('Send'), ); return $form; } /** + * Parse an XRDS document. + * + * @param $raw_xml + * A string containing the XRDS document. + * @return + * An array of service entries. + */ +function _openid_xrds_parse($raw_xml) { + $services = array(); + try { + $xml = @new SimpleXMLElement($raw_xml); + foreach ($xml->children(OPENID_NS_XRD)->XRD as $xrd) { + foreach ($xrd->children(OPENID_NS_XRD)->Service as $service_element) { + $service = array( + 'priority' => $service_element->attributes()->priority ? (int)$service_element->attributes()->priority : PHP_INT_MAX, + 'types' => array(), + 'uri' => (string)$service_element->children(OPENID_NS_XRD)->URI, + 'service' => $service_element, + 'xrd' => $xrd, + ); + foreach ($service_element->Type as $type) { + $service['types'][] = (string)$type; + } + if ($service_element->children(OPENID_NS_XRD)->Delegate) { + $service['identity'] = (string)$service_element->children(OPENID_NS_XRD)->Delegate; + } + if ($service_element->children(OPENID_NS_XRD)->LocalID) { + $service['identity'] = (string)$service_element->children(OPENID_NS_XRD)->LocalID; + } + else { + $service['identity'] = FALSE; + } + $services[] = $service; + } + } + } + catch (Exception $e) { + // Invalid XML. + } + return $services; +} + +/** * Select a service element. * * The procedure is described in OpenID Authentication 2.0, section 7.3.2. * * A new entry is added to the returned array with the key 'version' and the * value 1 or 2 specifying the protocol version used by the service. * * @param $services * An array of service arrays as returned by openid_discovery(). @@ -149,18 +202,24 @@ function _openid_select_service(array $s if (!empty($service['uri']) && in_array('http://specs.openid.net/auth/2.0/signon', $service['types'])) { $service['version'] = 2; if (!$selected_service || $service['priority'] < $selected_service['priority']) { $selected_service = $service; } } } } + if ($selected_service) { + // Unset SimpleXMLElement instances that cannot be saved in $_SESSION. + unset($selected_service['xrd']); + unset($selected_service['service']); + } + return $selected_service; } /** * Determine if the given identifier is an XRI ID. */ function _openid_is_xri($identifier) { // Strip the xri:// scheme from the identifier if present. if (stripos($identifier, 'xri://') !== FALSE) { Index: modules/openid/openid.module =================================================================== RCS file: /cvs/drupal/drupal/modules/openid/openid.module,v retrieving revision 1.75 diff -u -9 -p -r1.75 openid.module --- modules/openid/openid.module 9 Mar 2010 12:07:37 -0000 1.75 +++ modules/openid/openid.module 21 Mar 2010 20:47:48 -0000 @@ -202,28 +202,24 @@ function openid_begin($claimed_id, $retu if (function_exists('bcadd')) { $assoc_handle = openid_association($service['uri']); } if (in_array('http://specs.openid.net/auth/2.0/server', $service['types'])) { // User entered an OP Identifier. $claimed_id = $identity = 'http://specs.openid.net/auth/2.0/identifier_select'; } else { - // Look for OP-Local Identifier. - if (!empty($service['localid'])) { - $identity = $service['localid']; - } - elseif (!empty($service['delegate'])) { - $identity = $service['delegate']; - } - else { - $identity = $claimed_id; + // Use Claimed ID and/or OP-Local Identifier from service description, if + // available. + if (!empty($service['claimed_id'])) { + $claimed_id = $service['claimed_id']; } + $identity = !empty($service['identity']) ? $service['identity'] : $claimed_id; } $request = openid_authentication_request($claimed_id, $identity, $return_to, $assoc_handle, $service); if ($service['version'] == 2) { openid_redirect($service['uri'], $request); } else { openid_redirect_http($service['uri'], $request); } @@ -252,24 +248,33 @@ function openid_complete($response = arr $claimed_id = $_SESSION['openid']['claimed_id']; unset($_SESSION['openid']['service']); unset($_SESSION['openid']['claimed_id']); if (isset($response['openid.mode'])) { if ($response['openid.mode'] == 'cancel') { $response['status'] = 'cancel'; } else { if (openid_verify_assertion($service['uri'], $response)) { + // OpenID Authentication, section 7.3.2.3 and Appendix A.5: + // The CanonicalID specified in the XRDS document must be used as the + // account key. We rely on the XRI proxy resolver to verify that the + // provider is authorized to respond on behalf of the specified + // identifer (required per Extensible Resource Identifier (XRI) + // (XRI) Resolution Version 2.0, section 14.3): + if (!empty($service['claimed_id'])) { + $response['openid.claimed_id'] = $service['claimed_id']; + } // OpenID Authentication, section 11.2: // If the returned Claimed Identifier is different from the one sent // to the OpenID Provider, we need to do discovery on the returned - // identififer to make sure that the provider is authorized to respond + // identifier to make sure that the provider is authorized to respond // on behalf of this. - if ($service['version'] == 2 && $response['openid.claimed_id'] != openid_normalize($claimed_id)) { + elseif ($service['version'] == 2 && $response['openid.claimed_id'] != openid_normalize($claimed_id)) { $services = openid_discovery($response['openid.claimed_id']); $uris = array(); foreach ($services as $discovered_service) { if (in_array('http://specs.openid.net/auth/2.0/server', $discovered_service['types']) || in_array('http://specs.openid.net/auth/2.0/signon', $discovered_service['types'])) { $uris[] = $discovered_service['uri']; } } if (!in_array($service['uri'], $uris)) { return $response; @@ -332,20 +337,26 @@ function openid_openid_discovery_method_ /** * OpenID discovery method: perform an XRI discovery. * * @see http://openid.net/specs/openid-authentication-2_0.html#discovery * @see hook_openid_discovery_method_info() */ function _openid_xri_discovery($claimed_id) { if (_openid_is_xri($claimed_id)) { - $xrds_url = 'http://xri.net/' . $claimed_id; - _openid_xrds_discovery($xrds_url); + // Resolve XRI using a proxy resolver (Extensible Resource Identifier (XRI) + // Resolution Version 2.0, section 11.2). + $xrds_url = variable_get('xri_proxy_resolver', 'http://xri.net/') . rawurlencode($claimed_id) . '?_xrd_r=application/xrds+xml'; + $services = _openid_xrds_discovery($xrds_url); + foreach ($services as &$service) { + $service['claimed_id'] = openid_normalize((string)$service['xrd']->children(OPENID_NS_XRD)->CanonicalID); + } + return $services; } } /** * OpenID discovery method: perform a XRDS discovery. * * @see http://openid.net/specs/openid-authentication-2_0.html#discovery * @see hook_openid_discovery_method_info() */ @@ -356,55 +367,55 @@ function _openid_xrds_discovery($claimed $scheme = @parse_url($xrds_url, PHP_URL_SCHEME); if ($scheme == 'http' || $scheme == 'https') { // For regular URLs, try Yadis resolution first, then HTML-based discovery $headers = array('Accept' => 'application/xrds+xml'); $result = drupal_http_request($xrds_url, array('headers' => $headers)); if (!isset($result->error)) { if (isset($result->headers['Content-Type']) && preg_match("/application\/xrds\+xml/", $result->headers['Content-Type'])) { // Parse XML document to find URL - $services = xrds_parse($result->data); + $services = _openid_xrds_parse($result->data); } else { $xrds_url = NULL; if (isset($result->headers['X-XRDS-Location'])) { $xrds_url = $result->headers['X-XRDS-Location']; } else { // Look for meta http-equiv link in HTML head $xrds_url = _openid_meta_httpequiv('X-XRDS-Location', $result->data); } if (!empty($xrds_url)) { $headers = array('Accept' => 'application/xrds+xml'); $xrds_result = drupal_http_request($xrds_url, array('headers' => $headers)); if (!isset($xrds_result->error)) { - $services = xrds_parse($xrds_result->data); + $services = _openid_xrds_parse($xrds_result->data); } } } // Check for HTML delegation if (count($services) == 0) { // Look for 2.0 links $uri = _openid_link_href('openid2.provider', $result->data); - $delegate = _openid_link_href('openid2.local_id', $result->data); + $identity = _openid_link_href('openid2.local_id', $result->data); $type = 'http://specs.openid.net/auth/2.0/signon'; // 1.x links if (empty($uri)) { $uri = _openid_link_href('openid.server', $result->data); - $delegate = _openid_link_href('openid.delegate', $result->data); + $identity = _openid_link_href('openid.delegate', $result->data); $type = 'http://openid.net/signon/1.1'; } if (!empty($uri)) { $services[] = array( 'uri' => $uri, - 'delegate' => $delegate, + 'identity' => $identity, 'types' => array($type), ); } } } } return $services; } @@ -426,21 +437,21 @@ function _openid_google_user_discovery($ $response = drupal_http_request('https://www.google.com/accounts/o8/.well-known/host-meta?hd=' . rawurlencode($url['host'])); if (isset($response->error) || $response->code != 200) { return; } if (preg_match('/Link: <(.*)>/', $response->data, $matches)) { $xrds_url = $matches[1]; $services = _openid_xrds_discovery($xrds_url); - foreach ($services as $service) { - if (in_array('http://www.iana.org/assignments/relation/describedby', $service['types']) && !empty($service['additional']['URITEMPLATE'])) { - $template = $service['additional']['URITEMPLATE']; + foreach ($services as $i => $service) { + if (in_array('http://www.iana.org/assignments/relation/describedby', $service['types']) && $service['service']->children(OPENID_NS_GOOGLE)->URITemplate) { + $template = (string)$service['service']->children(OPENID_NS_GOOGLE)->URITemplate; $xrds_url = str_replace('{%uri}', rawurlencode($claimed_id), $template); return _openid_xrds_discovery($xrds_url); } } } } /** * Implementation of hook_openid_normalization_method_info(). Index: modules/openid/openid.test =================================================================== RCS file: /cvs/drupal/drupal/modules/openid/openid.test,v retrieving revision 1.15 diff -u -9 -p -r1.15 openid.test --- modules/openid/openid.test 2 Mar 2010 08:59:54 -0000 1.15 +++ modules/openid/openid.test 21 Mar 2010 20:47:48 -0000 @@ -53,18 +53,21 @@ class OpenIDFunctionalTest extends Drupa // Identifier is the URL of an HTML page that is sent with an HTTP header // that contains the URL of an XRDS document. $this->addIdentity(url('openid-test/yadis/x-xrds-location', array('absolute' => TRUE)), 2); // Identifier is the URL of an HTML page containing a // element that contains the URL of an XRDS document. $this->addIdentity(url('openid-test/yadis/http-equiv', array('absolute' => TRUE)), 2); + // Identifier is an XRI. Resolve using our own dummy proxy resolver. + variable_set('xri_proxy_resolver', url('openid-test/yadis/xrds/xri', array('absolute' => TRUE)) . '/'); + $this->addIdentity('@example*résumé;%25', 2, 'http://example.com/user'); // HTML-based discovery: // If the User-supplied Identifier is a URL of an HTML page, the page may // contain a element containing the URL of the OpenID // Provider Endpoint. OpenID 1 and 2 describe slightly different formats. // OpenID Authentication 1.1, section 3.1: $this->addIdentity(url('openid-test/html/openid1', array('absolute' => TRUE)), 1); Index: modules/openid/xrds.inc =================================================================== RCS file: modules/openid/xrds.inc diff -N modules/openid/xrds.inc --- modules/openid/xrds.inc 2 Mar 2010 08:59:54 -0000 1.6 +++ /dev/null 1 Jan 1970 00:00:00 -0000 @@ -1,90 +0,0 @@ - $value) { - if (_xrds_strip_namespace($attribute_name) == 'PRIORITY') { - $xrds_current_service['priority'] = intval($value); - } - } - } -} - -function _xrds_element_end(&$parser, $name) { - global $xrds_open_elements, $xrds_services, $xrds_current_service; - - $name = _xrds_strip_namespace($name); - if ($name == 'SERVICE') { - if (!isset($xrds_current_service['priority'])) { - // If the priority attribute is absent, the default is infinity. - $xrds_current_service['priority'] = PHP_INT_MAX; - } - $xrds_services[] = $xrds_current_service; - $xrds_current_service = array(); - } - array_pop($xrds_open_elements); -} - -function _xrds_cdata(&$parser, $data) { - global $xrds_open_elements, $xrds_services, $xrds_current_service; - $path = strtoupper(implode('/', $xrds_open_elements)); - switch ($path) { - case 'XRDS/XRD/SERVICE/TYPE': - $xrds_current_service['types'][] = $data; - break; - case 'XRDS/XRD/SERVICE/URI': - $xrds_current_service['uri'] = $data; - break; - case 'XRDS/XRD/SERVICE/DELEGATE': - $xrds_current_service['delegate'] = $data; - break; - case 'XRDS/XRD/SERVICE/LOCALID': - $xrds_current_service['localid'] = $data; - break; - default: - if (preg_match('@^XRDS/XRD/SERVICE/(.*)$@', $path, $matches)) { - $xrds_current_service['additional'][$matches[1]] = $data; - } - break; - } -} - -function _xrds_strip_namespace($name) { - // Strip namespacing. - $pos = strrpos($name, ':'); - if ($pos !== FALSE) { - $name = substr($name, $pos + 1, strlen($name)); - } - - return $name; -} Index: modules/openid/tests/openid_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/openid/tests/openid_test.module,v retrieving revision 1.10 diff -u -9 -p -r1.10 openid_test.module --- modules/openid/tests/openid_test.module 2 Mar 2010 08:55:42 -0000 1.10 +++ modules/openid/tests/openid_test.module 21 Mar 2010 20:47:48 -0000 @@ -63,22 +63,41 @@ function openid_test_menu() { ); return $items; } /** * Menu callback; XRDS document that references the OP Endpoint URL. */ function openid_test_yadis_xrds() { if ($_SERVER['HTTP_ACCEPT'] == 'application/xrds+xml') { + // Only respond to XRI requests for one specific XRI. The is used to verify + // that the XRI has been properly encoded. The "+" sign in the _xrd_r query + // parameter is decoded to a space by PHP. + if (arg(3) == 'xri') { + if (variable_get('clean_url', 0)) { + if (arg(4) != '@example*résumé;%25' || $_GET['_xrd_r'] != 'application/xrds xml') { + drupal_not_found(); + } + } + else { + // Drupal cannot properly emulate an XRI proxy resolver using unclean + // URLs, so the arguments gets messed up. + if (arg(4) . '/' . arg(5) != '@example*résumé;%25?_xrd_r=application/xrds xml') { + drupal_not_found(); + } + } + } drupal_add_http_header('Content-Type', 'application/xrds+xml'); print ' + xri://@ + http://example.com/user http://example.com/this-is-ignored http://specs.openid.net/auth/2.0/signon http://openid.net/srv/ax/1.0 ' . url('openid-test/endpoint', array('absolute' => TRUE)) . ' @@ -96,19 +115,19 @@ function openid_test_yadis_xrds() { http://specs.openid.net/auth/2.0/server http://example.com/this-has-too-low-priority http://specs.openid.net/auth/2.0/server ' . url('openid-test/endpoint', array('absolute' => TRUE)) . ' '; } print ' - + '; } else { return t('This is a regular HTML page. If the client sends an Accept: application/xrds+xml header when requesting this URL, an XRDS document is returned.'); } } /** * Menu callback; regular HTML page with an X-XRDS-Location HTTP header.