diff --git a/modules/openid/openid.api.php b/modules/openid/openid.api.php
index 11faa71..5e3d15d 100644
--- a/modules/openid/openid.api.php
+++ b/modules/openid/openid.api.php
@@ -49,8 +49,13 @@ function hook_openid_response($response, $account) {
  * Allow modules to declare OpenID discovery methods.
  *
  * The discovery function callbacks will be called in turn with an unique
- * parameter, the claimed identifier. They have to return an array of services,
- * in the same form returned by openid_discover().
+ * parameter, the claimed identifier. They have to return an associative array
+ * with array of services and claimed identifier in the same form as returned by
+ * openid_discover(). The resulting array must contain following keys:
+ *   - 'services' (required) an array of discovered services (including OpenID
+ *   version, endpoint URI, etc).
+ *   - 'claimed_id' (optional) new claimed identifer, found by following HTTP
+ *   redirects during the services discovery.
  *
  * The first discovery method that succeed (return at least one services) will
  * stop the discovery process.
@@ -58,6 +63,7 @@ function hook_openid_response($response, $account) {
  * @return
  *   An associative array which keys are the name of the discovery methods and
  *   values are function callbacks.
+ *
  * @see hook_openid_discovery_method_info_alter()
  */
 function hook_openid_discovery_method_info() {
diff --git a/modules/openid/openid.module b/modules/openid/openid.module
index e312866..65d83e0 100644
--- a/modules/openid/openid.module
+++ b/modules/openid/openid.module
@@ -250,28 +250,39 @@ function openid_login_validate($form, &$form_state) {
  *  - Create the authentication request.
  *  - Perform the appropriate redirect.
  *
- * @param $claimed_id The OpenID to authenticate
- * @param $return_to The endpoint to return to from the OpenID Provider
+ * @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');
 
+  $service = NULL;
   $claimed_id = openid_normalize($claimed_id);
+  $discovery = openid_discovery($claimed_id);
 
-  $services = openid_discovery($claimed_id);
-  $service = _openid_select_service($services);
+  if (!empty($discovery['services'])) {
+    $service = _openid_select_service($discovery['services']);
+  }
 
-  if (!$service) {
+  // Quit if the discovery result was empty or if we can't select any service.
+  if (!$discovery || !$service) {
     form_set_error('openid_identifier', t('Sorry, that is not a valid OpenID. Ensure you have spelled your ID correctly.'));
     return;
   }
 
+  // Set claimed id from discovery.
+  if (!empty($service['claimed_id'])) {
+    $claimed_id = $discovery['claimed_id'];
+  }
+
   // Store discovered information in the users' session so we don't have to rediscover.
   $_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.
+  // user_external_login later.
   $_SESSION['openid']['user_login_values'] = $form_values;
 
   // If a supported math library is present, then create an association.
@@ -306,10 +317,12 @@ function openid_begin($claimed_id, $return_to = '', $form_values = array()) {
  * Completes OpenID authentication by validating returned data from the OpenID
  * Provider.
  *
- * @param $response Array of returned values from the OpenID Provider.
+ * @param $response
+ *   Array of returned values from the OpenID Provider.
  *
- * @return $response Response values for further processing with
- *   $response['status'] set to one of 'success', 'failed' or 'cancel'.
+ * @return $response
+ *   Response values for further processing with $response['status'] set to one
+ *   of 'success', 'failed' or 'cancel'.
  */
 function openid_complete($response = array()) {
   module_load_include('inc', 'openid');
@@ -352,11 +365,13 @@ function openid_complete($response = array()) {
             // identififer to make sure that the provider is authorized to
             // respond on behalf of this.
             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'])) {
-                  $uris[] = $discovered_service['uri'];
+              $discovery = openid_discovery($response['openid.claimed_id']);
+              if ($discovery && !empty($discovery['services'])) {
+                $uris = array();
+                foreach ($discovery['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)) {
@@ -378,10 +393,21 @@ function openid_complete($response = array()) {
 /**
  * Perform discovery on a claimed ID to determine the OpenID provider endpoint.
  *
- * @param $claimed_id The OpenID URL to perform discovery on.
+ * Discovery methods are provided by the hook_openid_discovery_method_info and
+ * could be further altered using the hook_openid_discovery_method_info_alter.
+ *
+ * @param $claimed_id
+ *   The OpenID URL to perform discovery on.
  *
- * @return Array of services discovered (including OpenID version, endpoint
- * URI, etc).
+ * @return
+ *   The resulting discovery array from the first successful discovery method,
+ *   which must contain following keys:
+ *   - 'services' (required) an array of discovered services (including OpenID
+ *   version, endpoint URI, etc).
+ *   - 'claimed_id' (optional) new claimed identifer, found by following HTTP
+ *   redirects during the services discovery.
+ *   If all the discovery method fails or if no appropriate discovery method is
+ *   found, FALSE is returned.
  */
 function openid_discovery($claimed_id) {
   module_load_include('inc', 'openid');
@@ -389,15 +415,15 @@ function openid_discovery($claimed_id) {
   $methods = module_invoke_all('openid_discovery_method_info');
   drupal_alter('openid_discovery_method_info', $methods);
 
-  // Execute each method in turn.
+  // Execute each method in turn and return first successful discovery.
   foreach ($methods as $method) {
-    $discovered_services = $method($claimed_id);
-    if (!empty($discovered_services)) {
-      return $discovered_services;
+    $discovery = $method($claimed_id);
+    if (!empty($discovery)) {
+      return $discovery;
     }
   }
 
-  return array();
+  return FALSE;
 }
 
 /**
@@ -406,10 +432,10 @@ function openid_discovery($claimed_id) {
  * Define standard discovery methods.
  */
 function openid_openid_discovery_method_info() {
-  // The discovery process will stop as soon as one discovery method succeed.
+  // The discovery process will stop as soon as one discovery method succeeds.
   // We first attempt to discover XRI-based identifiers, then standard XRDS
-  // identifiers via Yadis and HTML-based discovery, conforming to the OpenID 2.0
-  // specification.
+  // identifiers via Yadis and HTML-based discovery, conforming to the
+  // OpenID 2.0 specification.
   return array(
     'xri' => '_openid_xri_discovery',
     'xrds' => '_openid_xrds_discovery',
@@ -421,24 +447,33 @@ function openid_openid_discovery_method_info() {
  *
  * @see http://openid.net/specs/openid-authentication-2_0.html#discovery
  * @see hook_openid_discovery_method_info()
+ * @see openid_discovery()
+ *
+ * @return
+ *   An array of discovered services and claimed identifier or NULL. See
+ *   openid_discovery() for more specific information.
  */
 function _openid_xri_discovery($claimed_id) {
   if (_openid_is_xri($claimed_id)) {
     // Resolve XRI using a proxy resolver (Extensible Resource Identifier (XRI)
     // Resolution Version 2.0, section 11.2 and 14.3).
     $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 $i => &$service) {
-      $status = $service['xrd']->children(OPENID_NS_XRD)->Status;
-      if ($status && $status->attributes()->cid == 'verified') {
-        $service['claimed_id'] = openid_normalize((string)$service['xrd']->children(OPENID_NS_XRD)->CanonicalID);
+    $discovery = _openid_xrds_discovery($xrds_url);
+    if (!empty($discovery['services']) && is_array($discovery['services'])) {
+      foreach ($discovery['services'] as $i => &$service) {
+        $status = $service['xrd']->children(OPENID_NS_XRD)->Status;
+        if ($status && $status->attributes()->cid == 'verified') {
+          $service['claimed_id'] = openid_normalize((string)$service['xrd']->children(OPENID_NS_XRD)->CanonicalID);
+        }
+        else {
+          // Ignore service if Canonical ID could not be verified
+          unset($discovery['services'][$i]);
+        }
       }
-      else {
-        // Ignore service if CanonicalID could not be verified.
-        unset($services[$i]);
+      if (!empty($discovery['services'])) {
+        return $discovery;
       }
     }
-    return $services;
   }
 }
 
@@ -447,6 +482,11 @@ function _openid_xri_discovery($claimed_id) {
  *
  * @see http://openid.net/specs/openid-authentication-2_0.html#discovery
  * @see hook_openid_discovery_method_info()
+ * @see openid_discovery()
+ *
+ * @return
+ *   An array of discovered services and claimed identifier or NULL. See
+ *   openid_discovery() for more specific information.
  */
 function _openid_xrds_discovery($claimed_id) {
   $services = array();
@@ -458,7 +498,18 @@ function _openid_xrds_discovery($claimed_id) {
     $headers = array('Accept' => 'application/xrds+xml');
     $result = drupal_http_request($xrds_url, array('headers' => $headers));
 
-    if (!isset($result->error)) {
+    // Check for HTTP error and make sure, that we reach the target. If the
+    // maximum allowed redirects are exhausted, final destination URL isn't
+    // reached, but drupal_http_request() doesn't return any error.
+    // @todo Remove the check for 200 HTTP result code after the following issue
+    // will be fixed: http://drupal.org/node/1096890.
+    if (!isset($result->error) && $result->code == 200) {
+
+      // Replace user entered claimed_id if we got redirect
+      if (!empty($result->redirect_url)) {
+        $claimed_id = openid_normalize($result->redirect_url);
+      }
+
       if (isset($result->headers['content-type']) && preg_match("/application\/xrds\+xml/", $result->headers['content-type'])) {
         // Parse XML document to find URL
         $services = _openid_xrds_parse($result->data);
@@ -504,7 +555,13 @@ function _openid_xrds_discovery($claimed_id) {
       }
     }
   }
-  return $services;
+
+  if (!empty($services)) {
+    return array(
+      'services' => $services,
+      'claimed_id' => $claimed_id,
+    );
+  }
 }
 
 /**
diff --git a/modules/openid/openid.test b/modules/openid/openid.test
index 09632ba..125f095 100644
--- a/modules/openid/openid.test
+++ b/modules/openid/openid.test
@@ -124,6 +124,28 @@ class OpenIDFunctionalTestCase extends OpenIDWebTestCase {
 
     // OpenID Authentication 2.0, section 7.3.3:
     $this->addIdentity(url('openid-test/html/openid2', array('absolute' => TRUE)), 2, 'http://example.com/html-openid2');
+
+    // OpenID Authentication 2.0, section 7.2.4:
+    // URL Identifiers MUST then be further normalized by both (1) following
+    // redirects when retrieving their content and finally (2) applying the
+    // rules in Section 6 of RFC3986 to the final destination URL. This final
+    // URL MUST be noted by the Relying Party as the Claimed Identifier and be
+    // used when requesting authentication.
+
+    // Single redirect
+    $identity = $expected_claimed_id = url('openid-test/redirected/yadis/xrds/1', array('absolute' => TRUE));
+    $this->addRedirectedIdentity($identity, 2, 'http://example.com/xrds', $expected_claimed_id, 0);
+
+    // Exact 3 redirects (default value for the 'max_redirects' option in
+    // drupal_http_request()).
+    $identity = $expected_claimed_id = url('openid-test/redirected/yadis/xrds/2', array('absolute' => TRUE));
+    $this->addRedirectedIdentity($identity, 2, 'http://example.com/xrds', $expected_claimed_id, 2);
+
+    // Fails because there are more than 3 redirects (default value for the
+    // 'max_redirects' option in drupal_http_request()).
+    $identity = url('openid-test/redirected/yadis/xrds/3', array('absolute' => TRUE));
+    $expected_claimed_id = FALSE;
+    $this->addRedirectedIdentity($identity, 2, 'http://example.com/xrds', $expected_claimed_id, 3);
   }
 
   /**
@@ -280,6 +302,41 @@ class OpenIDFunctionalTestCase extends OpenIDWebTestCase {
   }
 
   /**
+   * Add OpenID identity, changed by the following redirects, to user's profile.
+   *
+   * According to OpenID Authentication 2.0, section 7.2.4, URL Identifiers MUST
+   * be further normalized by following redirects when retrieving their content
+   * and this final URL MUST be noted by the Relying Party as the Claimed
+   * Identifier and be used when requesting authentication.
+   *
+   * @param $identity
+   *   The User-supplied Identifier.
+   * @param $version
+   *   The protocol version used by the service.
+   * @param $local_id
+   *   The expected OP-Local Identifier found during discovery.
+   * @param $claimed_id
+   *   The expected Claimed Identifier returned by the OpenID Provider, or FALSE
+   *   if the discovery is expected to fail.
+   * @param $redirects
+   *   The number of redirects.
+   */
+  function addRedirectedIdentity($identity, $version = 2, $local_id = 'http://example.com/xrds', $claimed_id = NULL, $redirects = 0) {
+    // Set the final destination URL which is the same as the Claimed
+    // Identifier, we insert the same identifier also to the provider response,
+    // but provider could futher change the Claimed ID actually (e.g. it could
+    // add unique fragment).
+    variable_set('openid_test_redirect_url', $identity);
+    variable_set('openid_test_response', array('openid.claimed_id' => $identity));
+
+    $this->addIdentity(url('openid-test/redirect/' . $redirects, array('absolute' => TRUE)), $version, $local_id, $claimed_id);
+
+    // Cleanup
+    variable_del('openid_test_redirect_url');
+    variable_del('openid_test_response');
+  }
+
+  /**
    * Tests that openid.signed is verified.
    */
   function testSignatureValidation() {
diff --git a/modules/openid/tests/openid_test.module b/modules/openid/tests/openid_test.module
index bad1184..629dcd3 100644
--- a/modules/openid/tests/openid_test.module
+++ b/modules/openid/tests/openid_test.module
@@ -60,6 +60,19 @@ function openid_test_menu() {
     'access callback' => TRUE,
     'type' => MENU_CALLBACK,
   );
+  $items['openid-test/redirect'] = array(
+    'title' => 'OpenID Provider Redirection Point',
+    'page callback' => 'openid_test_redirect',
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+  );
+  $items['openid-test/redirected/%/%'] = array(
+    'title' => 'OpenID Provider Final URL',
+    'page callback' => 'openid_test_redirected_method',
+    'page arguments' => array(2, 3),
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+  );
   return $items;
 }
 
@@ -213,6 +226,28 @@ function openid_test_endpoint() {
 }
 
 /**
+ * Menu callback; redirect during Normalization/Discovery.
+ */
+function openid_test_redirect($count = 0) {
+  if ($count == 0) {
+    $url = variable_get('openid_test_redirect_url', '');
+  }
+  else {
+    $url = url('openid-test/redirect/' . --$count, array('absolute' => TRUE));
+  }
+  $http_response_code = variable_get('openid_test_redirect_http_reponse_code', 301);
+  header('Location: ' . $url, TRUE, $http_response_code);
+  exit();
+}
+
+/**
+ * Menu callback; respond with appropriate callback.
+ */
+function openid_test_redirected_method($method1, $method2) {
+  return call_user_func('openid_test_' . $method1 . '_' . $method2);
+}
+
+/**
  * OpenID endpoint; handle "associate" requests (see OpenID Authentication 2.0,
  * section 8).
  *
