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;