Commit e8d18e41 authored by Dries's avatar Dries

- Patch #218097 by c960657: OpenID must use canonical ID when authenticating XRI i-names.

parent 2598778e
......@@ -60,6 +60,16 @@
*/
define('OPENID_NS_AX', 'http://openid.net/srv/ax/1.0');
/**
* Extensible Resource Descriptor documents.
*/
define('OPENID_NS_XRD', 'xri://$xrd*($v*2.0)');
/**
* OpenID IDP for Google hosted domains.
*/
define('OPENID_NS_GOOGLE', 'http://namespace.google.com/openid/xmlns');
/**
* Performs an HTTP 302 redirect (for the 1.x protocol).
*/
......@@ -108,6 +118,49 @@ function openid_redirect_form($form, &$form_state, $url, $message) {
return $form;
}
/**
* Parse an XRDS document.
*
* @param $raw_xml
* A string containing the XRDS document.
* @return
* An array of service entries.
*/
function _openid_xrds_parse($raw_xml) {
$services = array();
try {
$xml = @new SimpleXMLElement($raw_xml);
foreach ($xml->children(OPENID_NS_XRD)->XRD as $xrd) {
foreach ($xrd->children(OPENID_NS_XRD)->Service as $service_element) {
$service = array(
'priority' => $service_element->attributes()->priority ? (int)$service_element->attributes()->priority : PHP_INT_MAX,
'types' => array(),
'uri' => (string)$service_element->children(OPENID_NS_XRD)->URI,
'service' => $service_element,
'xrd' => $xrd,
);
foreach ($service_element->Type as $type) {
$service['types'][] = (string)$type;
}
if ($service_element->children(OPENID_NS_XRD)->Delegate) {
$service['identity'] = (string)$service_element->children(OPENID_NS_XRD)->Delegate;
}
if ($service_element->children(OPENID_NS_XRD)->LocalID) {
$service['identity'] = (string)$service_element->children(OPENID_NS_XRD)->LocalID;
}
else {
$service['identity'] = FALSE;
}
$services[] = $service;
}
}
}
catch (Exception $e) {
// Invalid XML.
}
return $services;
}
/**
* Select a service element.
*
......@@ -155,6 +208,12 @@ function _openid_select_service(array $services) {
}
}
if ($selected_service) {
// Unset SimpleXMLElement instances that cannot be saved in $_SESSION.
unset($selected_service['xrd']);
unset($selected_service['service']);
}
return $selected_service;
}
......
......@@ -208,16 +208,12 @@ function openid_begin($claimed_id, $return_to = '', $form_values = array()) {
$claimed_id = $identity = 'http://specs.openid.net/auth/2.0/identifier_select';
}
else {
// Look for OP-Local Identifier.
if (!empty($service['localid'])) {
$identity = $service['localid'];
}
elseif (!empty($service['delegate'])) {
$identity = $service['delegate'];
}
else {
$identity = $claimed_id;
// Use Claimed ID and/or OP-Local Identifier from service description, if
// available.
if (!empty($service['claimed_id'])) {
$claimed_id = $service['claimed_id'];
}
$identity = !empty($service['identity']) ? $service['identity'] : $claimed_id;
}
$request = openid_authentication_request($claimed_id, $identity, $return_to, $assoc_handle, $service);
......@@ -258,12 +254,21 @@ function openid_complete($response = array()) {
}
else {
if (openid_verify_assertion($service['uri'], $response)) {
// OpenID Authentication, section 7.3.2.3 and Appendix A.5:
// The CanonicalID specified in the XRDS document must be used as the
// account key. We rely on the XRI proxy resolver to verify that the
// provider is authorized to respond on behalf of the specified
// identifer (required per Extensible Resource Identifier (XRI)
// (XRI) Resolution Version 2.0, section 14.3):
if (!empty($service['claimed_id'])) {
$response['openid.claimed_id'] = $service['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
// identifier 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)) {
elseif ($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) {
......@@ -338,8 +343,14 @@ function openid_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);
// Resolve XRI using a proxy resolver (Extensible Resource Identifier (XRI)
// Resolution Version 2.0, section 11.2).
$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 &$service) {
$service['claimed_id'] = openid_normalize((string)$service['xrd']->children(OPENID_NS_XRD)->CanonicalID);
}
return $services;
}
}
......@@ -362,7 +373,7 @@ function _openid_xrds_discovery($claimed_id) {
if (!isset($result->error)) {
if (isset($result->headers['Content-Type']) && preg_match("/application\/xrds\+xml/", $result->headers['Content-Type'])) {
// Parse XML document to find URL
$services = xrds_parse($result->data);
$services = _openid_xrds_parse($result->data);
}
else {
$xrds_url = NULL;
......@@ -377,7 +388,7 @@ function _openid_xrds_discovery($claimed_id) {
$headers = array('Accept' => 'application/xrds+xml');
$xrds_result = drupal_http_request($xrds_url, array('headers' => $headers));
if (!isset($xrds_result->error)) {
$services = xrds_parse($xrds_result->data);
$services = _openid_xrds_parse($xrds_result->data);
}
}
}
......@@ -386,19 +397,19 @@ function _openid_xrds_discovery($claimed_id) {
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);
$identity = _openid_link_href('openid2.local_id', $result->data);
$type = 'http://specs.openid.net/auth/2.0/signon';
// 1.x links
if (empty($uri)) {
$uri = _openid_link_href('openid.server', $result->data);
$delegate = _openid_link_href('openid.delegate', $result->data);
$identity = _openid_link_href('openid.delegate', $result->data);
$type = 'http://openid.net/signon/1.1';
}
if (!empty($uri)) {
$services[] = array(
'uri' => $uri,
'delegate' => $delegate,
'identity' => $identity,
'types' => array($type),
);
}
......@@ -432,9 +443,9 @@ function _openid_google_user_discovery($claimed_id) {
$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'];
foreach ($services as $i => $service) {
if (in_array('http://www.iana.org/assignments/relation/describedby', $service['types']) && $service['service']->children(OPENID_NS_GOOGLE)->URITemplate) {
$template = (string)$service['service']->children(OPENID_NS_GOOGLE)->URITemplate;
$xrds_url = str_replace('{%uri}', rawurlencode($claimed_id), $template);
return _openid_xrds_discovery($xrds_url);
}
......
......@@ -59,6 +59,9 @@ class OpenIDFunctionalTest extends DrupalWebTestCase {
// element that contains the URL of an XRDS document.
$this->addIdentity(url('openid-test/yadis/http-equiv', array('absolute' => TRUE)), 2);
// Identifier is an XRI. Resolve using our own dummy proxy resolver.
variable_set('xri_proxy_resolver', url('openid-test/yadis/xrds/xri', array('absolute' => TRUE)) . '/');
$this->addIdentity('@example*résumé;%25', 2, 'http://example.com/user');
// HTML-based discovery:
// If the User-supplied Identifier is a URL of an HTML page, the page may
......
......@@ -69,10 +69,29 @@ function openid_test_menu() {
*/
function openid_test_yadis_xrds() {
if ($_SERVER['HTTP_ACCEPT'] == 'application/xrds+xml') {
// Only respond to XRI requests for one specific XRI. The is used to verify
// that the XRI has been properly encoded. The "+" sign in the _xrd_r query
// parameter is decoded to a space by PHP.
if (arg(3) == 'xri') {
if (variable_get('clean_url', 0)) {
if (arg(4) != '@example*résumé;%25' || $_GET['_xrd_r'] != 'application/xrds xml') {
drupal_not_found();
}
}
else {
// Drupal cannot properly emulate an XRI proxy resolver using unclean
// URLs, so the arguments gets messed up.
if (arg(4) . '/' . arg(5) != '@example*résumé;%25?_xrd_r=application/xrds xml') {
drupal_not_found();
}
}
}
drupal_add_http_header('Content-Type', 'application/xrds+xml');
print '<?xml version="1.0" encoding="UTF-8"?>
<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">
<XRD>
<ProviderID>xri://@</ProviderID>
<CanonicalID>http://example.com/user</CanonicalID>
<Service>
<Type>http://example.com/this-is-ignored</Type>
</Service>
......@@ -102,7 +121,7 @@ function openid_test_yadis_xrds() {
</Service>';
}
print '
<XRD>
</XRD>
</xrds:XRDS>';
}
else {
......
<?php
// $Id$
// Global variables to track parsing state
$xrds_open_elements = array();
$xrds_services = array();
$xrds_current_service = array();
/**
* Main entry point for parsing XRDS documents
*/
function xrds_parse($xml) {
global $xrds_services;
$parser = xml_parser_create_ns();
xml_set_element_handler($parser, '_xrds_element_start', '_xrds_element_end');
xml_set_character_data_handler($parser, '_xrds_cdata');
xml_parse($parser, $xml);
xml_parser_free($parser);
return $xrds_services;
}
/**
* Parser callback functions
*/
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 (!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) {
case 'XRDS/XRD/SERVICE/TYPE':
$xrds_current_service['types'][] = $data;
break;
case 'XRDS/XRD/SERVICE/URI':
$xrds_current_service['uri'] = $data;
break;
case 'XRDS/XRD/SERVICE/DELEGATE':
$xrds_current_service['delegate'] = $data;
break;
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;
}
}
function _xrds_strip_namespace($name) {
// Strip namespacing.
$pos = strrpos($name, ':');
if ($pos !== FALSE) {
$name = substr($name, $pos + 1, strlen($name));
}
return $name;
}
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