Index: modules/openid/openid.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/openid/openid.inc,v retrieving revision 1.22 diff -u -9 -p -r1.22 openid.inc --- modules/openid/openid.inc 24 Nov 2009 05:20:48 -0000 1.22 +++ modules/openid/openid.inc 31 Jan 2010 16:53:34 -0000 @@ -93,18 +93,68 @@ function openid_redirect_form($form, &$f '#prefix' => '', '#value' => t('Send'), ); return $form; } /** + * 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(). + * @return + * The selected service array, or NULL if no valid services were found. + */ +function _openid_select_service(array $services) { + // Extensible Resource Identifier (XRI) Resolution Version 2.0, section 4.3.3: + // Find the service with the highest priority (lowest integer value). If there + // is a tie, select a random one, not just the first in the XML document. + $selected_service = NULL; + shuffle($services); + + // Search for an OP Identifier Element. + foreach ($services as $service) { + if (!empty($service['uri'])) { + if (in_array('http://specs.openid.net/auth/2.0/server', $service['types'])) { + $service['version'] = 2; + } + elseif (in_array(OPENID_NS_1_0, $service['types']) || in_array(OPENID_NS_1_1, $service['types'])) { + $service['version'] = 1; + } + if (isset($service['version']) && (!$selected_service || $service['priority'] < $selected_service['priority'])) { + $selected_service = $service; + } + } + } + + if (!$selected_service) { + // Search for Claimed Identifier Element. + foreach ($services as $service) { + 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; + } + } + } + } + + 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) { $identifier = substr($identifier, 6); } @@ -112,19 +162,21 @@ function _openid_is_xri($identifier) { $firstchar = substr($identifier, 0, 1); if (strpos("=@+$!(", $firstchar) !== FALSE) { return TRUE; } return FALSE; } /** - * Normalize the given identifier as per spec. + * Normalize the given identifier. + * + * The procedure is described in OpenID Authentication 2.0, section 7.2. */ function _openid_normalize($identifier) { if (_openid_is_xri($identifier)) { return _openid_normalize_xri($identifier); } else { return _openid_normalize_url($identifier); } } Index: modules/openid/openid.module =================================================================== RCS file: /cvs/drupal/drupal/modules/openid/openid.module,v retrieving revision 1.70 diff -u -9 -p -r1.70 openid.module --- modules/openid/openid.module 11 Jan 2010 16:25:16 -0000 1.70 +++ modules/openid/openid.module 31 Jan 2010 16:53:36 -0000 @@ -176,62 +176,62 @@ function openid_login_validate($form, &$ * @param $claimed_id The OpenID to authenticate * @param $return_to The endpoint to return to from the OpenID Provider */ function openid_begin($claimed_id, $return_to = '', $form_values = array()) { module_load_include('inc', 'openid'); $claimed_id = _openid_normalize($claimed_id); $services = openid_discovery($claimed_id); - if (count($services) == 0) { + $service = _openid_select_service($services); + + if (!$service) { form_set_error('openid_identifier', t('Sorry, that is not a valid OpenID. Ensure you have spelled your ID correctly.')); return; } // Store discovered information in the users' session so we don't have to rediscover. - $_SESSION['openid']['service'] = $services[0]; + $_SESSION['openid']['service'] = $service; // Store the claimed id $_SESSION['openid']['claimed_id'] = $claimed_id; // Store the login form values so we can pass them to // user_exteral_login later. $_SESSION['openid']['user_login_values'] = $form_values; - $op_endpoint = $services[0]['uri']; // If bcmath is present, then create an association $assoc_handle = ''; if (function_exists('bcadd')) { - $assoc_handle = openid_association($op_endpoint); + $assoc_handle = openid_association($service['uri']); } - // Now that there is an association created, move on - // to request authentication from the IdP - // First check for LocalID. If not found, check for Delegate. Fall - // back to $claimed_id if neither is found. - if (!empty($services[0]['localid'])) { - $identity = $services[0]['localid']; - } - elseif (!empty($services[0]['delegate'])) { - $identity = $services[0]['delegate']; + 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 { - $identity = $claimed_id; - } - - if (isset($services[0]['types']) && is_array($services[0]['types']) && in_array(OPENID_NS_2_0 . '/server', $services[0]['types'])) { - $claimed_id = $identity = 'http://specs.openid.net/auth/2.0/identifier_select'; + // Look for OP-Local Identifier. + if (!empty($service['localid'])) { + $identity = $service['localid']; + } + elseif (!empty($service['delegate'])) { + $identity = $service['delegate']; + } + else { + $identity = $claimed_id; + } } - $authn_request = openid_authentication_request($claimed_id, $identity, $return_to, $assoc_handle, $services[0]['version']); + $request = openid_authentication_request($claimed_id, $identity, $return_to, $assoc_handle, $service['version']); - if ($services[0]['version'] == 2) { - openid_redirect($op_endpoint, $authn_request); + if ($service['version'] == 2) { + openid_redirect($service['uri'], $request); } else { - openid_redirect_http($op_endpoint, $authn_request); + openid_redirect_http($service['uri'], $request); } } /** * Completes OpenID authentication by validating returned data from the OpenID * Provider. * * @param $response Array of returned values from the OpenID Provider. * @@ -252,23 +252,32 @@ 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)) { - // If the returned claimed_id is different from the session claimed_id, - // then we need to do discovery and make sure the op_endpoint matches. - if ($service['version'] == 2 && $response['openid.claimed_id'] != $claimed_id) { - $disco = openid_discovery($response['openid.claimed_id']); - if ($disco[0]['uri'] != $service['uri']) { + // 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 + // on behalf of this. + if ($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; } } else { $response['openid.claimed_id'] = $claimed_id; } $response['status'] = 'success'; } } @@ -323,28 +332,32 @@ function openid_discovery($claimed_id) { } } } // 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); - $version = 2; + $type = 'http://specs.openid.net/auth/2.0/signon'; - // 1.0 links + // 1.x links if (empty($uri)) { $uri = _openid_link_href('openid.server', $result->data); $delegate = _openid_link_href('openid.delegate', $result->data); - $version = 1; + $type = 'http://openid.net/signon/1.1'; } if (!empty($uri)) { - $services[] = array('uri' => $uri, 'delegate' => $delegate, 'version' => $version); + $services[] = array( + 'uri' => $uri, + 'delegate' => $delegate, + 'types' => array($type), + ); } } } } return $services; } /** * Attempt to create a shared secret with the OpenID Provider. Index: modules/openid/openid.test =================================================================== RCS file: /cvs/drupal/drupal/modules/openid/openid.test,v retrieving revision 1.11 diff -u -9 -p -r1.11 openid.test --- modules/openid/openid.test 30 Jan 2010 07:59:25 -0000 1.11 +++ modules/openid/openid.test 31 Jan 2010 16:53:36 -0000 @@ -39,18 +39,24 @@ class OpenIDFunctionalTest extends Drupa // Yadis discovery (see Yadis Specification 1.0, section 6.2.5): // If the User-supplied Identifier is a URL, it may be a direct or indirect // reference to an XRDS document (a Yadis Resource Descriptor) that contains // the URL of the OpenID Provider Endpoint. // Identifier is the URL of an XRDS document. $this->addIdentity(url('openid-test/yadis/xrds', array('absolute' => TRUE)), 2); + // Identifier is the URL of an XRDS document containing an OP Identifier + // Element. The Relying Party sends the special value + // "http://specs.openid.net/auth/2.0/identifier_select" as Claimed + // Identifier. The OpenID Provider responds with the actual identifier. + $this->addIdentity(url('openid-test/yadis/xrds/server', array('absolute' => TRUE)), 2, url('openid-test/yadis/xrds/dummy-user', array('absolute' => TRUE))); + // 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); @@ -120,32 +126,42 @@ class OpenIDFunctionalTest extends Drupa $this->clickLink(t('Delete')); $this->drupalPost(NULL, array(), t('Confirm')); $this->assertText(t('OpenID deleted.'), t('Identity deleted')); $this->assertNoText($identity, t('Identity no longer appears in list.')); } /** * Add OpenID identity to user's profile. + * + * @param $identity + * The User-supplied Identifier. + * @param $version + * The protocol version used by the service. + * @param $claimed_id + * The expected Claimed Identifier returned by the OpenID Provider. */ - function addIdentity($identity, $version = 2) { + function addIdentity($identity, $version = 2, $claimed_id = NULL) { $this->drupalGet('user/' . $this->web_user->uid . '/openid'); $edit = array('openid_identifier' => $identity); $this->drupalPost(NULL, $edit, t('Add an OpenID')); // OpenID 1 used a HTTP redirect, OpenID 2 uses a HTML form that is submitted automatically using JavaScript. if ($version == 2) { // Manually submit form because SimpleTest is not able to execute JavaScript. $this->assertRaw('', t('JavaScript form submission found.')); $this->drupalPost(NULL, array(), t('Send')); } - $this->assertRaw(t('Successfully added %identity', array('%identity' => $identity)), t('Identity %identity was added.', array('%identity' => $identity))); + if (!$claimed_id) { + $claimed_id = $identity; + } + $this->assertRaw(t('Successfully added %identity', array('%identity' => $claimed_id)), t('Identity %identity was added.', array('%identity' => $identity))); } /** * Test OpenID auto-registration with e-mail verification disabled. */ function testRegisterUserWithoutEmailVerification() { variable_set('user_email_verification', FALSE); // Load the front page to get the user login block. Index: modules/openid/xrds.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/openid/xrds.inc,v retrieving revision 1.4 diff -u -9 -p -r1.4 xrds.inc --- modules/openid/xrds.inc 30 Jan 2010 07:59:25 -0000 1.4 +++ modules/openid/xrds.inc 31 Jan 2010 16:53:36 -0000 @@ -19,40 +19,43 @@ function xrds_parse($xml) { xml_parse($parser, $xml); xml_parser_free($parser); return $xrds_services; } /** * Parser callback functions */ -function _xrds_element_start(&$parser, $name, $attribs) { - global $xrds_open_elements; +function _xrds_element_start(&$parser, $name, $attributes) { + global $xrds_open_elements, $xrds_current_service; $xrds_open_elements[] = _xrds_strip_namespace($name); + + $path = strtoupper(implode('/', $xrds_open_elements)); + if ($path == 'XRDS/XRD/SERVICE') { + foreach ($attributes as $attribute_name => $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 (in_array(OPENID_NS_2_0 . '/signon', $xrds_current_service['types']) || - in_array(OPENID_NS_2_0 . '/server', $xrds_current_service['types'])) { - $xrds_current_service['version'] = 2; - } - elseif (in_array(OPENID_NS_1_1, $xrds_current_service['types']) || - in_array(OPENID_NS_1_0, $xrds_current_service['types'])) { - $xrds_current_service['version'] = 1; - } - if (!empty($xrds_current_service['version'])) { - $xrds_services[] = $xrds_current_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) { Index: modules/openid/tests/openid_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/openid/tests/openid_test.module,v retrieving revision 1.8 diff -u -9 -p -r1.8 openid_test.module --- modules/openid/tests/openid_test.module 4 Dec 2009 16:49:47 -0000 1.8 +++ modules/openid/tests/openid_test.module 31 Jan 2010 16:53:37 -0000 @@ -68,21 +68,45 @@ function openid_test_menu() { * Menu callback; XRDS document that references the OP Endpoint URL. */ function openid_test_yadis_xrds() { if ($_SERVER['HTTP_ACCEPT'] == 'application/xrds+xml') { drupal_add_http_header('Content-Type', 'application/xrds+xml'); print ' + http://example.com/this-is-ignored + + http://specs.openid.net/auth/2.0/signon ' . url('openid-test/endpoint', array('absolute' => TRUE)) . ' + + http://specs.openid.net/auth/2.0/signon + http://example.com/this-has-too-low-priority + + + http://specs.openid.net/auth/2.0/signon + http://example.com/this-has-too-low-priority + + '; + if (arg(3) == 'server') { + print ' + + 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.'); } } /** @@ -196,34 +220,44 @@ function _openid_test_endpoint_associate * OpenID endpoint; handle "authenticate" requests. * * All requests result in a successful response. The request is a GET or POST * made by the user's browser based on an HTML form or HTTP redirect generated * by the Relying Party. The user is redirected back to the Relying Party using * a URL containing a signed message in the query string confirming the user's * identity. */ function _openid_test_endpoint_authenticate() { - global $base_url; - module_load_include('inc', 'openid'); // Generate unique identifier for this authentication. $nonce = _openid_nonce(); + if (!isset($_REQUEST['openid_claimed_id'])) { + // openid.claimed_id is not used in OpenID 1.x. + $claimed_id = ''; + } + elseif ($_REQUEST['openid_claimed_id'] == 'http://specs.openid.net/auth/2.0/identifier_select') { + // The Relying Party did not specify a Claimed Identifier, so the OpenID + // Provider decides on one. + $claimed_id = url('openid-test/yadis/xrds/dummy-user', array('absolute' => TRUE)); + } + else { + $claimed_id = $_REQUEST['openid_claimed_id']; + } + // Generate response containing the user's identity. The openid.sreg.xxx // entries contain profile data stored by the OpenID Provider (see OpenID // Simple Registration Extension 1.0). $response = variable_get('openid_test_response', array()) + array( 'openid.ns' => OPENID_NS_2_0, 'openid.mode' => 'id_res', - 'openid.op_endpoint' => $base_url . url('openid/provider'), - // openid.claimed_id is not sent by OpenID 1 clients. - 'openid.claimed_id' => isset($_REQUEST['openid_claimed_id']) ? $_REQUEST['openid_claimed_id'] : '', + 'openid.op_endpoint' => url('openid-test/endpoint', array('absolute' => TRUE)), + 'openid.claimed_id' => $claimed_id, 'openid.identity' => $_REQUEST['openid_identity'], 'openid.return_to' => $_REQUEST['openid_return_to'], 'openid.response_nonce' => $nonce, 'openid.assoc_handle' => 'openid-test', 'openid.signed' => 'op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle', ); // Sign the message using the MAC key that was exchanged during association. $association = new stdClass;