Commit 0d9eedc8 authored by Dries's avatar Dries

- Patch #727650 by Damien Tournoud: made OpenID more pluggable so we can...

- Patch #727650 by Damien Tournoud: made OpenID more pluggable so we can extend it in contrib, and added support for Google's OpenID discovery protocol.
parent 094a6b4f
......@@ -99,6 +99,11 @@ Drupal 7.0, xxxx-xx-xx (development version)
at the operating system level.
* Removed per-user themes: Contributed modules with similar functionality
are available.
- OpenID:
* Added support for Gmail and Google Apps for Domain identifiers. Users can
now login with their user@domain.com identifier when domain.com is powered
by Google.
* Made the OpenID module more pluggable.
- Added code registry:
* Using the registry, modules declare their includable files via their .info file,
allowing Drupal to lazy-load classes and interfaces as needed.
......
......@@ -46,6 +46,66 @@ 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().
*
* The first discovery method that succeed (return at least one services) will
* stop the discovery process.
*
* @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() {
return array(
'new_discovery_idea' => '_my_discovery_method',
);
}
/**
* Allow modules to alter discovery methods.
*/
function hook_openid_discovery_method_info_alter(&$methods) {
// Remove Google discovery scheme.
unset($methods['google']);
}
/**
* Allow modules to declare OpenID normalization methods.
*
* The discovery function callbacks will be called in turn with an unique
* parameter, the identifier to normalize. They have to return a normalized
* identifier, or NULL if the identifier is not in a form they can handle.
*
* The first normalization method that succeed (return a value that is not NULL)
* will stop the normalization process.
*
* @return
* An array with a set of function callbacks, that will be called in turn
* when normalizing an OpenID identifier. The normalization functions have
* to return a normalized identifier, or NULL if the identifier is not in
* a form they can handle.
* @see hook_openid_normalization_method_info_alter()
*/
function hook_openid_normalization_method_info() {
return array(
'new_normalization_idea' => '_my_normalization_method',
);
}
/**
* Allow modules to alter normalization methods.
*/
function hook_openid_normalization_method_info_alter(&$methods) {
// Remove Google IDP normalization.
unset($methods['google_idp']);
}
/**
* @} End of "addtogroup hooks".
*/
......@@ -182,24 +182,39 @@ function _openid_is_xri($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);
function openid_normalize($identifier) {
$methods = module_invoke_all('openid_normalization_method_info');
drupal_alter('openid_normalization_method_info', $methods);
// Execute each method in turn, stopping after the first method accepted
// the identifier.
foreach ($methods as $method) {
$result = $method($identifier);
if ($result !== NULL) {
$identifier = $result;
break;
}
}
return $identifier;
}
function _openid_normalize_xri($xri) {
$normalized_xri = $xri;
if (stristr($xri, 'xri://') !== FALSE) {
$normalized_xri = substr($xri, 6);
/**
* OpenID normalization method: normalize XRI identifiers.
*/
function _openid_xri_normalize($identifier) {
if (_openid_is_xri($identifier)) {
if (stristr($identifier, 'xri://') !== FALSE) {
$identifier = substr($identifier, 6);
}
return $identifier;
}
return $normalized_xri;
}
function _openid_normalize_url($url) {
/**
* OpenID normalization method: normalize URL identifiers.
*/
function _openid_url_normalize($url) {
$normalized_url = $url;
if (stristr($url, '://') === FALSE) {
......@@ -216,6 +231,34 @@ function _openid_normalize_url($url) {
return $normalized_url;
}
/**
* OpenID normalization method: Normalize Google identifiers.
*
* This transforms a Google identifier (user@domain) into an XRDS URL.
*
* @see http://sites.google.com/site/oauthgoog/fedlogininterp/openiddiscovery#TOC-IdP-Discovery
*/
function _openid_google_idp_normalize($identifier) {
if (!valid_email_address($identifier)) {
return;
}
// If the identifier is a valid email address, try to discover the domain
// with Google Federated Login. We only use the generic URL, because the
// domain-specific URL (http://example.com/.well-known/host-meta) cannot
// be trusted.
list($name, $domain) = explode('@', $identifier, 2);
$response = drupal_http_request('https://www.google.com/accounts/o8/.well-known/host-meta?hd=' . rawurlencode($domain));
if (isset($response->error) || $response->code != 200) {
return;
}
if (preg_match('/Link: <(.*)>/', $response->data, $matches)) {
$xrds_url = $matches[1];
return $xrds_url;
}
}
/**
* Create a serialized message packet as per spec: $key:$value\n .
*/
......
......@@ -179,7 +179,7 @@ function openid_login_validate($form, &$form_state) {
function openid_begin($claimed_id, $return_to = '', $form_values = array()) {
module_load_include('inc', 'openid');
$claimed_id = _openid_normalize($claimed_id);
$claimed_id = openid_normalize($claimed_id);
$services = openid_discovery($claimed_id);
$service = _openid_select_service($services);
......@@ -263,7 +263,7 @@ function openid_complete($response = array()) {
// 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)) {
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) {
......@@ -298,12 +298,61 @@ function openid_discovery($claimed_id) {
module_load_include('inc', 'openid');
module_load_include('inc', 'openid', 'xrds');
$services = array();
$methods = module_invoke_all('openid_discovery_method_info');
drupal_alter('openid_discovery_method_info', $methods);
$xrds_url = $claimed_id;
// Execute each method in turn.
foreach ($methods as $method) {
$discovered_services = $method($claimed_id);
if (!empty($discovered_services)) {
return $discovered_services;
}
}
return array();
}
/**
* Implementation of hook_openid_discovery_method_info().
*
* Define standard discovery methods.
*/
function openid_openid_discovery_method_info() {
// The discovery process will stop as soon as one discovery method succeed.
// 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. If those fail, we attempt to discover services based on
// the Google user discovery scheme.
return array(
'xri' => '_openid_xri_discovery',
'xrds' => '_openid_xrds_discovery',
'google' => '_openid_google_user_discovery',
);
}
/**
* OpenID discovery method: perform an XRI discovery.
*
* @see http://openid.net/specs/openid-authentication-2_0.html#discovery
* @see hook_openid_discovery_method_info()
*/
function _openid_xri_discovery($claimed_id) {
if (_openid_is_xri($claimed_id)) {
$xrds_url = 'http://xri.net/' . $claimed_id;
_openid_xrds_discovery($xrds_url);
}
}
/**
* OpenID discovery method: perform a XRDS discovery.
*
* @see http://openid.net/specs/openid-authentication-2_0.html#discovery
* @see hook_openid_discovery_method_info()
*/
function _openid_xrds_discovery($claimed_id) {
$services = array();
$xrds_url = $claimed_id;
$scheme = @parse_url($xrds_url, PHP_URL_SCHEME);
if ($scheme == 'http' || $scheme == 'https') {
// For regular URLs, try Yadis resolution first, then HTML-based discovery
......@@ -359,6 +408,57 @@ function openid_discovery($claimed_id) {
return $services;
}
/**
* OpenID discovery method: Perform an user discovery using Google Discovery protocol.
*
* This transforms a OpenID identifier into an OpenID endpoint.
*
* @see http://sites.google.com/site/oauthgoog/fedlogininterp/openiddiscovery#TOC-User-Discovery
* @see hook_openid_discovery_method_info()
*/
function _openid_google_user_discovery($claimed_id) {
$xrds_url = $claimed_id;
$url = @parse_url($xrds_url);
if (empty($url['scheme']) || ($url['scheme'] != 'http' && $scheme['scheme'] != 'https') || empty($url['host'])) {
return;
}
$response = drupal_http_request('https://www.google.com/accounts/o8/.well-known/host-meta?hd=' . rawurlencode($url['host']));
if (isset($response->error) || $response->code != 200) {
return;
}
if (preg_match('/Link: <(.*)>/', $response->data, $matches)) {
$xrds_url = $matches[1];
$services = _openid_xrds_discovery($xrds_url);
foreach ($services as $service) {
if (in_array('http://www.iana.org/assignments/relation/describedby', $service['types']) && !empty($service['additional']['URITEMPLATE'])) {
$template = $service['additional']['URITEMPLATE'];
$xrds_url = str_replace('{%uri}', rawurlencode($claimed_id), $template);
return _openid_xrds_discovery($xrds_url);
}
}
}
}
/**
* Implementation of hook_openid_normalization_method_info().
*
* Define standard normalization methods.
*/
function openid_openid_normalization_method_info() {
// We first try to normalize Google Identifiers (user@domain) into their
// corresponding XRDS URL. If this fail, we proceed with standard OpenID
// normalization by normalizing XRI idenfiers. Finally, normalize the identifier
// into a canonical URL.
return array(
'google_idp' => '_openid_google_idp_normalize',
'xri' => '_openid_xri_normalize',
'url' => '_openid_url_normalize',
);
}
/**
* Attempt to create a shared secret with the OpenID Provider.
*
......
......@@ -80,7 +80,7 @@ function openid_user_add() {
function openid_user_add_validate($form, &$form_state) {
// Check for existing entries.
$claimed_id = _openid_normalize($form_state['values']['openid_identifier']);
$claimed_id = openid_normalize($form_state['values']['openid_identifier']);
if (db_query("SELECT authname FROM {authmap} WHERE authname = :authname", (array(':authname' => $claimed_id)))->fetchField()) {
form_set_error('openid_identifier', t('That OpenID is already in use on this site.'));
}
......
......@@ -445,22 +445,33 @@ class OpenIDUnitTest extends DrupalWebTestCase {
}
/**
* Test _openid_normalize().
* Test openid_normalize().
*/
function testOpenidNormalize() {
// Test that the normalization is according to OpenID Authentication 2.0,
// section 7.2 and 11.5.2.
$this->assertEqual(_openid_normalize('$foo'), '$foo', t('_openid_normalize() correctly normalized an XRI.'));
$this->assertEqual(_openid_normalize('xri://$foo'), '$foo', t('_openid_normalize() correctly normalized an XRI with an xri:// scheme.'));
$this->assertEqual(openid_normalize('$foo'), '$foo', t('openid_normalize() correctly normalized an XRI.'));
$this->assertEqual(openid_normalize('xri://$foo'), '$foo', t('openid_normalize() correctly normalized an XRI with an xri:// scheme.'));
$this->assertEqual(_openid_normalize('example.com/'), 'http://example.com/', t('_openid_normalize() correctly normalized a URL with a missing scheme.'));
$this->assertEqual(_openid_normalize('example.com'), 'http://example.com/', t('_openid_normalize() correctly normalized a URL with a missing scheme and empty path.'));
$this->assertEqual(_openid_normalize('http://example.com'), 'http://example.com/', t('_openid_normalize() correctly normalized a URL with an empty path.'));
$this->assertEqual(openid_normalize('example.com/'), 'http://example.com/', t('openid_normalize() correctly normalized a URL with a missing scheme.'));
$this->assertEqual(openid_normalize('example.com'), 'http://example.com/', t('openid_normalize() correctly normalized a URL with a missing scheme and empty path.'));
$this->assertEqual(openid_normalize('http://example.com'), 'http://example.com/', t('openid_normalize() correctly normalized a URL with an empty path.'));
$this->assertEqual(_openid_normalize('http://example.com/path'), 'http://example.com/path', t('_openid_normalize() correctly normalized a URL with a path.'));
$this->assertEqual(openid_normalize('http://example.com/path'), 'http://example.com/path', t('openid_normalize() correctly normalized a URL with a path.'));
$this->assertEqual(_openid_normalize('http://example.com/path#fragment'), 'http://example.com/path', t('_openid_normalize() correctly normalized a URL with a fragment.'));
$this->assertEqual(openid_normalize('http://example.com/path#fragment'), 'http://example.com/path', t('openid_normalize() correctly normalized a URL with a fragment.'));
}
/**
* Test _openid_google_idp_normalize().
*/
function testGoogleIdpNormalize() {
// We consider that Gmail will always be Gmail.
$this->assertTrue(valid_url(_openid_google_idp_normalize('testuser@gmail.com'), TRUE), t('_openid_google_idp_normalize() correctly normalized a Google Gmail identifier.'));
// This is a test domain documented on http://sites.google.com/site/oauthgoog/fedlogininterp/saml-idp.
$this->assertTrue(valid_url(_openid_google_idp_normalize('test@lso-test-domain.com'), TRUE), t('_openid_google_idp_normalize() correctly normalized a Google Apps for Domain identifier.'));
// We consider that microsoft.com will never be hosted by Google.
$this->assertFalse(valid_url(_openid_google_idp_normalize('test@microsoft.com'), TRUE), t("_openid_google_idp_normalize() didn't normalized an identifier for a domain that is not Google-enabled."));
}
}
......@@ -71,6 +71,11 @@ function _xrds_cdata(&$parser, $data) {
case 'XRDS/XRD/SERVICE/LOCALID':
$xrds_current_service['localid'] = $data;
break;
default:
if (preg_match('@^XRDS/XRD/SERVICE/(.*)$@', $path, $matches)) {
$xrds_current_service['additional'][$matches[1]] = $data;
}
break;
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment