Issue #575810 by wojtha, Heine, vzima: Fixed OpenID discovery spec violation - follow redirects.

8.0.x
webchick 2011-09-05 12:05:36 -07:00
parent 446031031f
commit 7379f95ee5
4 changed files with 180 additions and 29 deletions

View File

@ -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() {

View File

@ -256,16 +256,25 @@ function openid_login_validate($form, &$form_state) {
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($discovery['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
@ -352,11 +361,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 +389,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.
*
* @return Array of services discovered (including OpenID version, endpoint
* URI, etc).
* @param $claimed_id
* The OpenID URL to perform discovery on.
*
* @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 +411,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;
}
/**
@ -421,24 +443,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 the 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 +478,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 +494,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 the user-entered claimed_id if we received a 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 +551,13 @@ function _openid_xrds_discovery($claimed_id) {
}
}
}
return $services;
if (!empty($services)) {
return array(
'services' => $services,
'claimed_id' => $claimed_id,
);
}
}
/**

View File

@ -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);
}
/**
@ -279,6 +301,41 @@ class OpenIDFunctionalTestCase extends OpenIDWebTestCase {
$this->assertRaw(t('Successfully added %identity', array('%identity' => $claimed_id)), t('Identity %identity was added.', array('%identity' => $identity)));
}
/**
* 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);
// Clean up.
variable_del('openid_test_redirect_url');
variable_del('openid_test_response');
}
/**
* Tests that openid.signed is verified.
*/

View File

@ -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;
}
@ -212,6 +225,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).