diff --git a/modules/openid/openid.inc b/modules/openid/openid.inc index 6945f34..1673b59 100644 --- a/modules/openid/openid.inc +++ b/modules/openid/openid.inc @@ -89,7 +89,7 @@ function openid_redirect_http($url, $message) { */ function openid_redirect($url, $message) { global $language; - + $output = '' . "\n"; $output .= '' . "\n"; $output .= "\n"; @@ -793,3 +793,77 @@ function _openid_math_powmod($x, $y, $z) { return bcpowmod($x, $y, $z); } } + +/** + * Provides transition for accounts with possibly invalid OpenID identifiers in authmap. + * + * This function provides less safe but less obtrusive procedure for users who + * cannot login with their OpenID identifiers. OpenID identifiers in the + * authmap could be incomplete due to invalid OpenID implementation in previous + * versions of Drupal (e.g. fragment part of the identifier could be missing). + * For more information see http://drupal.org/node/1120290. + * + * @param string $identity + * The user's claimed OpenID identifier. + * + * @return + * A fully-loaded user object if the user is found or FALSE if not found. + */ +function _openid_invalid_openid_transition($identity, $response) { + $account = FALSE; + $fallback_account = NULL; + $fallback_identity = $identity; + + // Step 1: Try to strip the fragment if it is present. + if (strpos($fallback_identity, '#') !== FALSE) { + $fallback_identity = preg_replace('/#.*/', '', $fallback_identity); + $fallback_account = user_external_load($fallback_identity); + } + + // Step 2: Try to replace https with http. OpenID providers often redirects + // from http to https, but Drupal didn't follow these redirects. + if (!$fallback_account && strpos($fallback_identity, 'https://') !== FALSE) { + $fallback_identity = str_replace('https://', 'http://', $fallback_identity); + $fallback_account = user_external_load($fallback_identity); + } + + // Step 3: Try to use original identifier. + if (!$fallback_account && isset($_SESSION['openid']['user_login_values']['openid_identifier'])) { + $fallback_identity = openid_normalize($_SESSION['openid']['user_login_values']['openid_identifier']); + $fallback_account = user_external_load($fallback_identity); + } + + if ($fallback_account) { + // Try to extract e-mail address from Simple Registration (SREG) or + // Attribute Exchanges (AX) keys. + $email = ''; + $sreg_values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg'); + $ax_values = openid_extract_namespace($response, OPENID_NS_AX, 'ax'); + if (!empty($sreg_values['email']) && valid_email_address($sreg_values['email'])) { + $email = $sreg_values['email']; + } + elseif ($ax_mail_values = openid_extract_ax_values($ax_values, array('http://axschema.org/contact/email', 'http://schema.openid.net/contact/email'))) { + $email = current($ax_mail_values); + } + + // If this e-mail address is the same as the e-mail address found in user + // account, login the user and update the claimed identifier. + if ($email && ($email == $fallback_account->mail || $email == $fallback_account->init)) { + $query = db_insert('authmap') + ->fields(array( + 'authname' => $identity, + 'uid' => $fallback_account->uid, + 'module' => 'openid', + )) + ->execute(); + drupal_set_message(t('New OpenID identifier %identity was added as a replacement for invalid identifier %invalid_identity. To finish the invalid OpenID transition process, please go to your OpenID identities page and remove the old identifier %invalid_identity.', array('%invalid_identity' => $fallback_identity, '%identity' => $identity, '@openid_url' => 'user/' . $fallback_account->uid . '/openid'))); + // Set the account to the found one. + $account = $fallback_account; + } + else { + drupal_set_message(t('There is already an existing account associated with the OpenID identifier that you have provided. However, due to the bug in the previous version of the authentication system, we can\'t be sure that this account belongs to you. If you are new on this site, please continue registering the new user account. If you already have a registered account on this site associated with the provided OpenID identifier, please try to reset the password or contact the site administrator.', array('@url_password' => 'user/password')), 'warning'); + } + } + + return $account; +} diff --git a/modules/openid/openid.module b/modules/openid/openid.module index 7673de8..a921b49 100644 --- a/modules/openid/openid.module +++ b/modules/openid/openid.module @@ -341,14 +341,17 @@ function openid_complete($response = array()) { $response['openid.claimed_id'] = $service['claimed_id']; } elseif ($service['version'] == 2) { - $response['openid.claimed_id'] = openid_normalize($response['openid.claimed_id']); + // Returned claimed id could have unique fragment hash to allow + // identifier recycling so we need to preserve it in response. + $response_claimed_id = openid_normalize($response['openid.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 on behalf of this. - if ($response['openid.claimed_id'] != $claimed_id) { - $services = openid_discovery($response['openid.claimed_id']); + if ($response_claimed_id != $claimed_id) { + $services = openid_discovery($response_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'])) { @@ -589,8 +592,15 @@ function openid_association($op_endpoint) { */ function openid_authentication($response) { $identity = $response['openid.claimed_id']; - $account = user_external_load($identity); + + // Tries to load user account if user_external_load fails due to possibly + // incompletely stored OpenID identifier in the authmap. + if (!isset($account->uid) && variable_get('openid_less_obtrusive_transition', FALSE)) { + module_load_include('inc', 'openid'); + $account = _openid_invalid_openid_transition($identity, $response); + } + if (isset($account->uid)) { if (!variable_get('user_email_verification', TRUE) || $account->login) { // Check if user is blocked. @@ -634,7 +644,7 @@ function openid_authentication($response) { drupal_set_message(t('Account registration using the information provided by your OpenID provider failed due to the reasons listed below. Complete the registration by filling out the form below. If you already have an account, you can log in now and add your OpenID under "My account".', array('@login' => url('user/login'))), 'warning'); // Append form validation errors below the above warning. foreach ($messages['error'] as $message) { - drupal_set_message( $message, 'error'); + drupal_set_message($message, 'error'); } } diff --git a/modules/openid/openid.test b/modules/openid/openid.test index 202a835..7286a7f 100644 --- a/modules/openid/openid.test +++ b/modules/openid/openid.test @@ -89,12 +89,12 @@ class OpenIDFunctionalTestCase extends OpenIDWebTestCase { // 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. - $identity = url('openid-test/yadis/xrds/dummy-user', array('absolute' => TRUE)); - // Tell openid_test.module to respond with this identifier. The URL scheme - // is stripped in order to test that the returned identifier is normalized in - // openid_complete(). - variable_set('openid_test_response', array('openid.claimed_id' => preg_replace('@^https?://@', '', $identity))); + // Identifier. The OpenID Provider responds with the actual identifier + // including fragment. + $identity = url('openid-test/yadis/xrds/dummy-user', array('absolute' => TRUE, 'fragment' => $this->randomName())); + // Tell openid_test.module to respond with this identifier. We test if + // openid_complete() process it right. + variable_set('openid_test_response', array('openid.claimed_id' => $identity)); $this->addIdentity(url('openid-test/yadis/xrds/server', array('absolute' => TRUE)), 2, 'http://specs.openid.net/auth/2.0/identifier_select', $identity); variable_set('openid_test_response', array()); @@ -479,6 +479,103 @@ class OpenIDRegistrationTestCase extends OpenIDWebTestCase { } /** + * Test account registration using Simple Registration and Attribute Exchange. + */ +class OpenIDInvalidIdentifierTransitionTestCase extends OpenIDFunctionalTestCase { + + public static function getInfo() { + return array( + 'name' => 'OpenID account update', + 'description' => 'Tries to correct OpenID identifiers attached to accounts if their identifiers were stripped.', + 'group' => 'OpenID', + ); + } + + function setUp() { + parent::setUp('openid', 'openid_test'); + variable_set('user_register', USER_REGISTER_VISITORS); + variable_set('openid_less_obtrusive_transition', TRUE); + } + + /** + * Test OpenID transition with e-mail mismatch. + */ + function testStrippedFragmentAccountEmailMismatch() { + $this->drupalLogin($this->web_user); + + // Use a User-supplied Identity that is the URL of an XRDS document. + $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE, 'fragment' => $this->randomName())); + $identity_stripped = preg_replace('/#.*/', '', $identity); + + // Add invalid identifier to the authmap (identifier has stripped fragment). + $this->addIdentity($identity_stripped); + + $this->drupalLogout(); + + // Test logging in via the login block on the front page. + $this->submitLoginForm($identity); + $this->assertLink(t('Log out'), 0, t('User was logged in.')); + + $this->drupalLogout(); + + // Test logging in via the login form, provider will respond with full + // identifier (including fragment) but with different email, so we can't + // provide auto-update. + variable_set('openid_test_response', array( + 'openid.claimed_id' => $identity, + 'openid.sreg.nickname' => $this->web_user->name, + 'openid.sreg.email' => 'invalid-' . $this->web_user->mail)); + + $edit = array('openid_identifier' => $identity_stripped); + $this->submitLoginForm($identity_stripped); + + // Verify user was redirected away from user login to an accessible page. + $this->assertResponse(200); + + // Verify the message. + $this->assertRaw(t('There is already an existing account associated with the OpenID identifier that you have provided.'), t('Message that OpenID identifier must be updated manually was displayed.')); + } + + /** + * Test OpenID auto transition with e-mail. + */ + function testStrippedFragmentAccountAutoUpdateSreg() { + $this->drupalLogin($this->web_user); + + // Use a User-supplied Identity that is the URL of an XRDS document. + $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE, 'fragment' => $this->randomName())); + $identity_stripped = preg_replace('/#.*/', '', $identity); + + // Add invalid identifier to the authmap (identifier has stripped fragment). + $this->addIdentity($identity_stripped); + + $this->drupalLogout(); + + // Test logging in via the login block on the front page. + $this->submitLoginForm($identity); + $this->assertLink(t('Log out'), 0, t('User was logged in.')); + + $this->drupalLogout(); + + // Test logging in via the login form, provider will respond with full + // identifier (including fragment) but with different email, so we can't + // provide auto-update. + variable_set('openid_test_response', array( + 'openid.claimed_id' => $identity, + 'openid.sreg.nickname' => $this->web_user->name, + 'openid.sreg.email' => $this->web_user->mail)); + + $this->submitLoginForm($identity_stripped); + + // Verify user was redirected away from user login to an accessible page. + $this->assertResponse(200); + + // Verify the message. + $this->assertRaw(t('New OpenID identifier %identity was added as a replacement for invalid identifier %invalid_identity.', array('%invalid_identity' => $identity_stripped, '%identity' => $identity)), t('Message that OpenID identifier was added automatically was displayed.')); + } +} + +/** * Test internal helper functions. */ class OpenIDUnitTest extends DrupalWebTestCase { diff --git a/modules/openid/tests/openid_test.module b/modules/openid/tests/openid_test.module index bad1184..6440e6b 100644 --- a/modules/openid/tests/openid_test.module +++ b/modules/openid/tests/openid_test.module @@ -60,6 +60,12 @@ function openid_test_menu() { 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); + $items['openid-test/redirect'] = array( + 'title' => 'OpenID Redirect', + 'page callback' => 'openid_test_redirect', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); return $items; } diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 0472f02..e8d9398 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -442,3 +442,29 @@ ini_set('session.cookie_lifetime', 2000000); * Remove the leading hash signs to disable. */ # $conf['allow_authorize_operations'] = FALSE; + +/** + * Automatic invalid OpenID identifiers transition: + * + * A less obtrusive but also less secure (and optional) method of invalid OpenID + * identifier transition. Due to the bug in the OpenID module, wrong identifiers + * were being saved to the authmap table (affects only particular OpenID + * providers). + * + * If enabled, Drupal will try to find matching account from the user database + * using the alternative OpenID identifier matching when the standard OpenID + * login failed. If the found site user account and the OpenID account have + * identical e-mail address, Drupal will assign the identifier to the existing + * user account and login user to the site. + * + * If disabled or when e-mail addresses don't match, user must login to the site + * using other method, e.g. using ordinary user login or password reset for the + * one-time temporary login. After login, user could fix the account by himself + * by re-adding his OpenID identifier and deleting the invalid one. For more + * information, see http://drupal.org/node/1120290. + * + * Enable only if you upgrade from Drupal 6.x or Drupal 7.8 and below! + * + * Remove the leading hash sign to enable. + */ +# $conf['openid_less_obtrusive_transition'] = TRUE;