diff --git a/lib/Auth/OpenID.php b/lib/Auth/OpenID.php index 551dee69b8d22bed9a37176af094b424d0b069b5..6556b5b01e8bb43ef315991932b370ce30535bdf 100644 --- a/lib/Auth/OpenID.php +++ b/lib/Auth/OpenID.php @@ -13,16 +13,22 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ +/** + * The library version string + */ +define('Auth_OpenID_VERSION', '2.1.2'); + /** * Require the fetcher code. */ require_once "Auth/Yadis/PlainHTTPFetcher.php"; require_once "Auth/Yadis/ParanoidHTTPFetcher.php"; require_once "Auth/OpenID/BigMath.php"; +require_once "Auth/OpenID/URINorm.php"; /** * Status code returned by the server when the only option is to show @@ -97,7 +103,7 @@ define('Auth_OpenID_punct', "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"); if (Auth_OpenID_getMathLib() === null) { - define('Auth_OpenID_NO_MATH_SUPPORT', true); + Auth_OpenID_setNoMathSupport(); } /** @@ -137,20 +143,42 @@ class Auth_OpenID { */ function getQuery($query_str=null) { + $data = array(); + if ($query_str !== null) { - $str = $query_str; - } else if ($_SERVER['REQUEST_METHOD'] == 'GET') { - $str = $_SERVER['QUERY_STRING']; - } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $data = Auth_OpenID::params_from_string($query_str); + } else if (!array_key_exists('REQUEST_METHOD', $_SERVER)) { + // Do nothing. + } else { + // XXX HACK FIXME HORRIBLE. + // + // POSTing to a URL with query parameters is acceptable, but + // we don't have a clean way to distinguish those parameters + // when we need to do things like return_to verification + // which only want to look at one kind of parameter. We're + // going to emulate the behavior of some other environments + // by defaulting to GET and overwriting with POST if POST + // data is available. + $data = Auth_OpenID::params_from_string($_SERVER['QUERY_STRING']); + + if ($_SERVER['REQUEST_METHOD'] == 'POST') { $str = file_get_contents('php://input'); if ($str === false) { - return array(); + $post = array(); + } else { + $post = Auth_OpenID::params_from_string($str); } - } else { - return array(); + + $data = array_merge($data, $post); + } } + return $data; + } + + function params_from_string($str) + { $chunks = explode("&", $str); $data = array(); @@ -180,11 +208,14 @@ class Auth_OpenID { if (is_dir($dir_name) || @mkdir($dir_name)) { return true; } else { - if (Auth_OpenID::ensureDir(dirname($dir_name))) { - return is_dir($dir_name) || @mkdir($dir_name); - } else { - return false; + $parent_dir = dirname($dir_name); + + // Terminal case; there is no parent directory to create. + if ($parent_dir == $dir_name) { + return true; } + + return (Auth_OpenID::ensureDir($parent_dir) && @mkdir($dir_name)); } } @@ -321,38 +352,6 @@ class Auth_OpenID { return $url . $sep . Auth_OpenID::httpBuildQuery($args); } - /** - * Turn a string into an ASCII string. - * - * Replace non-ascii characters with a %-encoded, UTF-8 - * encoding. This function will fail if the input is a string and - * there are non-7-bit-safe characters. It is assumed that the - * caller will have already translated the input into a Unicode - * character sequence, according to the encoding of the HTTP POST - * or GET. - * - * Do not escape anything that is already 7-bit safe, so we do the - * minimal transform on the identity URL - * - * @access private - */ - function quoteMinimal($s) - { - $res = array(); - for ($i = 0; $i < strlen($s); $i++) { - $c = $s[$i]; - if ($c >= "\x80") { - for ($j = 0; $j < count(utf8_encode($c)); $j++) { - array_push($res, sprintf("%02X", ord($c[$j]))); - } - } else { - array_push($res, $c); - } - } - - return implode('', $res); - } - /** * Implements python's urlunparse, which is not available in PHP. * Given the specified components of a URL, this function rebuilds @@ -381,7 +380,7 @@ class Auth_OpenID { } if (!$path) { - $path = '/'; + $path = ''; } $result = $scheme . "://" . $host; @@ -415,65 +414,28 @@ class Auth_OpenID { */ function normalizeUrl($url) { - if ($url === null) { - return null; - } - - assert(is_string($url)); - - $old_url = $url; - $url = trim($url); - - if (strpos($url, "://") === false) { - $url = "http://" . $url; - } - - $parsed = @parse_url($url); + @$parsed = parse_url($url); - if ($parsed === false) { + if (!$parsed) { return null; } - $defaults = array( - 'scheme' => '', - 'host' => '', - 'path' => '', - 'query' => '', - 'fragment' => '', - 'port' => '' - ); - - $parsed = array_merge($defaults, $parsed); - - if (($parsed['scheme'] == '') || - ($parsed['host'] == '')) { - if ($parsed['path'] == '' && - $parsed['query'] == '' && - $parsed['fragment'] == '') { + if (isset($parsed['scheme']) && + isset($parsed['host'])) { + $scheme = strtolower($parsed['scheme']); + if (!in_array($scheme, array('http', 'https'))) { return null; } - - $url = 'http://' + $url; - $parsed = parse_url($url); - - $parsed = array_merge($defaults, $parsed); + } else { + $url = 'http://' . $url; } - $tail = array_map(array('Auth_OpenID', 'quoteMinimal'), - array($parsed['path'], - $parsed['query'], - $parsed['fragment'])); - if ($tail[0] == '') { - $tail[0] = '/'; + $normalized = Auth_OpenID_urinorm($url); + if ($normalized === null) { + return null; } - - $url = Auth_OpenID::urlunparse($parsed['scheme'], $parsed['host'], - $parsed['port'], $tail[0], $tail[1], - $tail[2]); - - assert(is_string($url)); - - return $url; + list($defragged, $frag) = Auth_OpenID::urldefrag($normalized); + return $defragged; } /** @@ -523,6 +485,68 @@ class Auth_OpenID { return $b; } -} -?> \ No newline at end of file + function urldefrag($url) + { + $parts = explode("#", $url, 2); + + if (count($parts) == 1) { + return array($parts[0], ""); + } else { + return $parts; + } + } + + function filter($callback, &$sequence) + { + $result = array(); + + foreach ($sequence as $item) { + if (call_user_func_array($callback, array($item))) { + $result[] = $item; + } + } + + return $result; + } + + function update(&$dest, &$src) + { + foreach ($src as $k => $v) { + $dest[$k] = $v; + } + } + + /** + * Wrap PHP's standard error_log functionality. Use this to + * perform all logging. It will interpolate any additional + * arguments into the format string before logging. + * + * @param string $format_string The sprintf format for the message + */ + function log($format_string) + { + $args = func_get_args(); + $message = call_user_func_array('sprintf', $args); + error_log($message); + } + + function autoSubmitHTML($form, $title="OpenId transaction in progress") + { + return("<html>". + "<head><title>". + $title . + "</title></head>". + "<body onload='document.forms[0].submit();'>". + $form . + "<script>". + "var elements = document.forms[0].elements;". + "for (var i = 0; i < elements.length; i++) {". + " elements[i].style.display = \"none\";". + "}". + "</script>". + "</body>". + "</html>"); + } +} +?> diff --git a/lib/Auth/OpenID/AX.php b/lib/Auth/OpenID/AX.php new file mode 100644 index 0000000000000000000000000000000000000000..4a617ae30c313a9abbf8edbe2adf8d199d158396 --- /dev/null +++ b/lib/Auth/OpenID/AX.php @@ -0,0 +1,1023 @@ +<?php + +/** + * Implements the OpenID attribute exchange specification, version 1.0 + * as of svn revision 370 from openid.net svn. + * + * @package OpenID + */ + +/** + * Require utility classes and functions for the consumer. + */ +require_once "Auth/OpenID/Extension.php"; +require_once "Auth/OpenID/Message.php"; +require_once "Auth/OpenID/TrustRoot.php"; + +define('Auth_OpenID_AX_NS_URI', + 'http://openid.net/srv/ax/1.0'); + +// Use this as the 'count' value for an attribute in a FetchRequest to +// ask for as many values as the OP can provide. +define('Auth_OpenID_AX_UNLIMITED_VALUES', 'unlimited'); + +// Minimum supported alias length in characters. Here for +// completeness. +define('Auth_OpenID_AX_MINIMUM_SUPPORTED_ALIAS_LENGTH', 32); + +/** + * AX utility class. + * + * @package OpenID + */ +class Auth_OpenID_AX { + /** + * @param mixed $thing Any object which may be an + * Auth_OpenID_AX_Error object. + * + * @return bool true if $thing is an Auth_OpenID_AX_Error; false + * if not. + */ + function isError($thing) + { + return is_a($thing, 'Auth_OpenID_AX_Error'); + } +} + +/** + * Check an alias for invalid characters; raise AXError if any are + * found. Return None if the alias is valid. + */ +function Auth_OpenID_AX_checkAlias($alias) +{ + if (strpos($alias, ',') !== false) { + return new Auth_OpenID_AX_Error(sprintf( + "Alias %s must not contain comma", $alias)); + } + if (strpos($alias, '.') !== false) { + return new Auth_OpenID_AX_Error(sprintf( + "Alias %s must not contain period", $alias)); + } + + return true; +} + +/** + * Results from data that does not meet the attribute exchange 1.0 + * specification + * + * @package OpenID + */ +class Auth_OpenID_AX_Error { + function Auth_OpenID_AX_Error($message=null) + { + $this->message = $message; + } +} + +/** + * Abstract class containing common code for attribute exchange + * messages. + * + * @package OpenID + */ +class Auth_OpenID_AX_Message extends Auth_OpenID_Extension { + /** + * ns_alias: The preferred namespace alias for attribute exchange + * messages + */ + var $ns_alias = 'ax'; + + /** + * mode: The type of this attribute exchange message. This must be + * overridden in subclasses. + */ + var $mode = null; + + var $ns_uri = Auth_OpenID_AX_NS_URI; + + /** + * Return Auth_OpenID_AX_Error if the mode in the attribute + * exchange arguments does not match what is expected for this + * class; true otherwise. + * + * @access private + */ + function _checkMode($ax_args) + { + $mode = Auth_OpenID::arrayGet($ax_args, 'mode'); + if ($mode != $this->mode) { + return new Auth_OpenID_AX_Error( + sprintf( + "Expected mode '%s'; got '%s'", + $this->mode, $mode)); + } + + return true; + } + + /** + * Return a set of attribute exchange arguments containing the + * basic information that must be in every attribute exchange + * message. + * + * @access private + */ + function _newArgs() + { + return array('mode' => $this->mode); + } +} + +/** + * Represents a single attribute in an attribute exchange + * request. This should be added to an AXRequest object in order to + * request the attribute. + * + * @package OpenID + */ +class Auth_OpenID_AX_AttrInfo { + /** + * Construct an attribute information object. Do not call this + * directly; call make(...) instead. + * + * @param string $type_uri The type URI for this attribute. + * + * @param int $count The number of values of this type to request. + * + * @param bool $required Whether the attribute will be marked as + * required in the request. + * + * @param string $alias The name that should be given to this + * attribute in the request. + */ + function Auth_OpenID_AX_AttrInfo($type_uri, $count, $required, + $alias) + { + /** + * required: Whether the attribute will be marked as required + * when presented to the subject of the attribute exchange + * request. + */ + $this->required = $required; + + /** + * count: How many values of this type to request from the + * subject. Defaults to one. + */ + $this->count = $count; + + /** + * type_uri: The identifier that determines what the attribute + * represents and how it is serialized. For example, one type + * URI representing dates could represent a Unix timestamp in + * base 10 and another could represent a human-readable + * string. + */ + $this->type_uri = $type_uri; + + /** + * alias: The name that should be given to this attribute in + * the request. If it is not supplied, a generic name will be + * assigned. For example, if you want to call a Unix timestamp + * value 'tstamp', set its alias to that value. If two + * attributes in the same message request to use the same + * alias, the request will fail to be generated. + */ + $this->alias = $alias; + } + + /** + * Construct an attribute information object. For parameter + * details, see the constructor. + */ + function make($type_uri, $count=1, $required=false, + $alias=null) + { + if ($alias !== null) { + $result = Auth_OpenID_AX_checkAlias($alias); + + if (Auth_OpenID_AX::isError($result)) { + return $result; + } + } + + return new Auth_OpenID_AX_AttrInfo($type_uri, $count, $required, + $alias); + } + + /** + * When processing a request for this attribute, the OP should + * call this method to determine whether all available attribute + * values were requested. If self.count == UNLIMITED_VALUES, this + * returns True. Otherwise this returns False, in which case + * self.count is an integer. + */ + function wantsUnlimitedValues() + { + return $this->count === Auth_OpenID_AX_UNLIMITED_VALUES; + } +} + +/** + * Given a namespace mapping and a string containing a comma-separated + * list of namespace aliases, return a list of type URIs that + * correspond to those aliases. + * + * @param $namespace_map The mapping from namespace URI to alias + * @param $alias_list_s The string containing the comma-separated + * list of aliases. May also be None for convenience. + * + * @return $seq The list of namespace URIs that corresponds to the + * supplied list of aliases. If the string was zero-length or None, an + * empty list will be returned. + * + * return null If an alias is present in the list of aliases but + * is not present in the namespace map. + */ +function Auth_OpenID_AX_toTypeURIs(&$namespace_map, $alias_list_s) +{ + $uris = array(); + + if ($alias_list_s) { + foreach (explode(',', $alias_list_s) as $alias) { + $type_uri = $namespace_map->getNamespaceURI($alias); + if ($type_uri === null) { + // raise KeyError( + // 'No type is defined for attribute name %r' % (alias,)) + return new Auth_OpenID_AX_Error( + sprintf('No type is defined for attribute name %s', + $alias) + ); + } else { + $uris[] = $type_uri; + } + } + } + + return $uris; +} + +/** + * An attribute exchange 'fetch_request' message. This message is sent + * by a relying party when it wishes to obtain attributes about the + * subject of an OpenID authentication request. + * + * @package OpenID + */ +class Auth_OpenID_AX_FetchRequest extends Auth_OpenID_AX_Message { + + var $mode = 'fetch_request'; + + function Auth_OpenID_AX_FetchRequest($update_url=null) + { + /** + * requested_attributes: The attributes that have been + * requested thus far, indexed by the type URI. + */ + $this->requested_attributes = array(); + + /** + * update_url: A URL that will accept responses for this + * attribute exchange request, even in the absence of the user + * who made this request. + */ + $this->update_url = $update_url; + } + + /** + * Add an attribute to this attribute exchange request. + * + * @param attribute: The attribute that is being requested + * @return true on success, false when the requested attribute is + * already present in this fetch request. + */ + function add($attribute) + { + if ($this->contains($attribute->type_uri)) { + return new Auth_OpenID_AX_Error( + sprintf("The attribute %s has already been requested", + $attribute->type_uri)); + } + + $this->requested_attributes[$attribute->type_uri] = $attribute; + + return true; + } + + /** + * Get the serialized form of this attribute fetch request. + * + * @returns Auth_OpenID_AX_FetchRequest The fetch request message parameters + */ + function getExtensionArgs() + { + $aliases = new Auth_OpenID_NamespaceMap(); + + $required = array(); + $if_available = array(); + + $ax_args = $this->_newArgs(); + + foreach ($this->requested_attributes as $type_uri => $attribute) { + if ($attribute->alias === null) { + $alias = $aliases->add($type_uri); + } else { + $alias = $aliases->addAlias($type_uri, $attribute->alias); + + if ($alias === null) { + return new Auth_OpenID_AX_Error( + sprintf("Could not add alias %s for URI %s", + $attribute->alias, $type_uri + )); + } + } + + if ($attribute->required) { + $required[] = $alias; + } else { + $if_available[] = $alias; + } + + if ($attribute->count != 1) { + $ax_args['count.' . $alias] = strval($attribute->count); + } + + $ax_args['type.' . $alias] = $type_uri; + } + + if ($required) { + $ax_args['required'] = implode(',', $required); + } + + if ($if_available) { + $ax_args['if_available'] = implode(',', $if_available); + } + + return $ax_args; + } + + /** + * Get the type URIs for all attributes that have been marked as + * required. + * + * @return A list of the type URIs for attributes that have been + * marked as required. + */ + function getRequiredAttrs() + { + $required = array(); + foreach ($this->requested_attributes as $type_uri => $attribute) { + if ($attribute->required) { + $required[] = $type_uri; + } + } + + return $required; + } + + /** + * Extract a FetchRequest from an OpenID message + * + * @param request: The OpenID request containing the attribute + * fetch request + * + * @returns mixed An Auth_OpenID_AX_Error or the + * Auth_OpenID_AX_FetchRequest extracted from the request message if + * successful + */ + function &fromOpenIDRequest($request) + { + $m = $request->message; + $obj = new Auth_OpenID_AX_FetchRequest(); + $ax_args = $m->getArgs($obj->ns_uri); + + $result = $obj->parseExtensionArgs($ax_args); + + if (Auth_OpenID_AX::isError($result)) { + return $result; + } + + if ($obj->update_url) { + // Update URL must match the openid.realm of the + // underlying OpenID 2 message. + $realm = $m->getArg(Auth_OpenID_OPENID_NS, 'realm', + $m->getArg( + Auth_OpenID_OPENID_NS, + 'return_to')); + + if (!$realm) { + $obj = new Auth_OpenID_AX_Error( + sprintf("Cannot validate update_url %s " . + "against absent realm", $obj->update_url)); + } else if (!Auth_OpenID_TrustRoot::match($realm, + $obj->update_url)) { + $obj = new Auth_OpenID_AX_Error( + sprintf("Update URL %s failed validation against realm %s", + $obj->update_url, $realm)); + } + } + + return $obj; + } + + /** + * Given attribute exchange arguments, populate this FetchRequest. + * + * @return $result Auth_OpenID_AX_Error if the data to be parsed + * does not follow the attribute exchange specification. At least + * when 'if_available' or 'required' is not specified for a + * particular attribute type. Returns true otherwise. + */ + function parseExtensionArgs($ax_args) + { + $result = $this->_checkMode($ax_args); + if (Auth_OpenID_AX::isError($result)) { + return $result; + } + + $aliases = new Auth_OpenID_NamespaceMap(); + + foreach ($ax_args as $key => $value) { + if (strpos($key, 'type.') === 0) { + $alias = substr($key, 5); + $type_uri = $value; + + $alias = $aliases->addAlias($type_uri, $alias); + + if ($alias === null) { + return new Auth_OpenID_AX_Error( + sprintf("Could not add alias %s for URI %s", + $alias, $type_uri) + ); + } + + $count_s = Auth_OpenID::arrayGet($ax_args, 'count.' . $alias); + if ($count_s) { + $count = Auth_OpenID::intval($count_s); + if (($count === false) && + ($count_s === Auth_OpenID_AX_UNLIMITED_VALUES)) { + $count = $count_s; + } + } else { + $count = 1; + } + + if ($count === false) { + return new Auth_OpenID_AX_Error( + sprintf("Integer value expected for %s, got %s", + 'count.' . $alias, $count_s)); + } + + $attrinfo = Auth_OpenID_AX_AttrInfo::make($type_uri, $count, + false, $alias); + + if (Auth_OpenID_AX::isError($attrinfo)) { + return $attrinfo; + } + + $this->add($attrinfo); + } + } + + $required = Auth_OpenID_AX_toTypeURIs($aliases, + Auth_OpenID::arrayGet($ax_args, 'required')); + + foreach ($required as $type_uri) { + $attrib =& $this->requested_attributes[$type_uri]; + $attrib->required = true; + } + + $if_available = Auth_OpenID_AX_toTypeURIs($aliases, + Auth_OpenID::arrayGet($ax_args, 'if_available')); + + $all_type_uris = array_merge($required, $if_available); + + foreach ($aliases->iterNamespaceURIs() as $type_uri) { + if (!in_array($type_uri, $all_type_uris)) { + return new Auth_OpenID_AX_Error( + sprintf('Type URI %s was in the request but not ' . + 'present in "required" or "if_available"', + $type_uri)); + + } + } + + $this->update_url = Auth_OpenID::arrayGet($ax_args, 'update_url'); + + return true; + } + + /** + * Iterate over the AttrInfo objects that are contained in this + * fetch_request. + */ + function iterAttrs() + { + return array_values($this->requested_attributes); + } + + function iterTypes() + { + return array_keys($this->requested_attributes); + } + + /** + * Is the given type URI present in this fetch_request? + */ + function contains($type_uri) + { + return in_array($type_uri, $this->iterTypes()); + } +} + +/** + * An abstract class that implements a message that has attribute keys + * and values. It contains the common code between fetch_response and + * store_request. + * + * @package OpenID + */ +class Auth_OpenID_AX_KeyValueMessage extends Auth_OpenID_AX_Message { + + function Auth_OpenID_AX_KeyValueMessage() + { + $this->data = array(); + } + + /** + * Add a single value for the given attribute type to the + * message. If there are already values specified for this type, + * this value will be sent in addition to the values already + * specified. + * + * @param type_uri: The URI for the attribute + * @param value: The value to add to the response to the relying + * party for this attribute + * @return null + */ + function addValue($type_uri, $value) + { + if (!array_key_exists($type_uri, $this->data)) { + $this->data[$type_uri] = array(); + } + + $values =& $this->data[$type_uri]; + $values[] = $value; + } + + /** + * Set the values for the given attribute type. This replaces any + * values that have already been set for this attribute. + * + * @param type_uri: The URI for the attribute + * @param values: A list of values to send for this attribute. + */ + function setValues($type_uri, &$values) + { + $this->data[$type_uri] =& $values; + } + + /** + * Get the extension arguments for the key/value pairs contained + * in this message. + * + * @param aliases: An alias mapping. Set to None if you don't care + * about the aliases for this request. + * + * @access private + */ + function _getExtensionKVArgs(&$aliases) + { + if ($aliases === null) { + $aliases = new Auth_OpenID_NamespaceMap(); + } + + $ax_args = array(); + + foreach ($this->data as $type_uri => $values) { + $alias = $aliases->add($type_uri); + + $ax_args['type.' . $alias] = $type_uri; + $ax_args['count.' . $alias] = strval(count($values)); + + foreach ($values as $i => $value) { + $key = sprintf('value.%s.%d', $alias, $i + 1); + $ax_args[$key] = $value; + } + } + + return $ax_args; + } + + /** + * Parse attribute exchange key/value arguments into this object. + * + * @param ax_args: The attribute exchange fetch_response + * arguments, with namespacing removed. + * + * @return Auth_OpenID_AX_Error or true + */ + function parseExtensionArgs($ax_args) + { + $result = $this->_checkMode($ax_args); + if (Auth_OpenID_AX::isError($result)) { + return $result; + } + + $aliases = new Auth_OpenID_NamespaceMap(); + + foreach ($ax_args as $key => $value) { + if (strpos($key, 'type.') === 0) { + $type_uri = $value; + $alias = substr($key, 5); + + $result = Auth_OpenID_AX_checkAlias($alias); + + if (Auth_OpenID_AX::isError($result)) { + return $result; + } + + $alias = $aliases->addAlias($type_uri, $alias); + + if ($alias === null) { + return new Auth_OpenID_AX_Error( + sprintf("Could not add alias %s for URI %s", + $alias, $type_uri) + ); + } + } + } + + foreach ($aliases->iteritems() as $pair) { + list($type_uri, $alias) = $pair; + + if (array_key_exists('count.' . $alias, $ax_args)) { + + $count_key = 'count.' . $alias; + $count_s = $ax_args[$count_key]; + + $count = Auth_OpenID::intval($count_s); + + if ($count === false) { + return new Auth_OpenID_AX_Error( + sprintf("Integer value expected for %s, got %s", + 'count. %s' . $alias, $count_s, + Auth_OpenID_AX_UNLIMITED_VALUES) + ); + } + + $values = array(); + for ($i = 1; $i < $count + 1; $i++) { + $value_key = sprintf('value.%s.%d', $alias, $i); + + if (!array_key_exists($value_key, $ax_args)) { + return new Auth_OpenID_AX_Error( + sprintf( + "No value found for key %s", + $value_key)); + } + + $value = $ax_args[$value_key]; + $values[] = $value; + } + } else { + $key = 'value.' . $alias; + + if (!array_key_exists($key, $ax_args)) { + return new Auth_OpenID_AX_Error( + sprintf( + "No value found for key %s", + $key)); + } + + $value = $ax_args['value.' . $alias]; + + if ($value == '') { + $values = array(); + } else { + $values = array($value); + } + } + + $this->data[$type_uri] = $values; + } + + return true; + } + + /** + * Get a single value for an attribute. If no value was sent for + * this attribute, use the supplied default. If there is more than + * one value for this attribute, this method will fail. + * + * @param type_uri: The URI for the attribute + * @param default: The value to return if the attribute was not + * sent in the fetch_response. + * + * @return $value Auth_OpenID_AX_Error on failure or the value of + * the attribute in the fetch_response message, or the default + * supplied + */ + function getSingle($type_uri, $default=null) + { + $values = Auth_OpenID::arrayGet($this->data, $type_uri); + if (!$values) { + return $default; + } else if (count($values) == 1) { + return $values[0]; + } else { + return new Auth_OpenID_AX_Error( + sprintf('More than one value present for %s', + $type_uri) + ); + } + } + + /** + * Get the list of values for this attribute in the + * fetch_response. + * + * XXX: what to do if the values are not present? default + * parameter? this is funny because it's always supposed to return + * a list, so the default may break that, though it's provided by + * the user's code, so it might be okay. If no default is + * supplied, should the return be None or []? + * + * @param type_uri: The URI of the attribute + * + * @return $values The list of values for this attribute in the + * response. May be an empty list. If the attribute was not sent + * in the response, returns Auth_OpenID_AX_Error. + */ + function get($type_uri) + { + if (array_key_exists($type_uri, $this->data)) { + return $this->data[$type_uri]; + } else { + return new Auth_OpenID_AX_Error( + sprintf("Type URI %s not found in response", + $type_uri) + ); + } + } + + /** + * Get the number of responses for a particular attribute in this + * fetch_response message. + * + * @param type_uri: The URI of the attribute + * + * @returns int The number of values sent for this attribute. If + * the attribute was not sent in the response, returns + * Auth_OpenID_AX_Error. + */ + function count($type_uri) + { + if (array_key_exists($type_uri, $this->data)) { + return count($this->get($type_uri)); + } else { + return new Auth_OpenID_AX_Error( + sprintf("Type URI %s not found in response", + $type_uri) + ); + } + } +} + +/** + * A fetch_response attribute exchange message. + * + * @package OpenID + */ +class Auth_OpenID_AX_FetchResponse extends Auth_OpenID_AX_KeyValueMessage { + var $mode = 'fetch_response'; + + function Auth_OpenID_AX_FetchResponse($update_url=null) + { + $this->Auth_OpenID_AX_KeyValueMessage(); + $this->update_url = $update_url; + } + + /** + * Serialize this object into arguments in the attribute exchange + * namespace + * + * @return $args The dictionary of unqualified attribute exchange + * arguments that represent this fetch_response, or + * Auth_OpenID_AX_Error on error. + */ + function getExtensionArgs($request=null) + { + $aliases = new Auth_OpenID_NamespaceMap(); + + $zero_value_types = array(); + + if ($request !== null) { + // Validate the data in the context of the request (the + // same attributes should be present in each, and the + // counts in the response must be no more than the counts + // in the request) + + foreach ($this->data as $type_uri => $unused) { + if (!$request->contains($type_uri)) { + return new Auth_OpenID_AX_Error( + sprintf("Response attribute not present in request: %s", + $type_uri) + ); + } + } + + foreach ($request->iterAttrs() as $attr_info) { + // Copy the aliases from the request so that reading + // the response in light of the request is easier + if ($attr_info->alias === null) { + $aliases->add($attr_info->type_uri); + } else { + $alias = $aliases->addAlias($attr_info->type_uri, + $attr_info->alias); + + if ($alias === null) { + return new Auth_OpenID_AX_Error( + sprintf("Could not add alias %s for URI %s", + $attr_info->alias, $attr_info->type_uri) + ); + } + } + + if (array_key_exists($attr_info->type_uri, $this->data)) { + $values = $this->data[$attr_info->type_uri]; + } else { + $values = array(); + $zero_value_types[] = $attr_info; + } + + if (($attr_info->count != Auth_OpenID_AX_UNLIMITED_VALUES) && + ($attr_info->count < count($values))) { + return new Auth_OpenID_AX_Error( + sprintf("More than the number of requested values " . + "were specified for %s", + $attr_info->type_uri) + ); + } + } + } + + $kv_args = $this->_getExtensionKVArgs($aliases); + + // Add the KV args into the response with the args that are + // unique to the fetch_response + $ax_args = $this->_newArgs(); + + // For each requested attribute, put its type/alias and count + // into the response even if no data were returned. + foreach ($zero_value_types as $attr_info) { + $alias = $aliases->getAlias($attr_info->type_uri); + $kv_args['type.' . $alias] = $attr_info->type_uri; + $kv_args['count.' . $alias] = '0'; + } + + $update_url = null; + if ($request) { + $update_url = $request->update_url; + } else { + $update_url = $this->update_url; + } + + if ($update_url) { + $ax_args['update_url'] = $update_url; + } + + Auth_OpenID::update(&$ax_args, $kv_args); + + return $ax_args; + } + + /** + * @return $result Auth_OpenID_AX_Error on failure or true on + * success. + */ + function parseExtensionArgs($ax_args) + { + $result = parent::parseExtensionArgs($ax_args); + + if (Auth_OpenID_AX::isError($result)) { + return $result; + } + + $this->update_url = Auth_OpenID::arrayGet($ax_args, 'update_url'); + + return true; + } + + /** + * Construct a FetchResponse object from an OpenID library + * SuccessResponse object. + * + * @param success_response: A successful id_res response object + * + * @param signed: Whether non-signed args should be processsed. If + * True (the default), only signed arguments will be processsed. + * + * @return $response A FetchResponse containing the data from the + * OpenID message + */ + function fromSuccessResponse($success_response, $signed=true) + { + $obj = new Auth_OpenID_AX_FetchResponse(); + if ($signed) { + $ax_args = $success_response->getSignedNS($obj->ns_uri); + } else { + $ax_args = $success_response->message->getArgs($obj->ns_uri); + } + if ($ax_args === null || Auth_OpenID::isFailure($ax_args) || + sizeof($ax_args) == 0) { + return null; + } + + $result = $obj->parseExtensionArgs($ax_args); + if (Auth_OpenID_AX::isError($result)) { + #XXX log me + return null; + } + return $obj; + } +} + +/** + * A store request attribute exchange message representation. + * + * @package OpenID + */ +class Auth_OpenID_AX_StoreRequest extends Auth_OpenID_AX_KeyValueMessage { + var $mode = 'store_request'; + + /** + * @param array $aliases The namespace aliases to use when making + * this store response. Leave as None to use defaults. + */ + function getExtensionArgs($aliases=null) + { + $ax_args = $this->_newArgs(); + $kv_args = $this->_getExtensionKVArgs($aliases); + Auth_OpenID::update(&$ax_args, $kv_args); + return $ax_args; + } +} + +/** + * An indication that the store request was processed along with this + * OpenID transaction. Use make(), NOT the constructor, to create + * response objects. + * + * @package OpenID + */ +class Auth_OpenID_AX_StoreResponse extends Auth_OpenID_AX_Message { + var $SUCCESS_MODE = 'store_response_success'; + var $FAILURE_MODE = 'store_response_failure'; + + /** + * Returns Auth_OpenID_AX_Error on error or an + * Auth_OpenID_AX_StoreResponse object on success. + */ + function &make($succeeded=true, $error_message=null) + { + if (($succeeded) && ($error_message !== null)) { + return new Auth_OpenID_AX_Error('An error message may only be '. + 'included in a failing fetch response'); + } + + return new Auth_OpenID_AX_StoreResponse($succeeded, $error_message); + } + + function Auth_OpenID_AX_StoreResponse($succeeded=true, $error_message=null) + { + if ($succeeded) { + $this->mode = $this->SUCCESS_MODE; + } else { + $this->mode = $this->FAILURE_MODE; + } + + $this->error_message = $error_message; + } + + /** + * Was this response a success response? + */ + function succeeded() + { + return $this->mode == $this->SUCCESS_MODE; + } + + function getExtensionArgs() + { + $ax_args = $this->_newArgs(); + if ((!$this->succeeded()) && $this->error_message) { + $ax_args['error'] = $this->error_message; + } + + return $ax_args; + } +} + +?> \ No newline at end of file diff --git a/lib/Auth/OpenID/Association.php b/lib/Auth/OpenID/Association.php index 16c83bf3df26acc8f439ee3d8c8f3412c28cf052..37ce0cbf4545c8a19e066c4218e520bd4b8d7426 100644 --- a/lib/Auth/OpenID/Association.php +++ b/lib/Auth/OpenID/Association.php @@ -10,8 +10,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** @@ -27,7 +27,7 @@ require_once 'Auth/OpenID/KVForm.php'; /** * @access private */ -require_once 'Auth/OpenID/HMACSHA1.php'; +require_once 'Auth/OpenID/HMAC.php'; /** * This class represents an association between a server and a @@ -64,6 +64,11 @@ class Auth_OpenID_Association { 'assoc_type' ); + var $_macs = array( + 'HMAC-SHA1' => 'Auth_OpenID_HMACSHA1', + 'HMAC-SHA256' => 'Auth_OpenID_HMACSHA256' + ); + /** * This is an alternate constructor (factory method) used by the * OpenID consumer library to create associations. OpenID store @@ -82,9 +87,9 @@ class Auth_OpenID_Association { * generated for this association. * * @param assoc_type This is the type of association this - * instance represents. The only valid value of this field at - * this time is 'HMAC-SHA1', but new types may be defined in the - * future. + * instance represents. The only valid values of this field at + * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may + * be defined in the future. * * @return association An {@link Auth_OpenID_Association} * instance. @@ -119,9 +124,9 @@ class Auth_OpenID_Association { * association was issued. * * @param string $assoc_type This is the type of association this - * instance represents. The only valid value of this field at - * this time is 'HMAC-SHA1', but new types may be defined in the - * future. + * instance represents. The only valid values of this field at + * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may + * be defined in the future. */ function Auth_OpenID_Association( $handle, $secret, $issued, $lifetime, $assoc_type) @@ -258,7 +263,11 @@ class Auth_OpenID_Association { function sign($pairs) { $kv = Auth_OpenID_KVForm::fromArray($pairs); - return Auth_OpenID_HMACSHA1($this->secret, $kv); + + /* Invalid association types should be caught at constructor */ + $callback = $this->_macs[$this->assoc_type]; + + return call_user_func_array($callback, array($this->secret, $kv)); } /** @@ -321,7 +330,7 @@ class Auth_OpenID_Association { function _makePairs(&$message) { $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed'); - if (!$signed) { + if (!$signed || Auth_OpenID::isFailure($signed)) { // raise ValueError('Message has no signed list: %s' % (message,)) return null; } @@ -360,7 +369,7 @@ class Auth_OpenID_Association { $sig = $message->getArg(Auth_OpenID_OPENID_NS, 'sig'); - if (!$sig) { + if (!$sig || Auth_OpenID::isFailure($sig)) { return false; } @@ -423,7 +432,7 @@ function Auth_OpenID_getDefaultAssociationOrder() { $order = array(); - if (!defined('Auth_OpenID_NO_MATH_SUPPORT')) { + if (!Auth_OpenID_noMathSupport()) { $order[] = array('HMAC-SHA1', 'DH-SHA1'); if (Auth_OpenID_HMACSHA256_SUPPORTED) { @@ -518,7 +527,8 @@ function &Auth_OpenID_getEncryptedNegotiator() class Auth_OpenID_SessionNegotiator { function Auth_OpenID_SessionNegotiator($allowed_types) { - $this->allowed_types = $allowed_types; + $this->allowed_types = array(); + $this->setAllowedTypes($allowed_types); } /** diff --git a/lib/Auth/OpenID/BigMath.php b/lib/Auth/OpenID/BigMath.php index cfa0b3541d3a49a93421f91ef42067dec7368046..45104947d6da44412671f6e319f0dcf817131893 100644 --- a/lib/Auth/OpenID/BigMath.php +++ b/lib/Auth/OpenID/BigMath.php @@ -11,8 +11,8 @@ * @access private * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** @@ -427,7 +427,7 @@ function &Auth_OpenID_getMathLib() return $lib; } - if (defined('Auth_OpenID_NO_MATH_SUPPORT')) { + if (Auth_OpenID_noMathSupport()) { $null = null; return $null; } @@ -443,8 +443,10 @@ function &Auth_OpenID_getMathLib() } $triedstr = implode(", ", $tried); - define('Auth_OpenID_NO_MATH_SUPPORT', true); - return null; + Auth_OpenID_setNoMathSupport(); + + $result = null; + return $result; } // Instantiate a new wrapper @@ -454,4 +456,16 @@ function &Auth_OpenID_getMathLib() return $lib; } +function Auth_OpenID_setNoMathSupport() +{ + if (!defined('Auth_OpenID_NO_MATH_SUPPORT')) { + define('Auth_OpenID_NO_MATH_SUPPORT', true); + } +} + +function Auth_OpenID_noMathSupport() +{ + return defined('Auth_OpenID_NO_MATH_SUPPORT'); +} + ?> diff --git a/lib/Auth/OpenID/Consumer.php b/lib/Auth/OpenID/Consumer.php index c1a05eeb13c8c91a27de12cf384c418d2bae7186..500890b6568def082e94b3afdc28ac893cd3592c 100644 --- a/lib/Auth/OpenID/Consumer.php +++ b/lib/Auth/OpenID/Consumer.php @@ -153,8 +153,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** @@ -162,13 +162,14 @@ */ require_once "Auth/OpenID.php"; require_once "Auth/OpenID/Message.php"; -require_once "Auth/OpenID/HMACSHA1.php"; +require_once "Auth/OpenID/HMAC.php"; require_once "Auth/OpenID/Association.php"; require_once "Auth/OpenID/CryptUtil.php"; require_once "Auth/OpenID/DiffieHellman.php"; require_once "Auth/OpenID/KVForm.php"; require_once "Auth/OpenID/Nonce.php"; require_once "Auth/OpenID/Discover.php"; +require_once "Auth/OpenID/URINorm.php"; require_once "Auth/Yadis/Manager.php"; require_once "Auth/Yadis/XRI.php"; @@ -388,6 +389,13 @@ class Auth_OpenID_Consumer { * request. It is called in step 4 of the flow described in the * consumer overview. * + * @param string $current_url The URL used to invoke the application. + * Extract the URL from your application's web + * request framework and specify it here to have it checked + * against the openid.current_url value in the response. If + * the current_url URL check fails, the status of the + * completion will be FAILURE. + * * @param array $query An array of the query parameters (key => * value pairs) for this HTTP request. Defaults to null. If * null, the GET or POST data are automatically gotten from the @@ -399,8 +407,16 @@ class Auth_OpenID_Consumer { * indicated by the status attribute, which will be one of * SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED. */ - function complete($query=null) + function complete($current_url, $query=null) { + if ($current_url && !is_string($current_url)) { + // This is ugly, but we need to complain loudly when + // someone uses the API incorrectly. + trigger_error("current_url must be a string; see NEWS file " . + "for upgrading notes.", + E_USER_ERROR); + } + if ($query === null) { $query = Auth_OpenID::getQuery(); } @@ -410,14 +426,10 @@ class Auth_OpenID_Consumer { $endpoint = $loader->fromSession($endpoint_data); - if ($endpoint === null) { - $response = new Auth_OpenID_FailureResponse(null, - 'No session state found'); - } else { - $message = Auth_OpenID_Message::fromPostArgs($query); - $response = $this->consumer->complete($message, $endpoint); - $this->session->del($this->_token_key); - } + $message = Auth_OpenID_Message::fromPostArgs($query); + $response = $this->consumer->complete($message, $endpoint, + $current_url); + $this->session->del($this->_token_key); if (in_array($response->status, array(Auth_OpenID_SUCCESS, Auth_OpenID_CANCEL))) { @@ -425,7 +437,7 @@ class Auth_OpenID_Consumer { $disco = $this->getDiscoveryObject($this->session, $response->identity_url, $this->session_key_prefix); - $disco->cleanup(); + $disco->cleanup(true); } } @@ -515,7 +527,7 @@ class Auth_OpenID_DiffieHellmanSHA256ConsumerSession extends */ class Auth_OpenID_PlainTextConsumerSession { var $session_type = 'no-encryption'; - var $allowed_assoc_types = array('HMAC-SHA1'); + var $allowed_assoc_types = array('HMAC-SHA1', 'HMAC-SHA256'); function getRequest() { @@ -574,6 +586,13 @@ class Auth_OpenID_GenericConsumer { */ var $openid1_nonce_query_arg_name = 'janrain_nonce'; + /** + * Another query parameter that gets added to the return_to for + * OpenID 1; if the user's session state is lost, use this claimed + * identifier to do discovery when verifying the response. + */ + var $openid1_return_to_identifier_name = 'openid1_claimed_id'; + /** * This method initializes a new {@link Auth_OpenID_Consumer} * instance to access the library. @@ -615,6 +634,12 @@ class Auth_OpenID_GenericConsumer { $r = new Auth_OpenID_AuthRequest($service_endpoint, $assoc); $r->return_to_args[$this->openid1_nonce_query_arg_name] = Auth_OpenID_mkNonce(); + + if ($r->message->isOpenID1()) { + $r->return_to_args[$this->openid1_return_to_identifier_name] = + $r->endpoint->claimed_id; + } + return $r; } @@ -625,41 +650,85 @@ class Auth_OpenID_GenericConsumer { * * @access private */ - function complete($message, $endpoint, $return_to = null) + function complete($message, $endpoint, $return_to) { $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode', '<no mode set>'); - if ($return_to !== null) { - if (!$this->_checkReturnTo($message, $return_to)) { - return new Auth_OpenID_FailureResponse($endpoint, - "openid.return_to does not match return URL"); - } + $mode_methods = array( + 'cancel' => '_complete_cancel', + 'error' => '_complete_error', + 'setup_needed' => '_complete_setup_needed', + 'id_res' => '_complete_id_res', + ); + + $method = Auth_OpenID::arrayGet($mode_methods, $mode, + '_completeInvalid'); + + return call_user_func_array(array(&$this, $method), + array($message, $endpoint, $return_to)); + } + + /** + * @access private + */ + function _completeInvalid($message, &$endpoint, $unused) + { + $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode', + '<No mode set>'); + + return new Auth_OpenID_FailureResponse($endpoint, + sprintf("Invalid openid.mode '%s'", $mode)); + } + + /** + * @access private + */ + function _complete_cancel($message, &$endpoint, $unused) + { + return new Auth_OpenID_CancelResponse($endpoint); + } + + /** + * @access private + */ + function _complete_error($message, &$endpoint, $unused) + { + $error = $message->getArg(Auth_OpenID_OPENID_NS, 'error'); + $contact = $message->getArg(Auth_OpenID_OPENID_NS, 'contact'); + $reference = $message->getArg(Auth_OpenID_OPENID_NS, 'reference'); + + return new Auth_OpenID_FailureResponse($endpoint, $error, + $contact, $reference); + } + + /** + * @access private + */ + function _complete_setup_needed($message, &$endpoint, $unused) + { + if (!$message->isOpenID2()) { + return $this->_completeInvalid($message, $endpoint); } - if ($mode == 'cancel') { - return new Auth_OpenID_CancelResponse($endpoint); - } else if ($mode == 'error') { - $error = $message->getArg(Auth_OpenID_OPENID_NS, 'error'); - $contact = $message->getArg(Auth_OpenID_OPENID_NS, 'contact'); - $reference = $message->getArg(Auth_OpenID_OPENID_NS, 'reference'); + $user_setup_url = $message->getArg(Auth_OpenID_OPENID2_NS, + 'user_setup_url'); + return new Auth_OpenID_SetupNeededResponse($endpoint, $user_setup_url); + } - return new Auth_OpenID_FailureResponse($endpoint, $error, - $contact, $reference); - } else if ($message->isOpenID2() && ($mode == 'setup_needed')) { - return new Auth_OpenID_SetupNeededResponse($endpoint); + /** + * @access private + */ + function _complete_id_res($message, &$endpoint, $return_to) + { + $user_setup_url = $message->getArg(Auth_OpenID_OPENID1_NS, + 'user_setup_url'); - } else if ($mode == 'id_res') { - if ($this->_checkSetupNeeded($message)) { - return SetupNeededResponse($endpoint, - $result->user_setup_url); - } else { - return $this->_doIdRes($message, $endpoint); - } + if ($this->_checkSetupNeeded($message)) { + return new Auth_OpenID_SetupNeededResponse( + $endpoint, $user_setup_url); } else { - return new Auth_OpenID_FailureResponse($endpoint, - sprintf("Invalid openid.mode '%s'", - $mode)); + return $this->_doIdRes($message, $endpoint, $return_to); } } @@ -685,26 +754,23 @@ class Auth_OpenID_GenericConsumer { /** * @access private */ - function _doIdRes($message, $endpoint) + function _doIdRes($message, $endpoint, $return_to) { - $signed_list_str = $message->getArg(Auth_OpenID_OPENID_NS, - 'signed'); - - if ($signed_list_str === null) { - return new Auth_OpenID_FailureResponse($endpoint, - "Response missing signed list"); - } - - $signed_list = explode(',', $signed_list_str); - // Checks for presence of appropriate fields (and checks // signed list fields) - $result = $this->_idResCheckForFields($message, $signed_list); + $result = $this->_idResCheckForFields($message); if (Auth_OpenID::isFailure($result)) { return $result; } + if (!$this->_checkReturnTo($message, $return_to)) { + return new Auth_OpenID_FailureResponse(null, + sprintf("return_to does not match return URL. Expected %s, got %s", + $return_to, + $message->getArg(Auth_OpenID_OPENID_NS, 'return_to'))); + } + // Verify discovery information: $result = $this->_verifyDiscoveryResults($message, $endpoint); @@ -721,15 +787,19 @@ class Auth_OpenID_GenericConsumer { return $result; } - $response_identity = $message->getArg(Auth_OpenID_OPENID_NS, - 'identity'); - $result = $this->_idResCheckNonce($message, $endpoint); if (Auth_OpenID::isFailure($result)) { return $result; } + $signed_list_str = $message->getArg(Auth_OpenID_OPENID_NS, 'signed', + Auth_OpenID_NO_DEFAULT); + if (Auth_OpenID::isFailure($signed_list_str)) { + return $signed_list_str; + } + $signed_list = explode(',', $signed_list_str); + $signed_fields = Auth_OpenID::addPrefix($signed_list, "openid."); return new Auth_OpenID_SuccessResponse($endpoint, $message, @@ -758,9 +828,13 @@ class Auth_OpenID_GenericConsumer { // message. $msg_return_to = $message->getArg(Auth_OpenID_OPENID_NS, 'return_to'); + if (Auth_OpenID::isFailure($return_to)) { + // XXX log me + return false; + } - $return_to_parts = parse_url($return_to); - $msg_return_to_parts = parse_url($msg_return_to); + $return_to_parts = parse_url(Auth_OpenID_urinorm($return_to)); + $msg_return_to_parts = parse_url(Auth_OpenID_urinorm($msg_return_to)); // If port is absent from both, add it so it's equal in the // check below. @@ -811,10 +885,13 @@ class Auth_OpenID_GenericConsumer { $message = Auth_OpenID_Message::fromPostArgs($query); $return_to = $message->getArg(Auth_OpenID_OPENID_NS, 'return_to'); + if (Auth_OpenID::isFailure($return_to)) { + return $return_to; + } // XXX: this should be checked by _idResCheckForFields if (!$return_to) { return new Auth_OpenID_FailureResponse(null, - "no openid.return_to in query"); + "Response has no return_to"); } $parsed_url = parse_url($return_to); @@ -840,6 +917,17 @@ class Auth_OpenID_GenericConsumer { } } + // Make sure all non-OpenID arguments in the response are also + // in the signed return_to. + $bare_args = $message->getArgs(Auth_OpenID_BARE_NS); + foreach ($bare_args as $key => $value) { + if (Auth_OpenID::arrayGet($q, $key) != $value) { + return new Auth_OpenID_FailureResponse(null, + sprintf("Parameter %s = %s not in return_to URL", + $key, $value)); + } + } + return true; } @@ -850,6 +938,9 @@ class Auth_OpenID_GenericConsumer { { $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, 'assoc_handle'); + if (Auth_OpenID::isFailure($assoc_handle)) { + return $assoc_handle; + } $assoc = $this->store->getAssociation($server_url, $assoc_handle); @@ -902,12 +993,17 @@ class Auth_OpenID_GenericConsumer { */ function _verifyDiscoveryResultsOpenID1($message, $endpoint) { - if ($endpoint === null) { + $claimed_id = $message->getArg(Auth_OpenID_BARE_NS, + $this->openid1_return_to_identifier_name); + + if (($endpoint === null) && ($claimed_id === null)) { return new Auth_OpenID_FailureResponse($endpoint, 'When using OpenID 1, the claimed ID must be supplied, ' . 'either by passing it through as a return_to parameter ' . 'or by using a session, and supplied to the GenericConsumer ' . 'as the argument to complete()'); + } else if (($endpoint !== null) && ($claimed_id === null)) { + $claimed_id = $endpoint->claimed_id; } $to_match = new Auth_OpenID_ServiceEndpoint(); @@ -916,7 +1012,7 @@ class Auth_OpenID_GenericConsumer { 'identity'); // Restore delegate information from the initiation phase - $to_match->claimed_id = $endpoint->claimed_id; + $to_match->claimed_id = $claimed_id; if ($to_match->local_id === null) { return new Auth_OpenID_FailureResponse($endpoint, @@ -926,17 +1022,27 @@ class Auth_OpenID_GenericConsumer { $to_match_1_0 = $to_match->copy(); $to_match_1_0->type_uris = array(Auth_OpenID_TYPE_1_0); - $result = $this->_verifyDiscoverySingle($endpoint, $to_match); + if ($endpoint !== null) { + $result = $this->_verifyDiscoverySingle($endpoint, $to_match); - if (is_a($result, 'Auth_OpenID_TypeURIMismatch')) { - $result = $this->_verifyDiscoverySingle($endpoint, $to_match_1_0); - } + if (is_a($result, 'Auth_OpenID_TypeURIMismatch')) { + $result = $this->_verifyDiscoverySingle($endpoint, + $to_match_1_0); + } - if (Auth_OpenID::isFailure($result)) { - return $result; - } else { - return $endpoint; + if (Auth_OpenID::isFailure($result)) { + // oidutil.log("Error attempting to use stored + // discovery information: " + str(e)) + // oidutil.log("Attempting discovery to + // verify endpoint") + } else { + return $endpoint; + } } + + // Endpoint is either bad (failed verification) or None + return $this->_discoverAndVerify($to_match->claimed_id, + array($to_match, $to_match_1_0)); } /** @@ -953,10 +1059,16 @@ class Auth_OpenID_GenericConsumer { } } - if ($to_match->claimed_id != $endpoint->claimed_id) { + // Fragments do not influence discovery, so we can't compare a + // claimed identifier with a fragment to discovered + // information. + list($defragged_claimed_id, $_) = + Auth_OpenID::urldefrag($to_match->claimed_id); + + if ($defragged_claimed_id != $endpoint->claimed_id) { return new Auth_OpenID_FailureResponse($endpoint, sprintf('Claimed ID does not match (different subjects!), ' . - 'Expected %s, got %s', $to_match->claimed_id, + 'Expected %s, got %s', $defragged_claimed_id, $endpoint->claimed_id)); } @@ -1012,111 +1124,117 @@ class Auth_OpenID_GenericConsumer { ($to_match->local_id !== null)) { return new Auth_OpenID_FailureResponse($endpoint, 'openid.identity is present without openid.claimed_id'); - } else if (($to_match->claimed_id !== null) && - ($to_match->local_id === null)) { + } + + if (($to_match->claimed_id !== null) && + ($to_match->local_id === null)) { return new Auth_OpenID_FailureResponse($endpoint, 'openid.claimed_id is present without openid.identity'); - } else if ($to_match->claimed_id === null) { + } + + if ($to_match->claimed_id === null) { // This is a response without identifiers, so there's // really no checking that we can do, so return an // endpoint that's for the specified `openid.op_endpoint' return Auth_OpenID_ServiceEndpoint::fromOPEndpointURL( $to_match->server_url); - } else if (!$endpoint) { + } + + if (!$endpoint) { // The claimed ID doesn't match, so we have to do // discovery again. This covers not using sessions, OP // identifier endpoints and responses that didn't match // the original request. // oidutil.log('No pre-discovered information supplied.') - return $this->_discoverAndVerify($to_match); - } else if ($to_match->claimed_id != $endpoint->claimed_id) { - // oidutil.log('Mismatched pre-discovered session data. ' - // 'Claimed ID in session=%s, in assertion=%s' % - // (endpoint.claimed_id, to_match.claimed_id)) - return $this->_discoverAndVerify($to_match); + return $this->_discoverAndVerify($to_match->claimed_id, + array($to_match)); } else { + // The claimed ID matches, so we use the endpoint that we // discovered in initiation. This should be the most // common case. $result = $this->_verifyDiscoverySingle($endpoint, $to_match); if (Auth_OpenID::isFailure($result)) { - return $result; + $endpoint = $this->_discoverAndVerify($to_match->claimed_id, + array($to_match)); + if (Auth_OpenID::isFailure($endpoint)) { + return $endpoint; + } } + } - return $endpoint; + // The endpoint we return should have the claimed ID from the + // message we just verified, fragment and all. + if ($endpoint->claimed_id != $to_match->claimed_id) { + $endpoint->claimed_id = $to_match->claimed_id; } - // Never reached. + return $endpoint; } /** * @access private */ - function _discoverAndVerify($to_match) + function _discoverAndVerify($claimed_id, $to_match_endpoints) { - // oidutil.log('Performing discovery on %s' % (to_match.claimed_id,)) + // oidutil.log('Performing discovery on %s' % (claimed_id,)) list($unused, $services) = call_user_func($this->discoverMethod, - $to_match->claimed_id, + $claimed_id, $this->fetcher); + if (!$services) { return new Auth_OpenID_FailureResponse(null, sprintf("No OpenID information found at %s", - $to_match->claimed_id)); + $claimed_id)); } + return $this->_verifyDiscoveryServices($claimed_id, $services, + $to_match_endpoints); + } + + /** + * @access private + */ + function _verifyDiscoveryServices($claimed_id, + &$services, &$to_match_endpoints) + { // Search the services resulting from discovery to find one // that matches the information from the assertion - $failure_messages = array(); foreach ($services as $endpoint) { - $result = $this->_verifyDiscoverySingle($endpoint, $to_match); - - if (Auth_OpenID::isFailure($result)) { - $failure_messages[] = $result; - } else { - // It matches, so discover verification has - // succeeded. Return this endpoint. - return $endpoint; + foreach ($to_match_endpoints as $to_match_endpoint) { + $result = $this->_verifyDiscoverySingle($endpoint, + $to_match_endpoint); + + if (!Auth_OpenID::isFailure($result)) { + // It matches, so discover verification has + // succeeded. Return this endpoint. + return $endpoint; + } } } return new Auth_OpenID_FailureResponse(null, sprintf('No matching endpoint found after discovering %s', - $to_match->claimed_id)); + $claimed_id)); } /** + * Extract the nonce from an OpenID 1 response. Return the nonce + * from the BARE_NS since we independently check the return_to + * arguments are the same as those in the response message. + * + * See the openid1_nonce_query_arg_name class variable + * + * @returns $nonce The nonce as a string or null + * * @access private */ function _idResGetNonceOpenID1($message, $endpoint) { - $return_to = $message->getArg(Auth_OpenID_OPENID1_NS, - 'return_to'); - if ($return_to === null) { - return null; - } - - $parsed_url = parse_url($return_to); - - if (!array_key_exists('query', $parsed_url)) { - return null; - } - - $query = $parsed_url['query']; - $pairs = Auth_OpenID::parse_str($query); - - if ($pairs === null) { - return null; - } - - foreach ($pairs as $k => $v) { - if ($k == $this->openid1_nonce_query_arg_name) { - return $v; - } - } - - return null; + return $message->getArg(Auth_OpenID_BARE_NS, + $this->openid1_nonce_query_arg_name); } /** @@ -1160,9 +1278,9 @@ class Auth_OpenID_GenericConsumer { /** * @access private */ - function _idResCheckForFields($message, $signed_list) + function _idResCheckForFields($message) { - $basic_fields = array('return_to', 'assoc_handle', 'sig'); + $basic_fields = array('return_to', 'assoc_handle', 'sig', 'signed'); $basic_sig_fields = array('return_to', 'identity'); $require_fields = array( @@ -1177,7 +1295,8 @@ class Auth_OpenID_GenericConsumer { Auth_OpenID_OPENID2_NS => array_merge($basic_sig_fields, array('response_nonce', 'claimed_id', - 'assoc_handle')), + 'assoc_handle', + 'op_endpoint')), Auth_OpenID_OPENID1_NS => array_merge($basic_sig_fields, array('nonce')) ); @@ -1189,6 +1308,14 @@ class Auth_OpenID_GenericConsumer { } } + $signed_list_str = $message->getArg(Auth_OpenID_OPENID_NS, + 'signed', + Auth_OpenID_NO_DEFAULT); + if (Auth_OpenID::isFailure($signed_list_str)) { + return $signed_list_str; + } + $signed_list = explode(',', $signed_list_str); + foreach ($require_sigs[$message->getOpenIDNamespace()] as $field) { // Field is present and not in signed list if ($message->hasKey(Auth_OpenID_OPENID_NS, $field) && @@ -1226,44 +1353,18 @@ class Auth_OpenID_GenericConsumer { function _createCheckAuthRequest($message) { $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed'); - if ($signed === null) { - return null; - } - - $whitelist = array('assoc_handle', 'sig', - 'signed', 'invalidate_handle'); - - $check_args = array(); - - foreach ($whitelist as $k) { - $val = $message->getArg(Auth_OpenID_OPENID_NS, $k); - if ($val !== null) { - $check_args[$k] = $val; - } - } - - $signed = $message->getArg(Auth_OpenID_OPENID_NS, - 'signed'); - if ($signed) { foreach (explode(',', $signed) as $k) { - if ($k == 'ns') { - $check_args['ns'] = $message->getOpenIDNamespace(); - continue; - } - - if (!$message->hasKey(Auth_OpenID_OPENID_NS, - $k)) { + $value = $message->getAliasedArg($k); + if ($value === null) { return null; } - - $val = $message->getAliasedArg($k); - $check_args[$k] = $val; } } - - $check_args['mode'] = 'check_authentication'; - return Auth_OpenID_Message::fromOpenIDArgs($check_args); + $ca_message = $message->copy(); + $ca_message->setArg(Auth_OpenID_OPENID_NS, 'mode', + 'check_authentication'); + return $ca_message; } /** @@ -1289,6 +1390,28 @@ class Auth_OpenID_GenericConsumer { return false; } + /** + * Adapt a POST response to a Message. + * + * @param $response Result of a POST to an OpenID endpoint. + * + * @access private + */ + function _httpResponseToMessage($response, $server_url) + { + // Should this function be named Message.fromHTTPResponse instead? + $response_message = Auth_OpenID_Message::fromKVForm($response->body); + + if ($response->status == 400) { + return Auth_OpenID_ServerErrorContainer::fromMessage( + $response_message); + } else if ($response->status != 200 and $response->status != 206) { + return null; + } + + return $response_message; + } + /** * @access private */ @@ -1298,19 +1421,10 @@ class Auth_OpenID_GenericConsumer { $resp = $this->fetcher->post($server_url, $body); if ($resp === null) { - return Auth_OpenID_ServerErrorContainer::fromMessage(''); - } - - $response_message = Auth_OpenID_Message::fromKVForm($resp->body); - - if ($resp->status == 400) { - return Auth_OpenID_ServerErrorContainer::fromMessage( - $response_message); - } else if ($resp->status != 200) { return null; } - return $response_message; + return $this->_httpResponseToMessage($resp, $server_url); } /** @@ -1338,6 +1452,47 @@ class Auth_OpenID_GenericConsumer { return $assoc; } + /** + * Handle ServerErrors resulting from association requests. + * + * @return $result If server replied with an C{unsupported-type} + * error, return a tuple of supported C{association_type}, + * C{session_type}. Otherwise logs the error and returns null. + * + * @access private + */ + function _extractSupportedAssociationType(&$server_error, &$endpoint, + $assoc_type) + { + // Any error message whose code is not 'unsupported-type' + // should be considered a total failure. + if (($server_error->error_code != 'unsupported-type') || + ($server_error->message->isOpenID1())) { + return null; + } + + // The server didn't like the association/session type that we + // sent, and it sent us back a message that might tell us how + // to handle it. + + // Extract the session_type and assoc_type from the error + // message + $assoc_type = $server_error->message->getArg(Auth_OpenID_OPENID_NS, + 'assoc_type'); + + $session_type = $server_error->message->getArg(Auth_OpenID_OPENID_NS, + 'session_type'); + + if (($assoc_type === null) || ($session_type === null)) { + return null; + } else if (!$this->negotiator->isAllowed($assoc_type, + $session_type)) { + return null; + } else { + return array($assoc_type, $session_type); + } + } + /** * @access private */ @@ -1356,42 +1511,12 @@ class Auth_OpenID_GenericConsumer { if (is_a($assoc, 'Auth_OpenID_ServerErrorContainer')) { $why = $assoc; - // Any error message whose code is not 'unsupported-type' - // should be considered a total failure. - if (($why->error_code != 'unsupported-type') || - ($why->message->isOpenID1())) { - // oidutil.log( - // 'Server error when requesting an association from %r: %s' - // % (endpoint.server_url, why.error_text)) - return null; - } + $supportedTypes = $this->_extractSupportedAssociationType( + $why, $endpoint, $assoc_type); - // The server didn't like the association/session type - // that we sent, and it sent us back a message that - // might tell us how to handle it. - // oidutil.log( - // 'Unsupported association type %s: %s' % (assoc_type, - // why.error_text,)) + if ($supportedTypes !== null) { + list($assoc_type, $session_type) = $supportedTypes; - // Extract the session_type and assoc_type from the - // error message - $assoc_type = $why->message->getArg(Auth_OpenID_OPENID_NS, - 'assoc_type'); - - $session_type = $why->message->getArg(Auth_OpenID_OPENID_NS, - 'session_type'); - - if (($assoc_type === null) || ($session_type === null)) { - // oidutil.log('Server responded with unsupported association ' - // 'session but did not supply a fallback.') - return null; - } else if (!$this->negotiator->isAllowed($assoc_type, - $session_type)) { - // fmt = ('Server sent unsupported session/association type: ' - // 'session_type=%s, assoc_type=%s') - // oidutil.log(fmt % (session_type, assoc_type)) - return null; - } else { // Attempt to create an association from the assoc_type // and session_type that the server told us it // supported. @@ -1409,10 +1534,12 @@ class Auth_OpenID_GenericConsumer { } else { return $assoc; } + } else { + return null; } + } else { + return $assoc; } - - return $assoc; } /** @@ -1447,18 +1574,16 @@ class Auth_OpenID_GenericConsumer { Auth_OpenID_OPENID_NS, 'assoc_type', Auth_OpenID_NO_DEFAULT); - if ($assoc_type === null) { - return new Auth_OpenID_FailureResponse(null, - 'assoc_type missing from association response'); + if (Auth_OpenID::isFailure($assoc_type)) { + return $assoc_type; } $assoc_handle = $assoc_response->getArg( Auth_OpenID_OPENID_NS, 'assoc_handle', Auth_OpenID_NO_DEFAULT); - if ($assoc_handle === null) { - return new Auth_OpenID_FailureResponse(null, - 'assoc_handle missing from association response'); + if (Auth_OpenID::isFailure($assoc_handle)) { + return $assoc_handle; } // expires_in is a base-10 string. The Python parsing will @@ -1469,14 +1594,16 @@ class Auth_OpenID_GenericConsumer { Auth_OpenID_OPENID_NS, 'expires_in', Auth_OpenID_NO_DEFAULT); - if ($expires_in_str === null) { - return new Auth_OpenID_FailureResponse(null, - 'expires_in missing from association response'); + if (Auth_OpenID::isFailure($expires_in_str)) { + return $expires_in_str; } $expires_in = Auth_OpenID::intval($expires_in_str); if ($expires_in === false) { - return null; + + $err = sprintf("Could not parse expires_in from association ". + "response %s", print_r($assoc_response, true)); + return new Auth_OpenID_FailureResponse(null, $err); } // OpenID 1 has funny association session behaviour. @@ -1487,9 +1614,8 @@ class Auth_OpenID_GenericConsumer { Auth_OpenID_OPENID2_NS, 'session_type', Auth_OpenID_NO_DEFAULT); - if ($session_type === null) { - return new Auth_OpenID_FailureResponse(null, - 'session_type missing from association response'); + if (Auth_OpenID::isFailure($session_type)) { + return $session_type; } } @@ -1627,8 +1753,7 @@ class Auth_OpenID_AuthRequest { $this->assoc = $assoc; $this->endpoint =& $endpoint; $this->return_to_args = array(); - $this->message = new Auth_OpenID_Message(); - $this->message->setOpenIDNamespace( + $this->message = new Auth_OpenID_Message( $endpoint->preferredNamespace()); $this->_anonymous = false; } @@ -1665,7 +1790,7 @@ class Auth_OpenID_AuthRequest { */ function addExtensionArg($namespace, $key, $value) { - $this->message->setArg($namespace, $key, $value); + return $this->message->setArg($namespace, $key, $value); } /** @@ -1813,6 +1938,24 @@ class Auth_OpenID_AuthRequest { $form_tag_attrs); } + /** + * Get a complete html document that will autosubmit the request + * to the IDP. + * + * Wraps formMarkup. See the documentation for that function. + */ + function htmlMarkup($realm, $return_to=null, $immediate=false, + $form_tag_attrs=null) + { + $form = $this->formMarkup($realm, $return_to, $immediate, + $form_tag_attrs); + + if (Auth_OpenID::isFailure($form)) { + return $form; + } + return Auth_OpenID::autoSubmitHTML($form); + } + function shouldSendRedirect() { return $this->endpoint->compatibilityMode(); @@ -1836,6 +1979,31 @@ class Auth_OpenID_ConsumerResponse { $this->identity_url = $endpoint->claimed_id; } } + + /** + * Return the display identifier for this response. + * + * The display identifier is related to the Claimed Identifier, but the + * two are not always identical. The display identifier is something the + * user should recognize as what they entered, whereas the response's + * claimed identifier (in the identity_url attribute) may have extra + * information for better persistence. + * + * URLs will be stripped of their fragments for display. XRIs will + * display the human-readable identifier (i-name) instead of the + * persistent identifier (i-number). + * + * Use the display identifier in your user interface. Use + * identity_url for querying your database or authorization server. + * + */ + function getDisplayIdentifier() + { + if ($this->endpoint !== null) { + return $this->endpoint->getDisplayIdentifier(); + } + return null; + } } /** @@ -1915,6 +2083,9 @@ class Auth_OpenID_SuccessResponse extends Auth_OpenID_ConsumerResponse { $args = array(); $msg_args = $this->message->getArgs($ns_uri); + if (Auth_OpenID::isFailure($msg_args)) { + return null; + } foreach ($msg_args as $key => $value) { if (!$this->isSigned($ns_uri, $key)) { diff --git a/lib/Auth/OpenID/CryptUtil.php b/lib/Auth/OpenID/CryptUtil.php index 8d7e06983442ff383eedaff04a6d5c63ea050ebd..aacc3cd3974857b0bce24e468e702d600dd71ab4 100644 --- a/lib/Auth/OpenID/CryptUtil.php +++ b/lib/Auth/OpenID/CryptUtil.php @@ -11,8 +11,8 @@ * @access private * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ if (!defined('Auth_OpenID_RAND_SOURCE')) { diff --git a/lib/Auth/OpenID/DatabaseConnection.php b/lib/Auth/OpenID/DatabaseConnection.php index 3f4515fa59bce89b3f5442d378dd85c501540001..9db6e0eb3f3c3cd0a771d97e108b04e19bb40341 100644 --- a/lib/Auth/OpenID/DatabaseConnection.php +++ b/lib/Auth/OpenID/DatabaseConnection.php @@ -6,8 +6,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** diff --git a/lib/Auth/OpenID/DiffieHellman.php b/lib/Auth/OpenID/DiffieHellman.php index 9b999096aa38034faebc6618aa3976ea0533fd49..f4ded7eba57191bc665d78844a1f206e10cf2f67 100644 --- a/lib/Auth/OpenID/DiffieHellman.php +++ b/lib/Auth/OpenID/DiffieHellman.php @@ -10,13 +10,12 @@ * @access private * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ require_once 'Auth/OpenID.php'; require_once 'Auth/OpenID/BigMath.php'; -require_once 'Auth/OpenID/HMACSHA1.php'; function Auth_OpenID_getDefaultMod() { @@ -90,27 +89,6 @@ class Auth_OpenID_DiffieHellman { return $this->public; } - /** - * Generate the arguments for an OpenID Diffie-Hellman association - * request - */ - function getAssocArgs() - { - $cpub = $this->lib->longToBase64($this->getPublicKey()); - $args = array( - 'openid.dh_consumer_public' => $cpub, - 'openid.session_type' => 'DH-SHA1' - ); - - if ($this->lib->cmp($this->mod, Auth_OpenID_getDefaultMod()) || - $this->lib->cmp($this->gen, Auth_OpenID_getDefaultGen())) { - $args['openid.dh_modulus'] = $this->lib->longToBase64($this->mod); - $args['openid.dh_gen'] = $this->lib->longToBase64($this->gen); - } - - return $args; - } - function usingDefaultValues() { return ($this->mod == Auth_OpenID_getDefaultMod() && @@ -131,3 +109,5 @@ class Auth_OpenID_DiffieHellman { return $xsecret; } } + +?> diff --git a/lib/Auth/OpenID/Discover.php b/lib/Auth/OpenID/Discover.php index bf20b90f7b403e6068a4d21ac8f8ecbc3d77e665..62aeb1d2bc550db88247e1c22502cf912781737e 100644 --- a/lib/Auth/OpenID/Discover.php +++ b/lib/Auth/OpenID/Discover.php @@ -19,6 +19,8 @@ define('Auth_OpenID_TYPE_1_1', 'http://openid.net/signon/1.1'); define('Auth_OpenID_TYPE_1_0', 'http://openid.net/signon/1.0'); define('Auth_OpenID_TYPE_2_0_IDP', 'http://specs.openid.net/auth/2.0/server'); define('Auth_OpenID_TYPE_2_0', 'http://specs.openid.net/auth/2.0/signon'); +define('Auth_OpenID_RP_RETURN_TO_URL_TYPE', + 'http://specs.openid.net/auth/2.0/return_to'); function Auth_OpenID_getOpenIDTypeURIs() { @@ -26,7 +28,8 @@ function Auth_OpenID_getOpenIDTypeURIs() Auth_OpenID_TYPE_2_0, Auth_OpenID_TYPE_1_2, Auth_OpenID_TYPE_1_1, - Auth_OpenID_TYPE_1_0); + Auth_OpenID_TYPE_1_0, + Auth_OpenID_RP_RETURN_TO_URL_TYPE); } /** @@ -41,6 +44,28 @@ class Auth_OpenID_ServiceEndpoint { $this->local_id = null; $this->canonicalID = null; $this->used_yadis = false; // whether this came from an XRDS + $this->display_identifier = null; + } + + function getDisplayIdentifier() + { + if ($this->display_identifier) { + return $this->display_identifier; + } + if (! $this->claimed_id) { + return $this->claimed_id; + } + $parsed = parse_url($this->claimed_id); + $scheme = $parsed['scheme']; + $host = $parsed['host']; + $path = $parsed['path']; + if (array_key_exists('query', $parsed)) { + $query = $parsed['query']; + $no_frag = "$scheme://$host$path?$query"; + } else { + $no_frag = "$scheme://$host$path"; + } + return $no_frag; } function usesExtension($extension_uri) @@ -58,6 +83,29 @@ class Auth_OpenID_ServiceEndpoint { } } + /* + * Query this endpoint to see if it has any of the given type + * URIs. This is useful for implementing other endpoint classes + * that e.g. need to check for the presence of multiple versions + * of a single protocol. + * + * @param $type_uris The URIs that you wish to check + * + * @return all types that are in both in type_uris and + * $this->type_uris + */ + function matchTypes($type_uris) + { + $result = array(); + foreach ($type_uris as $test_uri) { + if ($this->supportsType($test_uri)) { + $result[] = $test_uri; + } + } + + return $result; + } + function supportsType($type_uri) { // Does this endpoint support this type? @@ -123,6 +171,45 @@ class Auth_OpenID_ServiceEndpoint { } } + /* + * Parse the given document as XRDS looking for OpenID services. + * + * @return array of Auth_OpenID_ServiceEndpoint or null if the + * document cannot be parsed. + */ + function fromXRDS($uri, $xrds_text) + { + $xrds =& Auth_Yadis_XRDS::parseXRDS($xrds_text); + + if ($xrds) { + $yadis_services = + $xrds->services(array('filter_MatchesAnyOpenIDType')); + return Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services); + } + + return null; + } + + /* + * Create endpoints from a DiscoveryResult. + * + * @param discoveryResult Auth_Yadis_DiscoveryResult + * @return array of Auth_OpenID_ServiceEndpoint or null if + * endpoints cannot be created. + */ + function fromDiscoveryResult($discoveryResult) + { + if ($discoveryResult->isXRDS()) { + return Auth_OpenID_ServiceEndpoint::fromXRDS( + $discoveryResult->normalized_uri, + $discoveryResult->response_text); + } else { + return Auth_OpenID_ServiceEndpoint::fromHTML( + $discoveryResult->normalized_uri, + $discoveryResult->response_text); + } + } + function fromHTML($uri, $html) { $discovery_types = array( @@ -328,7 +415,9 @@ function Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services) return $s; } -function Auth_OpenID_discoverWithYadis($uri, &$fetcher) +function Auth_OpenID_discoverWithYadis($uri, &$fetcher, + $endpoint_filter='Auth_OpenID_getOPOrUserServices', + $discover_function=null) { // Discover OpenID services for a URI. Tries Yadis and falls back // on old-style <link rel='...'> discovery if Yadis fails. @@ -337,8 +426,15 @@ function Auth_OpenID_discoverWithYadis($uri, &$fetcher) // came back for that URI at all. I don't think falling back to // OpenID 1.0 discovery on the same URL will help, so don't bother // to catch it. + if ($discover_function === null) { + $discover_function = array('Auth_Yadis_Yadis', 'discover'); + } + $openid_services = array(); - $response = Auth_Yadis_Yadis::discover($uri, $fetcher); + + $response = call_user_func_array($discover_function, + array($uri, &$fetcher)); + $yadis_url = $response->normalized_uri; $yadis_services = array(); @@ -346,14 +442,11 @@ function Auth_OpenID_discoverWithYadis($uri, &$fetcher) return array($uri, array()); } - $xrds =& Auth_Yadis_XRDS::parseXRDS($response->response_text); - - if ($xrds) { - $yadis_services = - $xrds->services(array('filter_MatchesAnyOpenIDType')); - } + $openid_services = Auth_OpenID_ServiceEndpoint::fromXRDS( + $yadis_url, + $response->response_text); - if (!$yadis_services) { + if (!$openid_services) { if ($response->isXRDS()) { return Auth_OpenID_discoverWithoutYadis($uri, $fetcher); @@ -364,39 +457,25 @@ function Auth_OpenID_discoverWithYadis($uri, &$fetcher) $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML( $yadis_url, $response->response_text); - } else { - $openid_services = Auth_OpenID_makeOpenIDEndpoints($yadis_url, - $yadis_services); } - $openid_services = Auth_OpenID_getOPOrUserServices($openid_services); + $openid_services = call_user_func_array($endpoint_filter, + array(&$openid_services)); + return array($yadis_url, $openid_services); } function Auth_OpenID_discoverURI($uri, &$fetcher) { - $parsed = parse_url($uri); - - if ($parsed && isset($parsed['scheme']) && - isset($parsed['host'])) { - if (!in_array($parsed['scheme'], array('http', 'https'))) { - // raise DiscoveryFailure('URI scheme is not HTTP or HTTPS', None) - return array($uri, array()); - } - } else { - $uri = 'http://' . $uri; - } - $uri = Auth_OpenID::normalizeUrl($uri); - return Auth_OpenID_discoverWithYadis($uri, - $fetcher); + return Auth_OpenID_discoverWithYadis($uri, $fetcher); } function Auth_OpenID_discoverWithoutYadis($uri, &$fetcher) { $http_resp = @$fetcher->get($uri); - if ($http_resp->status != 200) { + if ($http_resp->status != 200 and $http_resp->status != 206) { return array($uri, array()); } @@ -427,6 +506,7 @@ function Auth_OpenID_discoverXRI($iname, &$fetcher) for ($i = 0; $i < count($openid_services); $i++) { $openid_services[$i]->canonicalID = $canonicalID; $openid_services[$i]->claimed_id = $canonicalID; + $openid_services[$i]->display_identifier = $iname; } // FIXME: returned xri should probably be in some normal form @@ -465,4 +545,4 @@ function Auth_OpenID_discover($uri, &$fetcher) return $result; } -?> \ No newline at end of file +?> diff --git a/lib/Auth/OpenID/DumbStore.php b/lib/Auth/OpenID/DumbStore.php index ef1a37f828b0d0a12a04484ba664560a312dc46f..22fd2d36610bca79a0feb1d6d3dd6944aaf23c0b 100644 --- a/lib/Auth/OpenID/DumbStore.php +++ b/lib/Auth/OpenID/DumbStore.php @@ -10,15 +10,15 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** * Import the interface for creating a new store class. */ require_once 'Auth/OpenID/Interface.php'; -require_once 'Auth/OpenID/HMACSHA1.php'; +require_once 'Auth/OpenID/HMAC.php'; /** * This is a store for use in the worst case, when you have no way of diff --git a/lib/Auth/OpenID/Extension.php b/lib/Auth/OpenID/Extension.php index 73b5d502eacba77d97bb136bb2e00c2d4a7c5119..f362a4b389e51e572bd225daba7e860aebe5050a 100644 --- a/lib/Auth/OpenID/Extension.php +++ b/lib/Auth/OpenID/Extension.php @@ -41,8 +41,12 @@ class Auth_OpenID_Extension { */ function toMessage(&$message) { - if ($message->namespaces->addAlias($this->ns_uri, - $this->ns_alias) === null) { + $implicit = $message->isOpenID1(); + $added = $message->namespaces->addAlias($this->ns_uri, + $this->ns_alias, + $implicit); + + if ($added === null) { if ($message->namespaces->getAlias($this->ns_uri) != $this->ns_alias) { return null; diff --git a/lib/Auth/OpenID/FileStore.php b/lib/Auth/OpenID/FileStore.php index dba0e4d5093a558423e2e3142649148ae864a370..29d8d20e76bec41b14d33f383a468013174f7f2d 100644 --- a/lib/Auth/OpenID/FileStore.php +++ b/lib/Auth/OpenID/FileStore.php @@ -10,8 +10,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** @@ -19,7 +19,8 @@ */ require_once 'Auth/OpenID.php'; require_once 'Auth/OpenID/Interface.php'; -require_once 'Auth/OpenID/HMACSHA1.php'; +require_once 'Auth/OpenID/HMAC.php'; +require_once 'Auth/OpenID/Nonce.php'; /** * This is a filesystem-based store for OpenID associations and @@ -115,6 +116,28 @@ class Auth_OpenID_FileStore extends Auth_OpenID_OpenIDStore { } } + function cleanupNonces() + { + global $Auth_OpenID_SKEW; + + $nonces = Auth_OpenID_FileStore::_listdir($this->nonce_dir); + $now = time(); + + $removed = 0; + // Check all nonces for expiry + foreach ($nonces as $nonce_fname) { + $base = basename($nonce_fname); + $parts = explode('-', $base, 2); + $timestamp = $parts[0]; + $timestamp = intval($timestamp, 16); + if (abs($timestamp - $now) > $Auth_OpenID_SKEW) { + Auth_OpenID_FileStore::_removeIfPresent($nonce_fname); + $removed += 1; + } + } + return $removed; + } + /** * Create a unique filename for a given server url and * handle. This implementation does not assume anything about the @@ -231,16 +254,15 @@ class Auth_OpenID_FileStore extends Auth_OpenID_OpenIDStore { // strip off the path to do the comparison $name = basename($filename); foreach ($association_files as $association_file) { - if (strpos($association_file, $name) === 0) { + $base = basename($association_file); + if (strpos($base, $name) === 0) { $matching_files[] = $association_file; } } $matching_associations = array(); // read the matching files and sort by time issued - foreach ($matching_files as $name) { - $full_name = $this->association_dir . DIRECTORY_SEPARATOR . - $name; + foreach ($matching_files as $full_name) { $association = $this->_getAssociation($full_name); if ($association !== null) { $matching_associations[] = array($association->issued, @@ -337,11 +359,17 @@ class Auth_OpenID_FileStore extends Auth_OpenID_OpenIDStore { */ function useNonce($server_url, $timestamp, $salt) { + global $Auth_OpenID_SKEW; + if (!$this->active) { trigger_error("FileStore no longer active", E_USER_ERROR); return null; } + if ( abs($timestamp - time()) > $Auth_OpenID_SKEW ) { + return False; + } + if ($server_url) { list($proto, $rest) = explode('://', $server_url, 2); } else { @@ -435,18 +463,6 @@ class Auth_OpenID_FileStore extends Auth_OpenID_OpenIDStore { } } - function getExpired() - { - $urls = array(); - foreach ($this->_allAssocs() as $pair) { - list($_, $assoc) = $pair; - if ($assoc->getExpiresIn() <= 0) { - $urls[] = $assoc->server_url; - } - } - return $urls; - } - /** * @access private */ @@ -526,7 +542,7 @@ class Auth_OpenID_FileStore extends Auth_OpenID_OpenIDStore { $files = array(); while (false !== ($filename = readdir($handle))) { if (!in_array($filename, array('.', '..'))) { - $files[] = $filename; + $files[] = $dir . DIRECTORY_SEPARATOR . $filename; } } return $files; @@ -584,6 +600,19 @@ class Auth_OpenID_FileStore extends Auth_OpenID_OpenIDStore { { return @unlink($filename); } + + function cleanupAssociations() + { + $removed = 0; + foreach ($this->_allAssocs() as $pair) { + list($assoc_filename, $assoc) = $pair; + if ($assoc->getExpiresIn() == 0) { + $this->_removeIfPresent($assoc_filename); + $removed += 1; + } + } + return $removed; + } } ?> diff --git a/lib/Auth/OpenID/HMACSHA1.php b/lib/Auth/OpenID/HMAC.php similarity index 94% rename from lib/Auth/OpenID/HMACSHA1.php rename to lib/Auth/OpenID/HMAC.php index 9fc293e7fb7d4b5777ca1b6e2a817c2d791b85c0..ec42db8dfc6f0aa60ed2f23b1d46d8dac9948358 100644 --- a/lib/Auth/OpenID/HMACSHA1.php +++ b/lib/Auth/OpenID/HMAC.php @@ -10,8 +10,8 @@ * @access private * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ require_once 'Auth/OpenID.php'; @@ -88,7 +88,7 @@ if (function_exists('hash_hmac') && function Auth_OpenID_HMACSHA256($key, $text) { // Return raw MAC (not hex string). - return hash_hmac('sha256', $key, $text, true); + return hash_hmac('sha256', $text, $key, true); } define('Auth_OpenID_HMACSHA256_SUPPORTED', true); diff --git a/lib/Auth/OpenID/Interface.php b/lib/Auth/OpenID/Interface.php index 9e7d496ec75ac80689ed5ed2d4468d65c51995f5..f4c6062f8c7fcfc12b87242b9e9f03f29991d430 100644 --- a/lib/Auth/OpenID/Interface.php +++ b/lib/Auth/OpenID/Interface.php @@ -9,8 +9,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** @@ -20,9 +20,9 @@ * consumers. If you want to create an SQL-driven store, please see * then {@link Auth_OpenID_SQLStore} class. * - * Change: Version 2.0 removed the storeNonce and getAuthKey methods, - * and changed the behavior of the useNonce method to support one-way - * nonces. + * Change: Version 2.0 removed the storeNonce, getAuthKey, and isDumb + * methods, and changed the behavior of the useNonce method to support + * one-way nonces. * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> @@ -47,6 +47,60 @@ class Auth_OpenID_OpenIDStore { "not implemented", E_USER_ERROR); } + /* + * Remove expired nonces from the store. + * + * Discards any nonce from storage that is old enough that its + * timestamp would not pass useNonce(). + * + * This method is not called in the normal operation of the + * library. It provides a way for store admins to keep their + * storage from filling up with expired data. + * + * @return the number of nonces expired + */ + function cleanupNonces() + { + trigger_error("Auth_OpenID_OpenIDStore::cleanupNonces ". + "not implemented", E_USER_ERROR); + } + + /* + * Remove expired associations from the store. + * + * This method is not called in the normal operation of the + * library. It provides a way for store admins to keep their + * storage from filling up with expired data. + * + * @return the number of associations expired. + */ + function cleanupAssociations() + { + trigger_error("Auth_OpenID_OpenIDStore::cleanupAssociations ". + "not implemented", E_USER_ERROR); + } + + /* + * Shortcut for cleanupNonces(), cleanupAssociations(). + * + * This method is not called in the normal operation of the + * library. It provides a way for store admins to keep their + * storage from filling up with expired data. + */ + function cleanup() + { + return array($this->cleanupNonces(), + $this->cleanupAssociations()); + } + + /** + * Report whether this storage supports cleanup + */ + function supportsCleanup() + { + return true; + } + /** * This method returns an Association object from storage that * matches the server URL and, if specified, handle. It returns @@ -56,7 +110,7 @@ class Auth_OpenID_OpenIDStore { * If no handle is specified, the store may return any association * which matches the server URL. If multiple associations are * valid, the recommended return value for this method is the one - * that will remain valid for the longest duration. + * most recently issued. * * This method is allowed (and encouraged) to garbage collect * expired associations when found. This method must not return @@ -132,15 +186,6 @@ class Auth_OpenID_OpenIDStore { "not implemented", E_USER_ERROR); } - /** - * Return all server URLs that have expired associations. - */ - function getExpired() - { - trigger_error("Auth_OpenID_OpenIDStore::getExpired ". - "not implemented", E_USER_ERROR); - } - /** * Removes all entries from the store; implementation is optional. */ diff --git a/lib/Auth/OpenID/KVForm.php b/lib/Auth/OpenID/KVForm.php index 6075c44f00518a434cca4489a082f52e2c54f2d8..fb342a00136606534306ae62dba9f02828ab3aa1 100644 --- a/lib/Auth/OpenID/KVForm.php +++ b/lib/Auth/OpenID/KVForm.php @@ -11,8 +11,8 @@ * @access private * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** diff --git a/lib/Auth/OpenID/MemcachedStore.php b/lib/Auth/OpenID/MemcachedStore.php new file mode 100644 index 0000000000000000000000000000000000000000..d357c6b11d7bb7ca2d92e76367be1a61f99a5a4e --- /dev/null +++ b/lib/Auth/OpenID/MemcachedStore.php @@ -0,0 +1,208 @@ +<?php + +/** + * This file supplies a memcached store backend for OpenID servers and + * consumers. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author Artemy Tregubenko <me@arty.name> + * @copyright 2008 JanRain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + * Contributed by Open Web Technologies <http://openwebtech.ru/> + */ + +/** + * Import the interface for creating a new store class. + */ +require_once 'Auth/OpenID/Interface.php'; + +/** + * This is a memcached-based store for OpenID associations and + * nonces. + * + * As memcache has limit of 250 chars for key length, + * server_url, handle and salt are hashed with sha1(). + * + * Most of the methods of this class are implementation details. + * People wishing to just use this store need only pay attention to + * the constructor. + * + * @package OpenID + */ +class Auth_OpenID_MemcachedStore extends Auth_OpenID_OpenIDStore { + + /** + * Initializes a new {@link Auth_OpenID_MemcachedStore} instance. + * Just saves memcached object as property. + * + * @param resource connection Memcache connection resourse + */ + function Auth_OpenID_MemcachedStore($connection, $compress = false) + { + $this->connection = $connection; + $this->compress = $compress ? MEMCACHE_COMPRESSED : 0; + } + + /** + * Store association until its expiration time in memcached. + * Overwrites any existing association with same server_url and + * handle. Handles list of associations for every server. + */ + function storeAssociation($server_url, $association) + { + // create memcached keys for association itself + // and list of associations for this server + $associationKey = $this->associationKey($server_url, + $association->handle); + $serverKey = $this->associationServerKey($server_url); + + // get list of associations + $serverAssociations = $this->connection->get($serverKey); + + // if no such list, initialize it with empty array + if (!$serverAssociations) { + $serverAssociations = array(); + } + // and store given association key in it + $serverAssociations[$association->issued] = $associationKey; + + // save associations' keys list + $this->connection->set( + $serverKey, + $serverAssociations, + $this->compress + ); + // save association itself + $this->connection->set( + $associationKey, + $association, + $this->compress, + $association->issued + $association->lifetime); + } + + /** + * Read association from memcached. If no handle given + * and multiple associations found, returns latest issued + */ + function getAssociation($server_url, $handle = null) + { + // simple case: handle given + if ($handle !== null) { + // get association, return null if failed + $association = $this->connection->get( + $this->associationKey($server_url, $handle)); + return $association ? $association : null; + } + + // no handle given, working with list + // create key for list of associations + $serverKey = $this->associationServerKey($server_url); + + // get list of associations + $serverAssociations = $this->connection->get($serverKey); + // return null if failed or got empty list + if (!$serverAssociations) { + return null; + } + + // get key of most recently issued association + $keys = array_keys($serverAssociations); + sort($keys); + $lastKey = $serverAssociations[array_pop($keys)]; + + // get association, return null if failed + $association = $this->connection->get($lastKey); + return $association ? $association : null; + } + + /** + * Immediately delete association from memcache. + */ + function removeAssociation($server_url, $handle) + { + // create memcached keys for association itself + // and list of associations for this server + $serverKey = $this->associationServerKey($server_url); + $associationKey = $this->associationKey($server_url, + $handle); + + // get list of associations + $serverAssociations = $this->connection->get($serverKey); + // return null if failed or got empty list + if (!$serverAssociations) { + return false; + } + + // ensure that given association key exists in list + $serverAssociations = array_flip($serverAssociations); + if (!array_key_exists($associationKey, $serverAssociations)) { + return false; + } + + // remove given association key from list + unset($serverAssociations[$associationKey]); + $serverAssociations = array_flip($serverAssociations); + + // save updated list + $this->connection->set( + $serverKey, + $serverAssociations, + $this->compress + ); + + // delete association + return $this->connection->delete($associationKey); + } + + /** + * Create nonce for server and salt, expiring after + * $Auth_OpenID_SKEW seconds. + */ + function useNonce($server_url, $timestamp, $salt) + { + global $Auth_OpenID_SKEW; + + // save one request to memcache when nonce obviously expired + if (abs($timestamp - time()) > $Auth_OpenID_SKEW) { + return false; + } + + // returns false when nonce already exists + // otherwise adds nonce + return $this->connection->add( + 'openid_nonce_' . sha1($server_url) . '_' . sha1($salt), + 1, // any value here + $this->compress, + $Auth_OpenID_SKEW); + } + + /** + * Memcache key is prefixed with 'openid_association_' string. + */ + function associationKey($server_url, $handle = null) + { + return 'openid_association_' . sha1($server_url) . '_' . sha1($handle); + } + + /** + * Memcache key is prefixed with 'openid_association_' string. + */ + function associationServerKey($server_url) + { + return 'openid_association_server_' . sha1($server_url); + } + + /** + * Report that this storage doesn't support cleanup + */ + function supportsCleanup() + { + return false; + } +} + +?> \ No newline at end of file diff --git a/lib/Auth/OpenID/Message.php b/lib/Auth/OpenID/Message.php index 95d1103d41828073475ec9331fab43364c97d88d..5ab115a86e0670d15dd32a9c75be6ef907309565 100644 --- a/lib/Auth/OpenID/Message.php +++ b/lib/Auth/OpenID/Message.php @@ -12,6 +12,7 @@ require_once 'Auth/OpenID.php'; require_once 'Auth/OpenID/KVForm.php'; require_once 'Auth/Yadis/XML.php'; +require_once 'Auth/OpenID/Consumer.php'; // For Auth_OpenID_FailureResponse // This doesn't REALLY belong here, but where is better? define('Auth_OpenID_IDENTIFIER_SELECT', @@ -23,6 +24,13 @@ define('Auth_OpenID_SREG_URI', 'http://openid.net/sreg/1.0'); // The OpenID 1.X namespace URI define('Auth_OpenID_OPENID1_NS', 'http://openid.net/signon/1.0'); +define('Auth_OpenID_THE_OTHER_OPENID1_NS', 'http://openid.net/signon/1.1'); + +function Auth_OpenID_isOpenID1($ns) +{ + return ($ns == Auth_OpenID_THE_OTHER_OPENID1_NS) || + ($ns == Auth_OpenID_OPENID1_NS); +} // The OpenID 2.0 namespace URI define('Auth_OpenID_OPENID2_NS', 'http://specs.openid.net/auth/2.0'); @@ -42,6 +50,10 @@ define('Auth_OpenID_BARE_NS', 'Bare namespace'); // return null instead of returning a default. define('Auth_OpenID_NO_DEFAULT', 'NO DEFAULT ALLOWED'); +// Limit, in bytes, of identity provider and return_to URLs, including +// response payload. See OpenID 1.1 specification, Appendix D. +define('Auth_OpenID_OPENID1_URL_LIMIT', 2047); + // All OpenID protocol fields. Used to check namespace aliases. global $Auth_OpenID_OPENID_PROTOCOL_FIELDS; $Auth_OpenID_OPENID_PROTOCOL_FIELDS = array( @@ -266,6 +278,7 @@ class Auth_OpenID_NamespaceMap { { $this->alias_to_namespace = new Auth_OpenID_Mapping(); $this->namespace_to_alias = new Auth_OpenID_Mapping(); + $this->implicit_namespaces = array(); } function getAlias($namespace_uri) @@ -295,7 +308,12 @@ class Auth_OpenID_NamespaceMap { return $this->namespace_to_alias->items(); } - function addAlias($namespace_uri, $desired_alias) + function isImplicit($namespace_uri) + { + return in_array($namespace_uri, $this->implicit_namespaces); + } + + function addAlias($namespace_uri, $desired_alias, $implicit=false) { // Add an alias from this namespace URI to the desired alias global $Auth_OpenID_OPENID_PROTOCOL_FIELDS; @@ -303,14 +321,15 @@ class Auth_OpenID_NamespaceMap { // Check that desired_alias is not an openid protocol field as // per the spec. if (in_array($desired_alias, $Auth_OpenID_OPENID_PROTOCOL_FIELDS)) { - // "%r is not an allowed namespace alias" % (desired_alias,); + Auth_OpenID::log("\"%s\" is not an allowed namespace alias", + $desired_alias); return null; } // Check that desired_alias does not contain a period as per // the spec. if (strpos($desired_alias, '.') !== false) { - // "%r must not contain a dot" % (desired_alias,) + Auth_OpenID::log('"%s" must not contain a dot', $desired_alias); return null; } @@ -321,7 +340,8 @@ class Auth_OpenID_NamespaceMap { if (($current_namespace_uri !== null) && ($current_namespace_uri != $namespace_uri)) { - // Cannot map because previous mapping exists + Auth_OpenID::log('Cannot map "%s" because previous mapping exists', + $namespace_uri); return null; } @@ -330,9 +350,9 @@ class Auth_OpenID_NamespaceMap { $alias = $this->namespace_to_alias->get($namespace_uri); if (($alias !== null) && ($alias != $desired_alias)) { - // fmt = ('Cannot map %r to alias %r. ' - // 'It is already mapped to alias %r') - // raise KeyError(fmt % (namespace_uri, desired_alias, alias)) + Auth_OpenID::log('Cannot map %s to alias %s. ' . + 'It is already mapped to alias %s', + $namespace_uri, $desired_alias, $alias); return null; } @@ -341,6 +361,9 @@ class Auth_OpenID_NamespaceMap { $this->alias_to_namespace->set($desired_alias, $namespace_uri); $this->namespace_to_alias->set($namespace_uri, $desired_alias); + if ($implicit) { + array_push($this->implicit_namespaces, $namespace_uri); + } return $desired_alias; } @@ -396,6 +419,7 @@ class Auth_OpenID_Message { // Create an empty Message $this->allowed_openid_namespaces = array( Auth_OpenID_OPENID1_NS, + Auth_OpenID_THE_OTHER_OPENID1_NS, Auth_OpenID_OPENID2_NS); $this->args = new Auth_OpenID_Mapping(); @@ -403,13 +427,14 @@ class Auth_OpenID_Message { if ($openid_namespace === null) { $this->_openid_ns_uri = null; } else { - $this->setOpenIDNamespace($openid_namespace); + $implicit = Auth_OpenID_isOpenID1($openid_namespace); + $this->setOpenIDNamespace($openid_namespace, $implicit); } } function isOpenID1() { - return $this->getOpenIDNamespace() == Auth_OpenID_OPENID1_NS; + return Auth_OpenID_isOpenID1($this->getOpenIDNamespace()); } function isOpenID2() @@ -500,8 +525,7 @@ class Auth_OpenID_Message { } else if (($ns_alias == Auth_OpenID_NULL_NAMESPACE) && ($ns_key == 'ns')) { // null namespace - if ($this->namespaces->addAlias($value, - Auth_OpenID_NULL_NAMESPACE) === null) { + if ($this->setOpenIDNamespace($value, false) === false) { return false; } } else { @@ -509,38 +533,25 @@ class Auth_OpenID_Message { } } - // Ensure that there is an OpenID namespace definition - $openid_ns_uri = - $this->namespaces->getNamespaceURI(Auth_OpenID_NULL_NAMESPACE); - - if ($openid_ns_uri === null) { - $openid_ns_uri = Auth_OpenID_OPENID1_NS; + if (!$this->getOpenIDNamespace()) { + if ($this->setOpenIDNamespace(Auth_OpenID_OPENID1_NS, true) === + false) { + return false; + } } - $this->setOpenIDNamespace($openid_ns_uri); - // Actually put the pairs into the appropriate namespaces foreach ($ns_args as $triple) { list($ns_alias, $ns_key, $value) = $triple; $ns_uri = $this->namespaces->getNamespaceURI($ns_alias); if ($ns_uri === null) { - // Only try to map an alias to a default if it's an - // OpenID 1.x message. - if ($openid_ns_uri == Auth_OpenID_OPENID1_NS) { - foreach ($Auth_OpenID_registered_aliases - as $alias => $uri) { - if ($alias == $ns_alias) { - $ns_uri = $uri; - break; - } - } - } - + $ns_uri = $this->_getDefaultNamespace($ns_alias); if ($ns_uri === null) { - $ns_uri = $openid_ns_uri; + + $ns_uri = Auth_OpenID_OPENID_NS; $ns_key = sprintf('%s.%s', $ns_alias, $ns_key); } else { - $this->namespaces->addAlias($ns_uri, $ns_alias); + $this->namespaces->addAlias($ns_uri, $ns_alias, true); } } @@ -550,16 +561,32 @@ class Auth_OpenID_Message { return true; } - function setOpenIDNamespace($openid_ns_uri) + function _getDefaultNamespace($mystery_alias) + { + global $Auth_OpenID_registered_aliases; + if ($this->isOpenID1()) { + return @$Auth_OpenID_registered_aliases[$mystery_alias]; + } + return null; + } + + function setOpenIDNamespace($openid_ns_uri, $implicit) { if (!in_array($openid_ns_uri, $this->allowed_openid_namespaces)) { - // raise ValueError('Invalid null namespace: %r' % (openid_ns_uri,)) + Auth_OpenID::log('Invalid null namespace: "%s"', $openid_ns_uri); + return false; + } + + $succeeded = $this->namespaces->addAlias($openid_ns_uri, + Auth_OpenID_NULL_NAMESPACE, + $implicit); + if ($succeeded === false) { return false; } - $this->namespaces->addAlias($openid_ns_uri, - Auth_OpenID_NULL_NAMESPACE); $this->_openid_ns_uri = $openid_ns_uri; + + return true; } function getOpenIDNamespace() @@ -589,25 +616,15 @@ class Auth_OpenID_Message { // Add namespace definitions to the output foreach ($this->namespaces->iteritems() as $pair) { list($ns_uri, $alias) = $pair; - + if ($this->namespaces->isImplicit($ns_uri)) { + continue; + } if ($alias == Auth_OpenID_NULL_NAMESPACE) { - if ($ns_uri != Auth_OpenID_OPENID1_NS) { - $args['openid.ns'] = $ns_uri; - } else { - // drop the default null namespace - // definition. This potentially changes a message - // since we have no way of knowing whether it was - // explicitly specified at the time the message - // was parsed. The vast majority of the time, this - // will be the right thing to do. Possibly this - // could look in the signed list. - } + $ns_key = 'openid.ns'; } else { - if ($this->getOpenIDNamespace() != Auth_OpenID_OPENID1_NS) { - $ns_key = 'openid.ns.' . $alias; - $args[$ns_key] = $ns_uri; - } + $ns_key = 'openid.ns.' . $alias; } + $args[$ns_key] = $ns_uri; } foreach ($this->args->items() as $pair) { @@ -716,19 +733,20 @@ class Auth_OpenID_Message { if ($namespace == Auth_OpenID_OPENID_NS) { if ($this->_openid_ns_uri === null) { - // raise UndefinedOpenIDNamespace('OpenID namespace not set') - return null; + return new Auth_OpenID_FailureResponse(null, + 'OpenID namespace not set'); } else { $namespace = $this->_openid_ns_uri; } } if (($namespace != Auth_OpenID_BARE_NS) && - (!is_string($namespace))) { - // raise TypeError( - // "Namespace must be BARE_NS, OPENID_NS or a string. got %r" - // % (namespace,)) - return null; + (!is_string($namespace))) { + //TypeError + $err_msg = sprintf("Namespace must be Auth_OpenID_BARE_NS, ". + "Auth_OpenID_OPENID_NS or a string. got %s", + print_r($namespace, true)); + return new Auth_OpenID_FailureResponse(null, $err_msg); } if (($namespace != Auth_OpenID_BARE_NS) && @@ -749,10 +767,11 @@ class Auth_OpenID_Message { function hasKey($namespace, $ns_key) { $namespace = $this->_fixNS($namespace); - if ($namespace !== null) { - return $this->args->contains(array($namespace, $ns_key)); - } else { + if (Auth_OpenID::isFailure($namespace)) { + // XXX log me return false; + } else { + return $this->args->contains(array($namespace, $ns_key)); } } @@ -760,6 +779,9 @@ class Auth_OpenID_Message { { // Get the key for a particular namespaced argument $namespace = $this->_fixNS($namespace); + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } if ($namespace == Auth_OpenID_BARE_NS) { return $ns_key; } @@ -785,15 +807,17 @@ class Auth_OpenID_Message { // Get a value for a namespaced key. $namespace = $this->_fixNS($namespace); - if ($namespace !== null) { + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } else { if ((!$this->args->contains(array($namespace, $key))) && - ($default == Auth_OpenID_NO_DEFAULT)) { - return null; + ($default == Auth_OpenID_NO_DEFAULT)) { + $err_msg = sprintf("Namespace %s missing required field %s", + $namespace, $key); + return new Auth_OpenID_FailureResponse(null, $err_msg); } else { return $this->args->get(array($namespace, $key), $default); } - } else { - return null; } } @@ -802,7 +826,9 @@ class Auth_OpenID_Message { // Get the arguments that are defined for this namespace URI $namespace = $this->_fixNS($namespace); - if ($namespace !== null) { + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } else { $stuff = array(); foreach ($this->args->items() as $pair) { list($key, $value) = $pair; @@ -814,8 +840,6 @@ class Auth_OpenID_Message { return $stuff; } - - return array(); } function updateArgs($namespace, $updates) @@ -824,13 +848,13 @@ class Auth_OpenID_Message { $namespace = $this->_fixNS($namespace); - if ($namespace !== null) { + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } else { foreach ($updates as $k => $v) { $this->setArg($namespace, $k, $v); } return true; - } else { - return false; } } @@ -839,14 +863,14 @@ class Auth_OpenID_Message { // Set a single argument in this namespace $namespace = $this->_fixNS($namespace); - if ($namespace !== null) { + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } else { $this->args->set(array($namespace, $key), $value); if ($namespace !== Auth_OpenID_BARE_NS) { $this->namespaces->add($namespace); } return true; - } else { - return false; } } @@ -854,22 +878,34 @@ class Auth_OpenID_Message { { $namespace = $this->_fixNS($namespace); - if ($namespace !== null) { - return $this->args->del(array($namespace, $key)); + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; } else { - return false; + return $this->args->del(array($namespace, $key)); } } function getAliasedArg($aliased_key, $default = null) { + if ($aliased_key == 'ns') { + // Return the namespace URI for the OpenID namespace + return $this->getOpenIDNamespace(); + } + $parts = explode('.', $aliased_key, 2); if (count($parts) != 2) { $ns = null; } else { list($alias, $key) = $parts; - $ns = $this->namespaces->getNamespaceURI($alias); + + if ($alias == 'ns') { + // Return the namespace URI for a namespace alias + // parameter. + return $this->namespaces->getNamespaceURI($key); + } else { + $ns = $this->namespaces->getNamespaceURI($alias); + } } if ($ns === null) { @@ -881,4 +917,4 @@ class Auth_OpenID_Message { } } -?> \ No newline at end of file +?> diff --git a/lib/Auth/OpenID/MySQLStore.php b/lib/Auth/OpenID/MySQLStore.php index 4b2d29d0f5350b746ca6872dd90676d63e633ae9..eb08af01626644e6b591d61805488ebb944c67ed 100644 --- a/lib/Auth/OpenID/MySQLStore.php +++ b/lib/Auth/OpenID/MySQLStore.php @@ -24,25 +24,26 @@ class Auth_OpenID_MySQLStore extends Auth_OpenID_SQLStore { { $this->sql['nonce_table'] = "CREATE TABLE %s (\n". - " server_url VARCHAR(2047),\n". - " timestamp INTEGER,\n". - " salt CHAR(40),\n". + " server_url VARCHAR(2047) NOT NULL,\n". + " timestamp INTEGER NOT NULL,\n". + " salt CHAR(40) NOT NULL,\n". " UNIQUE (server_url(255), timestamp, salt)\n". - ") TYPE=InnoDB"; + ") ENGINE=InnoDB"; $this->sql['assoc_table'] = "CREATE TABLE %s (\n". - " server_url BLOB,\n". - " handle VARCHAR(255),\n". - " secret BLOB,\n". - " issued INTEGER,\n". - " lifetime INTEGER,\n". - " assoc_type VARCHAR(64),\n". + " server_url BLOB NOT NULL,\n". + " handle VARCHAR(255) NOT NULL,\n". + " secret BLOB NOT NULL,\n". + " issued INTEGER NOT NULL,\n". + " lifetime INTEGER NOT NULL,\n". + " assoc_type VARCHAR(64) NOT NULL,\n". " PRIMARY KEY (server_url(255), handle)\n". - ") TYPE=InnoDB"; + ") ENGINE=InnoDB"; $this->sql['set_assoc'] = - "REPLACE INTO %s VALUES (?, ?, !, ?, ?, ?)"; + "REPLACE INTO %s (server_url, handle, secret, issued,\n". + " lifetime, assoc_type) VALUES (?, ?, !, ?, ?, ?)"; $this->sql['get_assocs'] = "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ". @@ -58,8 +59,11 @@ class Auth_OpenID_MySQLStore extends Auth_OpenID_SQLStore { $this->sql['add_nonce'] = "INSERT INTO %s (server_url, timestamp, salt) VALUES (?, ?, ?)"; - $this->sql['get_expired'] = - "SELECT server_url FROM %s WHERE issued + lifetime < ?"; + $this->sql['clean_nonce'] = + "DELETE FROM %s WHERE timestamp < ?"; + + $this->sql['clean_assoc'] = + "DELETE FROM %s WHERE issued + lifetime < ?"; } /** diff --git a/lib/Auth/OpenID/Nonce.php b/lib/Auth/OpenID/Nonce.php index 71a33e1a6e076c2a18a7de143f83cbb37f1fcfdf..effecac38521647c46e8902cd1a99b6b01b71536 100644 --- a/lib/Auth/OpenID/Nonce.php +++ b/lib/Auth/OpenID/Nonce.php @@ -96,7 +96,11 @@ function Auth_OpenID_mkNonce($when = null) $salt = Auth_OpenID_CryptUtil::randomString( 6, Auth_OpenID_Nonce_CHRS); if ($when === null) { - $when = gmmktime(); + // It's safe to call time() with no arguments; it returns a + // GMT unix timestamp on PHP 4 and PHP 5. gmmktime() with no + // args returns a local unix timestamp on PHP 4, so don't use + // that. + $when = time(); } $time_str = gmstrftime(Auth_OpenID_Nonce_TIME_FMT, $when); return $time_str . $salt; diff --git a/lib/Auth/OpenID/PAPE.php b/lib/Auth/OpenID/PAPE.php new file mode 100644 index 0000000000000000000000000000000000000000..62cba8a912d46cf6691d3920bab55966d9cde10b --- /dev/null +++ b/lib/Auth/OpenID/PAPE.php @@ -0,0 +1,301 @@ +<?php + +/** + * An implementation of the OpenID Provider Authentication Policy + * Extension 1.0 + * + * See: + * http://openid.net/developers/specs/ + */ + +require_once "Auth/OpenID/Extension.php"; + +define('Auth_OpenID_PAPE_NS_URI', + "http://specs.openid.net/extensions/pape/1.0"); + +define('PAPE_AUTH_MULTI_FACTOR_PHYSICAL', + 'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical'); +define('PAPE_AUTH_MULTI_FACTOR', + 'http://schemas.openid.net/pape/policies/2007/06/multi-factor'); +define('PAPE_AUTH_PHISHING_RESISTANT', + 'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant'); + +define('PAPE_TIME_VALIDATOR', + '^[0-9]{4,4}-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]Z$'); +/** + * A Provider Authentication Policy request, sent from a relying party + * to a provider + * + * preferred_auth_policies: The authentication policies that + * the relying party prefers + * + * max_auth_age: The maximum time, in seconds, that the relying party + * wants to allow to have elapsed before the user must re-authenticate + */ +class Auth_OpenID_PAPE_Request extends Auth_OpenID_Extension { + + var $ns_alias = 'pape'; + var $ns_uri = Auth_OpenID_PAPE_NS_URI; + + function Auth_OpenID_PAPE_Request($preferred_auth_policies=null, + $max_auth_age=null) + { + if ($preferred_auth_policies === null) { + $preferred_auth_policies = array(); + } + + $this->preferred_auth_policies = $preferred_auth_policies; + $this->max_auth_age = $max_auth_age; + } + + /** + * Add an acceptable authentication policy URI to this request + * + * This method is intended to be used by the relying party to add + * acceptable authentication types to the request. + * + * policy_uri: The identifier for the preferred type of + * authentication. + */ + function addPolicyURI($policy_uri) + { + if (!in_array($policy_uri, $this->preferred_auth_policies)) { + $this->preferred_auth_policies[] = $policy_uri; + } + } + + function getExtensionArgs() + { + $ns_args = array( + 'preferred_auth_policies' => + implode(' ', $this->preferred_auth_policies) + ); + + if ($this->max_auth_age !== null) { + $ns_args['max_auth_age'] = strval($this->max_auth_age); + } + + return $ns_args; + } + + /** + * Instantiate a Request object from the arguments in a checkid_* + * OpenID message + */ + function fromOpenIDRequest($request) + { + $obj = new Auth_OpenID_PAPE_Request(); + $args = $request->message->getArgs(Auth_OpenID_PAPE_NS_URI); + + if ($args === null || $args === array()) { + return null; + } + + $obj->parseExtensionArgs($args); + return $obj; + } + + /** + * Set the state of this request to be that expressed in these + * PAPE arguments + * + * @param args: The PAPE arguments without a namespace + */ + function parseExtensionArgs($args) + { + // preferred_auth_policies is a space-separated list of policy + // URIs + $this->preferred_auth_policies = array(); + + $policies_str = Auth_OpenID::arrayGet($args, 'preferred_auth_policies'); + if ($policies_str) { + foreach (explode(' ', $policies_str) as $uri) { + if (!in_array($uri, $this->preferred_auth_policies)) { + $this->preferred_auth_policies[] = $uri; + } + } + } + + // max_auth_age is base-10 integer number of seconds + $max_auth_age_str = Auth_OpenID::arrayGet($args, 'max_auth_age'); + if ($max_auth_age_str) { + $this->max_auth_age = Auth_OpenID::intval($max_auth_age_str); + } else { + $this->max_auth_age = null; + } + } + + /** + * Given a list of authentication policy URIs that a provider + * supports, this method returns the subsequence of those types + * that are preferred by the relying party. + * + * @param supported_types: A sequence of authentication policy + * type URIs that are supported by a provider + * + * @return array The sub-sequence of the supported types that are + * preferred by the relying party. This list will be ordered in + * the order that the types appear in the supported_types + * sequence, and may be empty if the provider does not prefer any + * of the supported authentication types. + */ + function preferredTypes($supported_types) + { + $result = array(); + + foreach ($supported_types as $st) { + if (in_array($st, $this->preferred_auth_policies)) { + $result[] = $st; + } + } + return $result; + } +} + +/** + * A Provider Authentication Policy response, sent from a provider to + * a relying party + */ +class Auth_OpenID_PAPE_Response extends Auth_OpenID_Extension { + + var $ns_alias = 'pape'; + var $ns_uri = Auth_OpenID_PAPE_NS_URI; + + function Auth_OpenID_PAPE_Response($auth_policies=null, $auth_time=null, + $nist_auth_level=null) + { + if ($auth_policies) { + $this->auth_policies = $auth_policies; + } else { + $this->auth_policies = array(); + } + + $this->auth_time = $auth_time; + $this->nist_auth_level = $nist_auth_level; + } + + /** + * Add a authentication policy to this response + * + * This method is intended to be used by the provider to add a + * policy that the provider conformed to when authenticating the + * user. + * + * @param policy_uri: The identifier for the preferred type of + * authentication. + */ + function addPolicyURI($policy_uri) + { + if (!in_array($policy_uri, $this->auth_policies)) { + $this->auth_policies[] = $policy_uri; + } + } + + /** + * Create an Auth_OpenID_PAPE_Response object from a successful + * OpenID library response. + * + * @param success_response $success_response A SuccessResponse + * from Auth_OpenID_Consumer::complete() + * + * @returns: A provider authentication policy response from the + * data that was supplied with the id_res response. + */ + function fromSuccessResponse($success_response) + { + $obj = new Auth_OpenID_PAPE_Response(); + + // PAPE requires that the args be signed. + $args = $success_response->getSignedNS(Auth_OpenID_PAPE_NS_URI); + + if ($args === null || $args === array()) { + return null; + } + + $result = $obj->parseExtensionArgs($args); + + if ($result === false) { + return null; + } else { + return $obj; + } + } + + /** + * Parse the provider authentication policy arguments into the + * internal state of this object + * + * @param args: unqualified provider authentication policy + * arguments + * + * @param strict: Whether to return false when bad data is + * encountered + * + * @return null The data is parsed into the internal fields of + * this object. + */ + function parseExtensionArgs($args, $strict=false) + { + $policies_str = Auth_OpenID::arrayGet($args, 'auth_policies'); + if ($policies_str && $policies_str != "none") { + $this->auth_policies = explode(" ", $policies_str); + } + + $nist_level_str = Auth_OpenID::arrayGet($args, 'nist_auth_level'); + if ($nist_level_str !== null) { + $nist_level = Auth_OpenID::intval($nist_level_str); + + if ($nist_level === false) { + if ($strict) { + return false; + } else { + $nist_level = null; + } + } + + if (0 <= $nist_level && $nist_level < 5) { + $this->nist_auth_level = $nist_level; + } else if ($strict) { + return false; + } + } + + $auth_time = Auth_OpenID::arrayGet($args, 'auth_time'); + if ($auth_time !== null) { + if (ereg(PAPE_TIME_VALIDATOR, $auth_time)) { + $this->auth_time = $auth_time; + } else if ($strict) { + return false; + } + } + } + + function getExtensionArgs() + { + $ns_args = array(); + if (count($this->auth_policies) > 0) { + $ns_args['auth_policies'] = implode(' ', $this->auth_policies); + } else { + $ns_args['auth_policies'] = 'none'; + } + + if ($this->nist_auth_level !== null) { + if (!in_array($this->nist_auth_level, range(0, 4), true)) { + return false; + } + $ns_args['nist_auth_level'] = strval($this->nist_auth_level); + } + + if ($this->auth_time !== null) { + if (!ereg(PAPE_TIME_VALIDATOR, $this->auth_time)) { + return false; + } + + $ns_args['auth_time'] = $this->auth_time; + } + + return $ns_args; + } +} + +?> \ No newline at end of file diff --git a/lib/Auth/OpenID/Parse.php b/lib/Auth/OpenID/Parse.php index 7d485155ef21cc93a1484ebd6ba27c7be135056f..546f34f6be717eb05ca807d42fddcd9a89b944b9 100644 --- a/lib/Auth/OpenID/Parse.php +++ b/lib/Auth/OpenID/Parse.php @@ -75,8 +75,8 @@ * @access private * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** @@ -105,6 +105,9 @@ class Auth_OpenID_Parse { var $_attr_find = '\b(\w+)=("[^"]*"|\'[^\']*\'|[^\'"\s\/<>]+)'; + var $_open_tag_expr = "<%s\b"; + var $_close_tag_expr = "<((\/%s\b)|(%s[^>\/]*\/))>"; + function Auth_OpenID_Parse() { $this->_link_find = sprintf("/<link\b(?!:)([^>]*)(?!<)>/%s", @@ -136,6 +139,8 @@ class Auth_OpenID_Parse { */ function tagMatcher($tag_name, $close_tags = null) { + $expr = $this->_tag_expr; + if ($close_tags) { $options = implode("|", array_merge(array($tag_name), $close_tags)); $closer = sprintf("(?:%s)", $options); @@ -143,18 +148,49 @@ class Auth_OpenID_Parse { $closer = $tag_name; } - $expr = sprintf($this->_tag_expr, $tag_name, $closer); + $expr = sprintf($expr, $tag_name, $closer); return sprintf("/%s/%s", $expr, $this->_re_flags); } - function htmlFind() + function openTag($tag_name) { - return $this->tagMatcher('html'); + $expr = sprintf($this->_open_tag_expr, $tag_name); + return sprintf("/%s/%s", $expr, $this->_re_flags); + } + + function closeTag($tag_name) + { + $expr = sprintf($this->_close_tag_expr, $tag_name, $tag_name); + return sprintf("/%s/%s", $expr, $this->_re_flags); + } + + function htmlBegin($s) + { + $matches = array(); + $result = preg_match($this->openTag('html'), $s, + $matches, PREG_OFFSET_CAPTURE); + if ($result === false || !$matches) { + return false; + } + // Return the offset of the first match. + return $matches[0][1]; + } + + function htmlEnd($s) + { + $matches = array(); + $result = preg_match($this->closeTag('html'), $s, + $matches, PREG_OFFSET_CAPTURE); + if ($result === false || !$matches) { + return false; + } + // Return the offset of the first match. + return $matches[count($matches) - 1][1]; } function headFind() { - return $this->tagMatcher('head', array('body')); + return $this->tagMatcher('head', array('body', 'html')); } function replaceEntities($str) @@ -194,17 +230,24 @@ class Auth_OpenID_Parse { "", $html); - // Try to find the <HTML> tag. - $html_re = $this->htmlFind(); - $html_matches = array(); - if (!preg_match($html_re, $stripped, $html_matches)) { + $html_begin = $this->htmlBegin($stripped); + $html_end = $this->htmlEnd($stripped); + + if ($html_begin === false) { return array(); } + if ($html_end === false) { + $html_end = strlen($stripped); + } + + $stripped = substr($stripped, $html_begin, + $html_end - $html_begin); + // Try to find the <HEAD> tag. $head_re = $this->headFind(); $head_matches = array(); - if (!preg_match($head_re, $html_matches[0], $head_matches)) { + if (!preg_match($head_re, $stripped, $head_matches)) { return array(); } diff --git a/lib/Auth/OpenID/PostgreSQLStore.php b/lib/Auth/OpenID/PostgreSQLStore.php index ffbbc69b67c6a81da6ef3a9b297df6eb5972ed94..69d95e7b8dca452a131dad23001e56a2098784fb 100644 --- a/lib/Auth/OpenID/PostgreSQLStore.php +++ b/lib/Auth/OpenID/PostgreSQLStore.php @@ -23,13 +23,19 @@ class Auth_OpenID_PostgreSQLStore extends Auth_OpenID_SQLStore { function setSQL() { $this->sql['nonce_table'] = - "CREATE TABLE %s (server_url VARCHAR(2047), timestamp INTEGER, ". - "salt CHAR(40), UNIQUE (server_url, timestamp, salt))"; + "CREATE TABLE %s (server_url VARCHAR(2047) NOT NULL, ". + "timestamp INTEGER NOT NULL, ". + "salt CHAR(40) NOT NULL, ". + "UNIQUE (server_url, timestamp, salt))"; $this->sql['assoc_table'] = - "CREATE TABLE %s (server_url VARCHAR(2047), handle VARCHAR(255), ". - "secret BYTEA, issued INTEGER, lifetime INTEGER, ". - "assoc_type VARCHAR(64), PRIMARY KEY (server_url, handle), ". + "CREATE TABLE %s (server_url VARCHAR(2047) NOT NULL, ". + "handle VARCHAR(255) NOT NULL, ". + "secret BYTEA NOT NULL, ". + "issued INTEGER NOT NULL, ". + "lifetime INTEGER NOT NULL, ". + "assoc_type VARCHAR(64) NOT NULL, ". + "PRIMARY KEY (server_url, handle), ". "CONSTRAINT secret_length_constraint CHECK ". "(LENGTH(secret) <= 128))"; @@ -54,13 +60,16 @@ class Auth_OpenID_PostgreSQLStore extends Auth_OpenID_SQLStore { $this->sql['remove_assoc'] = "DELETE FROM %s WHERE server_url = ? AND handle = ?"; - $this->sql['get_expired'] = - "SELECT server_url FROM %s WHERE issued + lifetime < ?"; - $this->sql['add_nonce'] = "INSERT INTO %s (server_url, timestamp, salt) VALUES ". "(?, ?, ?)" ; + + $this->sql['clean_nonce'] = + "DELETE FROM %s WHERE timestamp < ?"; + + $this->sql['clean_assoc'] = + "DELETE FROM %s WHERE issued + lifetime < ?"; } /** diff --git a/lib/Auth/OpenID/SQLStore.php b/lib/Auth/OpenID/SQLStore.php index b71729dcac595fbcf7db3cce9986e8d533419f66..da93c6aa25c4ade331613962433eb5e863f03ddb 100644 --- a/lib/Auth/OpenID/SQLStore.php +++ b/lib/Auth/OpenID/SQLStore.php @@ -9,8 +9,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** @@ -27,12 +27,18 @@ $__Auth_OpenID_PEAR_AVAILABLE = @include_once 'DB.php'; * @access private */ require_once 'Auth/OpenID/Interface.php'; +require_once 'Auth/OpenID/Nonce.php'; /** * @access private */ require_once 'Auth/OpenID.php'; +/** + * @access private + */ +require_once 'Auth/OpenID/Nonce.php'; + /** * This is the parent class for the SQL stores, which contains the * logic common to all of the SQL stores. @@ -161,8 +167,9 @@ class Auth_OpenID_SQLStore extends Auth_OpenID_OpenIDStore { function tableExists($table_name) { return !$this->isError( - $this->connection->query("SELECT * FROM %s LIMIT 0", - $table_name)); + $this->connection->query( + sprintf("SELECT * FROM %s LIMIT 0", + $table_name))); } /** @@ -224,8 +231,7 @@ class Auth_OpenID_SQLStore extends Auth_OpenID_OpenIDStore { 'set_assoc', 'get_assoc', 'get_assocs', - 'remove_assoc', - 'get_expired', + 'remove_assoc' ); foreach ($required_sql_keys as $key) { @@ -248,7 +254,8 @@ class Auth_OpenID_SQLStore extends Auth_OpenID_OpenIDStore { array( 'value' => $this->nonces_table_name, 'keys' => array('nonce_table', - 'add_nonce') + 'add_nonce', + 'clean_nonce') ), array( 'value' => $this->associations_table_name, @@ -257,7 +264,7 @@ class Auth_OpenID_SQLStore extends Auth_OpenID_OpenIDStore { 'get_assoc', 'get_assocs', 'remove_assoc', - 'get_expired') + 'clean_assoc') ) ); @@ -399,20 +406,6 @@ class Auth_OpenID_SQLStore extends Auth_OpenID_OpenIDStore { return true; } - function getExpired() - { - $sql = $this->sql['get_expired']; - $result = $this->connection->getAll($sql, array(time())); - - $expired = array(); - - foreach ($result as $row) { - $expired[] = $row['server_url']; - } - - return $expired; - } - function getAssociation($server_url, $handle = null) { if ($handle !== null) { @@ -486,6 +479,12 @@ class Auth_OpenID_SQLStore extends Auth_OpenID_OpenIDStore { function useNonce($server_url, $timestamp, $salt) { + global $Auth_OpenID_SKEW; + + if ( abs($timestamp - time()) > $Auth_OpenID_SKEW ) { + return False; + } + return $this->_add_nonce($server_url, $timestamp, $salt); } @@ -545,6 +544,26 @@ class Auth_OpenID_SQLStore extends Auth_OpenID_OpenIDStore { return $result; } + + function cleanupNonces() + { + global $Auth_OpenID_SKEW; + $v = time() - $Auth_OpenID_SKEW; + + $this->connection->query($this->sql['clean_nonce'], array($v)); + $num = $this->connection->affectedRows(); + $this->connection->commit(); + return $num; + } + + function cleanupAssociations() + { + $this->connection->query($this->sql['clean_assoc'], + array(time())); + $num = $this->connection->affectedRows(); + $this->connection->commit(); + return $num; + } } ?> diff --git a/lib/Auth/OpenID/SQLiteStore.php b/lib/Auth/OpenID/SQLiteStore.php index debb5fe125e84ee2cce9e9b41d061d0c77168f3a..ec2bf58e46ce436d93cc8272433d792f4d6c93d2 100644 --- a/lib/Auth/OpenID/SQLiteStore.php +++ b/lib/Auth/OpenID/SQLiteStore.php @@ -35,9 +35,6 @@ class Auth_OpenID_SQLiteStore extends Auth_OpenID_SQLStore { "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ". "WHERE server_url = ?"; - $this->sql['get_expired'] = - "SELECT server_url FROM %s WHERE issued + lifetime < ?"; - $this->sql['get_assoc'] = "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ". "WHERE server_url = ? AND handle = ?"; @@ -47,6 +44,12 @@ class Auth_OpenID_SQLiteStore extends Auth_OpenID_SQLStore { $this->sql['add_nonce'] = "INSERT INTO %s (server_url, timestamp, salt) VALUES (?, ?, ?)"; + + $this->sql['clean_nonce'] = + "DELETE FROM %s WHERE timestamp < ?"; + + $this->sql['clean_assoc'] = + "DELETE FROM %s WHERE issued + lifetime < ?"; } /** diff --git a/lib/Auth/OpenID/SReg.php b/lib/Auth/OpenID/SReg.php index 208a6aba9fd0b451f6588e9548fcc19d42c72b2e..63280769fd6befbfce9cc512e70bf0b445772bfd 100644 --- a/lib/Auth/OpenID/SReg.php +++ b/lib/Auth/OpenID/SReg.php @@ -22,7 +22,7 @@ * object and adds it to the id_res response: * * $sreg_req = Auth_OpenID_SRegRequest::fromOpenIDRequest( - * $checkid_request->message); + * $checkid_request); * // [ get the user's approval and data, informing the user that * // the fields in sreg_response were requested ] * $sreg_resp = Auth_OpenID_SRegResponse::extractResponse( @@ -175,9 +175,10 @@ class Auth_OpenID_SRegRequest extends Auth_OpenID_SRegBase { */ function build($required=null, $optional=null, $policy_url=null, - $sreg_ns_uri=Auth_OpenID_SREG_NS_URI) + $sreg_ns_uri=Auth_OpenID_SREG_NS_URI, + $cls='Auth_OpenID_SRegRequest') { - $obj = new Auth_OpenID_SRegRequest(); + $obj = new $cls(); $obj->required = array(); $obj->optional = array(); @@ -204,23 +205,28 @@ class Auth_OpenID_SRegRequest extends Auth_OpenID_SRegBase { * that were requested in the OpenID request with the given * arguments * - * $message: The arguments that were given for this OpenID - * authentication request + * $request: The OpenID authentication request from which to + * extract an sreg request. + * + * $cls: name of class to use when creating sreg request object. + * Used for testing. * * Returns the newly created simple registration request */ - function fromOpenIDRequest($message) + function fromOpenIDRequest($request, $cls='Auth_OpenID_SRegRequest') { - $obj = Auth_OpenID_SRegRequest::build(); + + $obj = call_user_func_array(array($cls, 'build'), + array(null, null, null, Auth_OpenID_SREG_NS_URI, $cls)); // Since we're going to mess with namespace URI mapping, don't // mutate the object that was passed in. - $m = $message; + $m = $request->message; $obj->ns_uri = $obj->_getSRegNS($m); $args = $m->getArgs($obj->ns_uri); - if ($args === null) { + if ($args === null || Auth_OpenID::isFailure($args)) { return null; } @@ -478,7 +484,7 @@ class Auth_OpenID_SRegResponse extends Auth_OpenID_SRegBase { $args = $success_response->message->getArgs($obj->ns_uri); } - if ($args === null) { + if ($args === null || Auth_OpenID::isFailure($args)) { return null; } @@ -512,30 +518,4 @@ class Auth_OpenID_SRegResponse extends Auth_OpenID_SRegBase { } } -/** - * Convenience function for copying all the sreg data that was - * requested from a supplied set of sreg data into the response - * message. If no data were requested, no data will be sent. - * - * openid_request: The OpenID (checkid_*) request that may be - * requesting sreg data. - * - * data: The simple registration data to send. All requested fields - * that are present in this dictionary will be added to the response - * message. - * - * openid_response: The OpenID C{id_res} response to which the simple - * registration data should be added - * - * Does not return a value; updates the openid_response instead. - */ -function Auth_OpenID_sendSRegFields(&$openid_request, $data, &$openid_response) -{ - $sreg_request = Auth_OpenID_SRegRequest::fromOpenIDRequest( - $openid_request->message); - $sreg_response = Auth_OpenID_SRegResponse::extractResponse( - $sreg_request, $data); - $sreg_response->toMessage($openid_response->fields); -} - -?> \ No newline at end of file +?> diff --git a/lib/Auth/OpenID/Server.php b/lib/Auth/OpenID/Server.php index 013892c5c8ee88244d647382d157289e2abee29f..f1db4d872567ec6f311b150fde193f0a29926119 100644 --- a/lib/Auth/OpenID/Server.php +++ b/lib/Auth/OpenID/Server.php @@ -85,8 +85,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** @@ -107,25 +107,27 @@ define('AUTH_OPENID_HTTP_OK', 200); define('AUTH_OPENID_HTTP_REDIRECT', 302); define('AUTH_OPENID_HTTP_ERROR', 400); -global $_Auth_OpenID_Request_Modes, - $_Auth_OpenID_Encode_Kvform, - $_Auth_OpenID_Encode_Url; - /** * @access private */ +global $_Auth_OpenID_Request_Modes; $_Auth_OpenID_Request_Modes = array('checkid_setup', 'checkid_immediate'); /** * @access private */ -$_Auth_OpenID_Encode_Kvform = array('kfvorm'); +define('Auth_OpenID_ENCODE_KVFORM', 'kfvorm'); + +/** + * @access private + */ +define('Auth_OpenID_ENCODE_URL', 'URL/redirect'); /** * @access private */ -$_Auth_OpenID_Encode_Url = array('URL/redirect'); +define('Auth_OpenID_ENCODE_HTML_FORM', 'HTML form'); /** * @access private @@ -155,18 +157,24 @@ class Auth_OpenID_ServerError { $this->reference = $reference; } + function getReturnTo() + { + if ($this->message && + $this->message->hasKey(Auth_OpenID_OPENID_NS, 'return_to')) { + return $this->message->getArg(Auth_OpenID_OPENID_NS, + 'return_to'); + } else { + return null; + } + } + /** * Returns the return_to URL for the request which caused this * error. */ function hasReturnTo() { - if ($this->message) { - return $this->message->hasKey(Auth_OpenID_OPENID_NS, - 'return_to'); - } else { - return false; - } + return $this->getReturnTo() !== null; } /** @@ -180,15 +188,8 @@ class Auth_OpenID_ServerError { return null; } - $return_to = $this->message->getArg(Auth_OpenID_OPENID_NS, - 'return_to'); - if (!$return_to) { - return null; - } - - return Auth_OpenID::appendArgs($return_to, - array('openid.mode' => 'error', - 'openid.error' => $this->toString())); + $msg = $this->toMessage(); + return $msg->toURL($this->getReturnTo()); } /** @@ -204,6 +205,18 @@ class Auth_OpenID_ServerError { 'error' => $this->toString())); } + function toFormMarkup($form_tag_attrs=null) + { + $msg = $this->toMessage(); + return $msg->toFormMarkup($this->getReturnTo(), $form_tag_attrs); + } + + function toHTML($form_tag_attrs=null) + { + return Auth_OpenID::autoSubmitHTML( + $this->toFormMarkup($form_tag_attrs)); + } + function toMessage() { // Generate a Message object for sending to the relying party, @@ -226,18 +239,22 @@ class Auth_OpenID_ServerError { } /** - * Returns one of $_Auth_OpenID_Encode_Url, - * $_Auth_OpenID_Encode_Kvform, or null, depending on the type of + * Returns one of Auth_OpenID_ENCODE_URL, + * Auth_OpenID_ENCODE_KVFORM, or null, depending on the type of * encoding expected for this error's payload. */ function whichEncoding() { - global $_Auth_OpenID_Encode_Url, - $_Auth_OpenID_Encode_Kvform, - $_Auth_OpenID_Request_Modes; + global $_Auth_OpenID_Request_Modes; if ($this->hasReturnTo()) { - return $_Auth_OpenID_Encode_Url; + if ($this->message->isOpenID2() && + (strlen($this->encodeToURL()) > + Auth_OpenID_OPENID1_URL_LIMIT)) { + return Auth_OpenID_ENCODE_HTML_FORM; + } else { + return Auth_OpenID_ENCODE_URL; + } } if (!$this->message) { @@ -249,7 +266,7 @@ class Auth_OpenID_ServerError { if ($mode) { if (!in_array($mode, $_Auth_OpenID_Request_Modes)) { - return $_Auth_OpenID_Encode_Kvform; + return Auth_OpenID_ENCODE_KVFORM; } } return null; @@ -366,14 +383,6 @@ class Auth_OpenID_CheckAuthRequest extends Auth_OpenID_Request { $signed_list = $message->getArg(Auth_OpenID_OPENID_NS, 'signed'); $signed_list = explode(",", $signed_list); - foreach ($signed_list as $field) { - if (!$message->hasKey(Auth_OpenID_OPENID_NS, $field)) { - return new Auth_OpenID_ServerError($message, - sprintf("Couldn't find signed field %s in query", - $field)); - } - } - $signed = $message; if ($signed->hasKey(Auth_OpenID_OPENID_NS, 'mode')) { $signed->setArg(Auth_OpenID_OPENID_NS, 'mode', 'id_res'); @@ -591,7 +600,7 @@ class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request { function fromMessage($message, $server=null) { if ($message->isOpenID1()) { - $session_type = $message->getArg(Auth_OpenID_OPENID1_NS, + $session_type = $message->getArg(Auth_OpenID_OPENID_NS, 'session_type'); if ($session_type == 'no-encryption') { @@ -601,7 +610,7 @@ class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request { $session_type = 'no-encryption'; } } else { - $session_type = $message->getArg(Auth_OpenID_OPENID2_NS, + $session_type = $message->getArg(Auth_OpenID_OPENID_NS, 'session_type'); if ($session_type === null) { return new Auth_OpenID_ServerError($message, @@ -652,7 +661,8 @@ class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request { $response->fields->updateArgs(Auth_OpenID_OPENID_NS, $this->session->answer($assoc->secret)); - if ($this->session->session_type != 'no-encryption') { + if (! ($this->session->session_type == 'no-encryption' + && $this->message->isOpenID1())) { $response->fields->setArg(Auth_OpenID_OPENID_NS, 'session_type', $this->session->session_type); @@ -697,6 +707,12 @@ class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request { * @package OpenID */ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { + /** + * Return-to verification callback. Default is + * Auth_OpenID_verifyReturnTo from TrustRoot.php. + */ + var $verifyReturnTo = 'Auth_OpenID_verifyReturnTo'; + /** * The mode of this request. */ @@ -712,6 +728,12 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { */ var $trust_root = null; + /** + * The OpenID namespace for this request. + * deprecated since version 2.0.2 + */ + var $namespace; + function make(&$message, $identity, $return_to, $trust_root = null, $immediate = false, $assoc_handle = null, $server = null) { @@ -743,12 +765,17 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { function Auth_OpenID_CheckIDRequest($identity, $return_to, $trust_root = null, $immediate = false, - $assoc_handle = null, $server = null) + $assoc_handle = null, $server = null, + $claimed_id = null) { $this->namespace = Auth_OpenID_OPENID2_NS; $this->assoc_handle = $assoc_handle; $this->identity = $identity; - $this->claimed_id = $identity; + if ($claimed_id === null) { + $this->claimed_id = $identity; + } else { + $this->claimed_id = $claimed_id; + } $this->return_to = $return_to; $this->trust_root = $trust_root; $this->server =& $server; @@ -774,6 +801,26 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { ($this->trust_root == $other->trust_root)); } + /* + * Does the relying party publish the return_to URL for this + * response under the realm? It is up to the provider to set a + * policy for what kinds of realms should be allowed. This + * return_to URL verification reduces vulnerability to data-theft + * attacks based on open proxies, corss-site-scripting, or open + * redirectors. + * + * This check should only be performed after making sure that the + * return_to URL matches the realm. + * + * @return true if the realm publishes a document with the + * return_to URL listed, false if not or if discovery fails + */ + function returnToVerified() + { + return call_user_func_array($this->verifyReturnTo, + array($this->trust_root, $this->return_to)); + } + function fromMessage(&$message, $server) { $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode'); @@ -790,9 +837,7 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { $return_to = $message->getArg(Auth_OpenID_OPENID_NS, 'return_to'); - $namespace = $message->getOpenIDNamespace(); - - if (($namespace == Auth_OpenID_OPENID1_NS) && + if (($message->isOpenID1()) && (!$return_to)) { $fmt = "Missing required field 'return_to' from checkid request"; return new Auth_OpenID_ServerError($message, $fmt); @@ -800,42 +845,43 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { $identity = $message->getArg(Auth_OpenID_OPENID_NS, 'identity'); - - if ($identity && $message->isOpenID2()) { - $claimed_id = $message->getArg(Auth_OpenID_OPENID_NS, - 'claimed_id'); - if (!$claimed_id) { - return new Auth_OpenID_ServerError($message, - "OpenID 2.0 message contained openid.identity " . - "but not claimed_id"); + $claimed_id = $message->getArg(Auth_OpenID_OPENID_NS, 'claimed_id'); + if ($message->isOpenID1()) { + if ($identity === null) { + $s = "OpenID 1 message did not contain openid.identity"; + return new Auth_OpenID_ServerError($message, $s); } } else { - $claimed_id = null; - } - - if (($identity === null) && - ($namespace == Auth_OpenID_OPENID1_NS)) { - return new Auth_OpenID_ServerError($message, - "OpenID 1 message did not contain openid.identity"); + if ($identity && !$claimed_id) { + $s = "OpenID 2.0 message contained openid.identity but not " . + "claimed_id"; + return new Auth_OpenID_ServerError($message, $s); + } else if ($claimed_id && !$identity) { + $s = "OpenID 2.0 message contained openid.claimed_id " . + "but not identity"; + return new Auth_OpenID_ServerError($message, $s); + } } // There's a case for making self.trust_root be a TrustRoot // here. But if TrustRoot isn't currently part of the // "public" API, I'm not sure it's worth doing. - if ($namespace == Auth_OpenID_OPENID1_NS) { - $trust_root = $message->getArg(Auth_OpenID_OPENID_NS, - 'trust_root', - $return_to); + if ($message->isOpenID1()) { + $trust_root_param = 'trust_root'; } else { - $trust_root = $message->getArg(Auth_OpenID_OPENID_NS, - 'realm', - $return_to); + $trust_root_param = 'realm'; + } + $trust_root = $message->getArg(Auth_OpenID_OPENID_NS, + $trust_root_param); + if (! $trust_root) { + $trust_root = $return_to; + } - if (($return_to === null) && - ($trust_root === null)) { - return new Auth_OpenID_ServerError($message, - "openid.realm required when openid.return_to absent"); - } + if (! $message->isOpenID1() && + ($return_to === null) && + ($trust_root === null)) { + return new Auth_OpenID_ServerError($message, + "openid.realm required when openid.return_to absent"); } $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, @@ -873,7 +919,8 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { $tr = Auth_OpenID_TrustRoot::_parse($this->trust_root); if ($tr === false) { - return new Auth_OpenID_MalformedTrustRoot(null, $this->trust_root); + return new Auth_OpenID_MalformedTrustRoot($this->message, + $this->trust_root); } if ($this->return_to !== null) { @@ -931,7 +978,7 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { } if (!$server_url) { - if (($this->namespace != Auth_OpenID_OPENID1_NS) && + if ((!$this->message->isOpenID1()) && (!$this->server->op_endpoint)) { return new Auth_OpenID_ServerError(null, "server should be constructed with op_endpoint to " . @@ -943,7 +990,7 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { if ($allow) { $mode = 'id_res'; - } else if ($this->namespace == Auth_OpenID_OPENID1_NS) { + } else if ($this->message->isOpenID1()) { if ($this->immediate) { $mode = 'id_res'; } else { @@ -966,7 +1013,7 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { $response = new Auth_OpenID_ServerResponse($this); if ($claimed_id && - ($this->namespace == Auth_OpenID_OPENID1_NS)) { + ($this->message->isOpenID1())) { return new Auth_OpenID_ServerError(null, "claimed_id is new in OpenID 2.0 and not " . "available for ".$this->namespace); @@ -1008,7 +1055,7 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { $response_identity = null; } - if (($this->namespace == Auth_OpenID_OPENID1_NS) && + if (($this->message->isOpenID1()) && ($response_identity === null)) { return new Auth_OpenID_ServerError(null, "Request was an OpenID 1 request, so response must " . @@ -1017,16 +1064,20 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { $response->fields->updateArgs(Auth_OpenID_OPENID_NS, array('mode' => $mode, - 'op_endpoint' => $server_url, 'return_to' => $this->return_to, 'response_nonce' => Auth_OpenID_mkNonce())); + if (!$this->message->isOpenID1()) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'op_endpoint', $server_url); + } + if ($response_identity !== null) { $response->fields->setArg( Auth_OpenID_OPENID_NS, 'identity', $response_identity); - if ($this->namespace == Auth_OpenID_OPENID2_NS) { + if ($this->message->isOpenID2()) { $response->fields->setArg( Auth_OpenID_OPENID_NS, 'claimed_id', @@ -1039,7 +1090,7 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { 'mode', $mode); if ($this->immediate) { - if (($this->namespace == Auth_OpenID_OPENID1_NS) && + if (($this->message->isOpenID1()) && (!$server_url)) { return new Auth_OpenID_ServerError(null, 'setup_url is required for $allow=false \ @@ -1052,7 +1103,9 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { $this->trust_root, false, $this->assoc_handle, - $this->server); + $this->server, + $this->claimed_id); + $setup_request->message = $this->message; $setup_url = $setup_request->encodeToURL($server_url); @@ -1086,7 +1139,7 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { 'return_to' => $this->return_to); if ($this->trust_root) { - if ($this->namespace == Auth_OpenID_OPENID1_NS) { + if ($this->message->isOpenID1()) { $q['trust_root'] = $this->trust_root; } else { $q['realm'] = $this->trust_root; @@ -1097,9 +1150,9 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { $q['assoc_handle'] = $this->assoc_handle; } - $response = new Auth_OpenID_Message($this->namespace); - $response->updateArgs($this->namespace, $q); - + $response = new Auth_OpenID_Message( + $this->message->getOpenIDNamespace()); + $response->updateArgs(Auth_OpenID_OPENID_NS, $q); return $response->toURL($server_url); } @@ -1116,7 +1169,8 @@ class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { requests."); } - $response = new Auth_OpenID_Message($this->namespace); + $response = new Auth_OpenID_Message( + $this->message->getOpenIDNamespace()); $response->setArg(Auth_OpenID_OPENID_NS, 'mode', 'cancel'); return $response->toURL($this->return_to); } @@ -1137,22 +1191,63 @@ class Auth_OpenID_ServerResponse { function whichEncoding() { - global $_Auth_OpenID_Encode_Kvform, - $_Auth_OpenID_Request_Modes, - $_Auth_OpenID_Encode_Url; + global $_Auth_OpenID_Request_Modes; if (in_array($this->request->mode, $_Auth_OpenID_Request_Modes)) { - return $_Auth_OpenID_Encode_Url; + if ($this->fields->isOpenID2() && + (strlen($this->encodeToURL()) > + Auth_OpenID_OPENID1_URL_LIMIT)) { + return Auth_OpenID_ENCODE_HTML_FORM; + } else { + return Auth_OpenID_ENCODE_URL; + } } else { - return $_Auth_OpenID_Encode_Kvform; + return Auth_OpenID_ENCODE_KVFORM; } } + /* + * Returns the form markup for this response. + * + * @return str + */ + function toFormMarkup($form_tag_attrs=null) + { + return $this->fields->toFormMarkup($this->request->return_to, + $form_tag_attrs); + } + + /* + * Returns an HTML document containing the form markup for this + * response that autosubmits with javascript. + */ + function toHTML() + { + return Auth_OpenID::autoSubmitHTML($this->toFormMarkup()); + } + + /* + * Returns True if this response's encoding is ENCODE_HTML_FORM. + * Convenience method for server authors. + * + * @return bool + */ + function renderAsForm() + { + return $this->whichEncoding() == Auth_OpenID_ENCODE_HTML_FORM; + } + + function encodeToURL() { return $this->fields->toURL($this->request->return_to); } + function addExtension($extension_response) + { + $extension_response->toMessage($this->fields); + } + function needsSigning() { return $this->fields->getArg(Auth_OpenID_OPENID_NS, @@ -1354,21 +1449,21 @@ class Auth_OpenID_Encoder { */ function encode(&$response) { - global $_Auth_OpenID_Encode_Kvform, - $_Auth_OpenID_Encode_Url; - $cls = $this->responseFactory; $encode_as = $response->whichEncoding(); - if ($encode_as == $_Auth_OpenID_Encode_Kvform) { + if ($encode_as == Auth_OpenID_ENCODE_KVFORM) { $wr = new $cls(null, null, $response->encodeToKVForm()); if (is_a($response, 'Auth_OpenID_ServerError')) { $wr->code = AUTH_OPENID_HTTP_ERROR; } - } else if ($encode_as == $_Auth_OpenID_Encode_Url) { + } else if ($encode_as == Auth_OpenID_ENCODE_URL) { $location = $response->encodeToURL(); $wr = new $cls(AUTH_OPENID_HTTP_REDIRECT, array('location' => $location)); + } else if ($encode_as == Auth_OpenID_ENCODE_HTML_FORM) { + $wr = new $cls(AUTH_OPENID_HTTP_OK, array(), + $response->toFormMarkup()); } else { return new Auth_OpenID_EncodingError($response); } @@ -1392,20 +1487,8 @@ class Auth_OpenID_SigningEncoder extends Auth_OpenID_Encoder { * Sign an {@link Auth_OpenID_ServerResponse} and return an * {@link Auth_OpenID_WebResponse}. */ - function encode(&$response) { - - -// -// $trace = debug_backtrace(); -// foreach ($trace AS $t) { -// $str = ''; -// foreach ($t AS $te) { -// if (is_string($te)) $str .= $te . ' '; -// } -// error_log($str); -// } - - + function encode(&$response) + { // the isinstance is a bit of a kludge... it means there isn't // really an adapter to make the interfaces quite match. if (!is_a($response, 'Auth_OpenID_ServerError') && @@ -1457,12 +1540,34 @@ class Auth_OpenID_Decoder { $message = Auth_OpenID_Message::fromPostArgs($query); + if ($message === null) { + /* + * It's useful to have a Message attached to a + * ProtocolError, so we override the bad ns value to build + * a Message out of it. Kinda kludgy, since it's made of + * lies, but the parts that aren't lies are more useful + * than a 'None'. + */ + $old_ns = $query['openid.ns']; + + $query['openid.ns'] = Auth_OpenID_OPENID2_NS; + $message = Auth_OpenID_Message::fromPostArgs($query); + return new Auth_OpenID_ServerError( + $message, + sprintf("Invalid OpenID namespace URI: %s", $old_ns)); + } + $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode'); if (!$mode) { return new Auth_OpenID_ServerError($message, "No mode value in message"); } + if (Auth_OpenID::isFailure($mode)) { + return new Auth_OpenID_ServerError($message, + $mode->message); + } + $handlerCls = Auth_OpenID::arrayGet($this->handlers, $mode, $this->defaultDecoder($message)); @@ -1477,8 +1582,14 @@ class Auth_OpenID_Decoder { function defaultDecoder($message) { $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode'); + + if (Auth_OpenID::isFailure($mode)) { + return new Auth_OpenID_ServerError($message, + $mode->message); + } + return new Auth_OpenID_ServerError($message, - sprintf("No decoder for mode %s", $mode)); + sprintf("Unrecognized OpenID mode %s", $mode)); } } @@ -1590,7 +1701,6 @@ class Auth_OpenID_Server { $handler = array($this, "openid_" . $request->mode); return call_user_func($handler, $request); } - error_log('Method did not exist ' . "openid_" . $request->mode); return null; } diff --git a/lib/Auth/OpenID/ServerRequest.php b/lib/Auth/OpenID/ServerRequest.php index 00728941a5a60f23d38c6dd116c6c34b0a1d44e4..33a8556ceaacd102602d3f1ab6c893c9f4c586ca 100644 --- a/lib/Auth/OpenID/ServerRequest.php +++ b/lib/Auth/OpenID/ServerRequest.php @@ -10,8 +10,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** diff --git a/lib/Auth/OpenID/TrustRoot.php b/lib/Auth/OpenID/TrustRoot.php index 88eff295d7300fe5ae540759c5a9d3e90d175659..4919a60651d3c76c83e5366325b9d3568bfe97a6 100644 --- a/lib/Auth/OpenID/TrustRoot.php +++ b/lib/Auth/OpenID/TrustRoot.php @@ -8,10 +8,12 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ +require_once 'Auth/OpenID/Discover.php'; + /** * A regular expression that matches a domain ending in a top-level domains. * Used in checking trust roots for sanity. @@ -19,23 +21,66 @@ * @access private */ define('Auth_OpenID___TLDs', - '/\.(com|edu|gov|int|mil|net|org|biz|info|name|museum|coop|aero|ac|' . - 'ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|az|ba|bb|bd|be|bf|bg|' . - 'bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|' . - 'cm|cn|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|' . - 'fi|fj|fk|fm|fo|fr|ga|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|' . - 'gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|' . - 'ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|' . - 'ma|mc|md|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|' . - 'nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|' . - 'ps|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|' . - 'so|sr|st|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tm|tn|to|tp|tr|tt|tv|tw|tz|' . - 'ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)$/'); + '/\.(ac|ad|ae|aero|af|ag|ai|al|am|an|ao|aq|ar|arpa|as|asia' . + '|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|biz|bj|bm|bn|bo|br' . + '|bs|bt|bv|bw|by|bz|ca|cat|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co' . + '|com|coop|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|edu|ee|eg' . + '|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl' . + '|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie' . + '|il|im|in|info|int|io|iq|ir|is|it|je|jm|jo|jobs|jp|ke|kg|kh' . + '|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly' . + '|ma|mc|md|me|mg|mh|mil|mk|ml|mm|mn|mo|mobi|mp|mq|mr|ms|mt' . + '|mu|museum|mv|mw|mx|my|mz|na|name|nc|ne|net|nf|ng|ni|nl|no' . + '|np|nr|nu|nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|pro|ps|pt' . + '|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl' . + '|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tel|tf|tg|th|tj|tk|tl|tm' . + '|tn|to|tp|tr|travel|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve' . + '|vg|vi|vn|vu|wf|ws|xn--0zwm56d|xn--11b5bs3a9aj6g' . + '|xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--g6w251d' . + '|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--jxalpdlp' . + '|xn--kgbechtv|xn--zckzah|ye|yt|yu|za|zm|zw)\.?$/'); + +define('Auth_OpenID___HostSegmentRe', + "/^(?:[-a-zA-Z0-9!$&'\\(\\)\\*+,;=._~]|%[a-zA-Z0-9]{2})*$/"); /** * A wrapper for trust-root related functions */ class Auth_OpenID_TrustRoot { + /* + * Return a discovery URL for this realm. + * + * Return null if the realm could not be parsed or was not valid. + * + * @param return_to The relying party return URL of the OpenID + * authentication request + * + * @return The URL upon which relying party discovery should be + * run in order to verify the return_to URL + */ + function buildDiscoveryURL($realm) + { + $parsed = Auth_OpenID_TrustRoot::_parse($realm); + + if ($parsed === false) { + return false; + } + + if ($parsed['wildcard']) { + // Use "www." in place of the star + if ($parsed['host'][0] != '.') { + return false; + } + + $www_domain = 'www' . $parsed['host']; + + return sprintf('%s://%s%s', $parsed['scheme'], + $www_domain, $parsed['path']); + } else { + return $parsed['unparsed']; + } + } + /** * Parse a URL into its trust_root parts. * @@ -50,10 +95,20 @@ class Auth_OpenID_TrustRoot { */ function _parse($trust_root) { + $trust_root = Auth_OpenID_urinorm($trust_root); + if ($trust_root === null) { + return false; + } + + if (preg_match("/:\/\/[^:]+(:\d+){2,}(\/|$)/", $trust_root)) { + return false; + } + $parts = @parse_url($trust_root); if ($parts === false) { return false; } + $required_parts = array('scheme', 'host'); $forbidden_parts = array('user', 'pass', 'fragment'); $keys = array_keys($parts); @@ -65,9 +120,7 @@ class Auth_OpenID_TrustRoot { return false; } - // Return false if the original trust root value has more than - // one port specification. - if (preg_match("/:\/\/[^:]+(:\d+){2,}(\/|$)/", $trust_root)) { + if (!preg_match(Auth_OpenID___HostSegmentRe, $parts['host'])) { return false; } @@ -103,16 +156,21 @@ class Auth_OpenID_TrustRoot { if (isset($parts['path'])) { $path = strtolower($parts['path']); - if (substr($path, -1) != '/') { - $path .= '/'; + if (substr($path, 0, 1) != '/') { + return false; } } else { $path = '/'; } + $parts['path'] = $path; if (!isset($parts['port'])) { $parts['port'] = false; } + + + $parts['unparsed'] = $trust_root; + return $parts; } @@ -152,6 +210,25 @@ class Auth_OpenID_TrustRoot { if ($parts['host'] == 'localhost') { return true; } + + $host_parts = explode('.', $parts['host']); + if ($parts['wildcard']) { + // Remove the empty string from the beginning of the array + array_shift($host_parts); + } + + if ($host_parts && !$host_parts[count($host_parts) - 1]) { + array_pop($host_parts); + } + + if (!$host_parts) { + return false; + } + + // Don't allow adjacent dots + if (in_array('', $host_parts, true)) { + return false; + } // Get the top-level domain of the host. If it is not a valid TLD, // it's not sane. @@ -161,19 +238,20 @@ class Auth_OpenID_TrustRoot { } $tld = $matches[1]; - // Require at least two levels of specificity for non-country - // tlds and three levels for country tlds. - $elements = explode('.', $parts['host']); - $n = count($elements); - if ($parts['wildcard']) { - $n -= 1; - } - if (strlen($tld) == 2) { - $n -= 1; - } - if ($n <= 1) { + if (count($host_parts) == 1) { return false; } + + if ($parts['wildcard']) { + // It's a 2-letter tld with a short second to last segment + // so there needs to be more than two segments specified + // (e.g. *.co.uk is insane) + $second_level = $host_parts[count($host_parts) - 2]; + if (strlen($tld) == 2 && strlen($second_level) <= 3) { + return count($host_parts) > 2; + } + } + return true; } @@ -221,8 +299,14 @@ class Auth_OpenID_TrustRoot { $base_path = $trust_root_parsed['path']; $path = $url_parsed['path']; if (!isset($trust_root_parsed['query'])) { - if (substr($path, 0, strlen($base_path)) != $base_path) { - return false; + if ($base_path != $path) { + if (substr($path, 0, strlen($base_path)) != $base_path) { + return false; + } + if (substr($base_path, strlen($base_path) - 1, 1) != '/' && + substr($path, strlen($base_path), 1) != '/') { + return false; + } } } else { $base_query = $trust_root_parsed['query']; @@ -240,4 +324,139 @@ class Auth_OpenID_TrustRoot { $url_parsed['port'] === $trust_root_parsed['port']); } } + +/* + * If the endpoint is a relying party OpenID return_to endpoint, + * return the endpoint URL. Otherwise, return None. + * + * This function is intended to be used as a filter for the Yadis + * filtering interface. + * + * @see: C{L{openid.yadis.services}} + * @see: C{L{openid.yadis.filters}} + * + * @param endpoint: An XRDS BasicServiceEndpoint, as returned by + * performing Yadis dicovery. + * + * @returns: The endpoint URL or None if the endpoint is not a + * relying party endpoint. + */ +function filter_extractReturnURL(&$endpoint) +{ + if ($endpoint->matchTypes(array(Auth_OpenID_RP_RETURN_TO_URL_TYPE))) { + return $endpoint; + } else { + return null; + } +} + +function &Auth_OpenID_extractReturnURL(&$endpoint_list) +{ + $result = array(); + + foreach ($endpoint_list as $endpoint) { + if (filter_extractReturnURL($endpoint)) { + $result[] = $endpoint; + } + } + + return $result; +} + +/* + * Is the return_to URL under one of the supplied allowed return_to + * URLs? + */ +function Auth_OpenID_returnToMatches($allowed_return_to_urls, $return_to) +{ + foreach ($allowed_return_to_urls as $allowed_return_to) { + // A return_to pattern works the same as a realm, except that + // it's not allowed to use a wildcard. We'll model this by + // parsing it as a realm, and not trying to match it if it has + // a wildcard. + + $return_realm = Auth_OpenID_TrustRoot::_parse($allowed_return_to); + if (// Parses as a trust root + ($return_realm !== false) && + // Does not have a wildcard + (!$return_realm['wildcard']) && + // Matches the return_to that we passed in with it + (Auth_OpenID_TrustRoot::match($allowed_return_to, $return_to))) { + return true; + } + } + + // No URL in the list matched + return false; +} + +/* + * Given a relying party discovery URL return a list of return_to + * URLs. + */ +function Auth_OpenID_getAllowedReturnURLs($relying_party_url, &$fetcher, + $discover_function=null) +{ + if ($discover_function === null) { + $discover_function = array('Auth_Yadis_Yadis', 'discover'); + } + + $xrds_parse_cb = array('Auth_OpenID_ServiceEndpoint', 'fromXRDS'); + + list($rp_url_after_redirects, $endpoints) = + Auth_Yadis_getServiceEndpoints($relying_party_url, $xrds_parse_cb, + $discover_function, $fetcher); + + if ($rp_url_after_redirects != $relying_party_url) { + // Verification caused a redirect + return false; + } + + call_user_func_array($discover_function, + array($relying_party_url, $fetcher)); + + $return_to_urls = array(); + $matching_endpoints = Auth_OpenID_extractReturnURL($endpoints); + + foreach ($matching_endpoints as $e) { + $return_to_urls[] = $e->server_url; + } + + return $return_to_urls; +} + +/* + * Verify that a return_to URL is valid for the given realm. + * + * This function builds a discovery URL, performs Yadis discovery on + * it, makes sure that the URL does not redirect, parses out the + * return_to URLs, and finally checks to see if the current return_to + * URL matches the return_to. + * + * @return true if the return_to URL is valid for the realm + */ +function Auth_OpenID_verifyReturnTo($realm_str, $return_to, &$fetcher, + $_vrfy='Auth_OpenID_getAllowedReturnURLs') +{ + $disco_url = Auth_OpenID_TrustRoot::buildDiscoveryURL($realm_str); + + if ($disco_url === false) { + return false; + } + + $allowable_urls = call_user_func_array($_vrfy, + array($disco_url, &$fetcher)); + + // The realm_str could not be parsed. + if ($allowable_urls === false) { + return false; + } + + if (Auth_OpenID_returnToMatches($allowable_urls, $return_to)) { + return true; + } else { + return false; + } +} + ?> \ No newline at end of file diff --git a/lib/Auth/OpenID/URINorm.php b/lib/Auth/OpenID/URINorm.php index 60e900a4c1737fb0967e396c40ccd418e3f4a5ef..f821d836a90b2256bb06727d45c629a408535813 100644 --- a/lib/Auth/OpenID/URINorm.php +++ b/lib/Auth/OpenID/URINorm.php @@ -5,8 +5,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ require_once 'Auth/Yadis/Misc.php'; @@ -27,6 +27,17 @@ function Auth_OpenID_getEncodedPattern() return '/%([0-9A-Fa-f]{2})/'; } +# gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" +# +# sub-delims = "!" / "$" / "&" / "'" / "(" / ")" +# / "*" / "+" / "," / ";" / "=" +# +# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +function Auth_OpenID_getURLIllegalCharRE() +{ + return "/([^-A-Za-z0-9:\/\?#\[\]@\!\$&'\(\)\*\+,;=\._~\%])/"; +} + function Auth_OpenID_getUnreserved() { $_unreserved = array(); @@ -88,7 +99,7 @@ function Auth_OpenID_pct_encoded_replace($mo) function Auth_OpenID_remove_dot_segments($path) { $result_segments = array(); - + while ($path) { if (Auth_Yadis_startswith($path, '../')) { $path = substr($path, 3); @@ -139,6 +150,13 @@ function Auth_OpenID_urinorm($uri) } } + $illegal_matches = array(); + preg_match(Auth_OpenID_getURLIllegalCharRE(), + $uri, $illegal_matches); + if ($illegal_matches) { + return null; + } + $scheme = $uri_matches[2]; if ($scheme) { $scheme = strtolower($scheme); diff --git a/lib/Auth/Yadis/HTTPFetcher.php b/lib/Auth/Yadis/HTTPFetcher.php index 4b461404fce27c7396635c79aafb19357d83e133..963b9a49a48f50495e561be4863957b61aa377a3 100644 --- a/lib/Auth/Yadis/HTTPFetcher.php +++ b/lib/Auth/Yadis/HTTPFetcher.php @@ -9,10 +9,19 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ +/** + * Require logging functionality + */ +require_once "Auth/OpenID.php"; + +define('Auth_OpenID_FETCHER_MAX_RESPONSE_KB', 1024); +define('Auth_OpenID_USER_AGENT', + 'php-openid/'.Auth_OpenID_VERSION.' (php/'.phpversion().')'); + class Auth_Yadis_HTTPResponse { function Auth_Yadis_HTTPResponse($final_url = null, $status = null, $headers = null, $body = null) @@ -36,6 +45,30 @@ class Auth_Yadis_HTTPFetcher { var $timeout = 20; // timeout in seconds. + /** + * Return whether a URL can be fetched. Returns false if the URL + * scheme is not allowed or is not supported by this fetcher + * implementation; returns true otherwise. + * + * @return bool + */ + function canFetchURL($url) + { + if ($this->isHTTPS($url) && !$this->supportsSSL()) { + Auth_OpenID::log("HTTPS URL unsupported fetching %s", + $url); + return false; + } + + if (!$this->allowedURL($url)) { + Auth_OpenID::log("URL fetching not allowed for '%s'", + $url); + return false; + } + + return true; + } + /** * Return whether a URL should be allowed. Override this method to * conform to your local policy. @@ -85,7 +118,7 @@ class Auth_Yadis_HTTPFetcher { function _findRedirect($headers) { foreach ($headers as $line) { - if (strpos($line, "Location: ") === 0) { + if (strpos(strtolower($line), "location: ") === 0) { $parts = explode(" ", $line, 2); return $parts[1]; } @@ -105,7 +138,7 @@ class Auth_Yadis_HTTPFetcher { * pass the URLHasAllowedScheme check or if the server's response * is malformed. */ - function get($url, $headers) + function get($url, $headers = null) { trigger_error("not implemented", E_USER_ERROR); } diff --git a/lib/Auth/Yadis/Manager.php b/lib/Auth/Yadis/Manager.php index bb60b8ae244452d40c3db7cda343c791c4c9de1c..d50cf7ad65eb697f1ab8ef745433338bc1c04f92 100644 --- a/lib/Auth/Yadis/Manager.php +++ b/lib/Auth/Yadis/Manager.php @@ -361,7 +361,7 @@ class Auth_Yadis_Manager { * * High-level usage pattern is to call .getNextService(discover) in * order to find the next available service for this user for this - * session. Once a request completes, call .finish() to clean up the + * session. Once a request completes, call .cleanup() to clean up the * session state. * * @package OpenID @@ -410,7 +410,6 @@ class Auth_Yadis_Discovery { $manager = $this->getManager(); if (!$manager || (!$manager->services)) { $this->destroyManager(); - $http_response = array(); list($yadis_url, $services) = call_user_func($discover_cb, $this->url, @@ -435,13 +434,16 @@ class Auth_Yadis_Discovery { * Clean up Yadis-related services in the session and return the * most-recently-attempted service from the manager, if one * exists. + * + * @param $force True if the manager should be deleted regardless + * of whether it's a manager for $this->url. */ - function cleanup() + function cleanup($force=false) { - $manager = $this->getManager(); + $manager = $this->getManager($force); if ($manager) { $service = $manager->current(); - $this->destroyManager(); + $this->destroyManager($force); } else { $service = null; } @@ -460,8 +462,11 @@ class Auth_Yadis_Discovery { /** * @access private + * + * @param $force True if the manager should be returned regardless + * of whether it's a manager for $this->url. */ - function &getManager() + function &getManager($force=false) { // Extract the YadisServiceManager for this object's URL and // suffix from the session. @@ -474,7 +479,7 @@ class Auth_Yadis_Discovery { $manager = $loader->fromSession(unserialize($manager_str)); } - if ($manager && $manager->forURL($this->url)) { + if ($manager && ($manager->forURL($this->url) || $force)) { return $manager; } else { $unused = null; @@ -508,10 +513,13 @@ class Auth_Yadis_Discovery { /** * @access private + * + * @param $force True if the manager should be deleted regardless + * of whether it's a manager for $this->url. */ - function destroyManager() + function destroyManager($force=false) { - if ($this->getManager() !== null) { + if ($this->getManager($force) !== null) { $key = $this->getSessionKey(); $this->session->del($key); } diff --git a/lib/Auth/Yadis/Misc.php b/lib/Auth/Yadis/Misc.php index a29ce4a00b5e13b1a3db5d92f8249d991fe5d672..1134a4ff4bc729f4ab2172848c4d212d80a12438 100644 --- a/lib/Auth/Yadis/Misc.php +++ b/lib/Auth/Yadis/Misc.php @@ -5,8 +5,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ function Auth_Yadis_getUCSChars() diff --git a/lib/Auth/Yadis/ParanoidHTTPFetcher.php b/lib/Auth/Yadis/ParanoidHTTPFetcher.php index 08aced69218a2d220021db86c9ac26535d2bbebb..6a418260eefebfa409768d5d750e1026457ff4dd 100644 --- a/lib/Auth/Yadis/ParanoidHTTPFetcher.php +++ b/lib/Auth/Yadis/ParanoidHTTPFetcher.php @@ -9,8 +9,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** @@ -18,6 +18,8 @@ */ require_once "Auth/Yadis/HTTPFetcher.php"; +require_once "Auth/OpenID.php"; + /** * A paranoid {@link Auth_Yadis_HTTPFetcher} class which uses CURL * for fetching. @@ -50,8 +52,12 @@ class Auth_Yadis_ParanoidHTTPFetcher extends Auth_Yadis_HTTPFetcher { */ function _writeData($ch, $data) { - $this->data .= $data; - return strlen($data); + if (strlen($this->data) > 1024*Auth_OpenID_FETCHER_MAX_RESPONSE_KB) { + return 0; + } else { + $this->data .= $data; + return strlen($data); + } } /** @@ -60,12 +66,18 @@ class Auth_Yadis_ParanoidHTTPFetcher extends Auth_Yadis_HTTPFetcher { function supportsSSL() { $v = curl_version(); - return in_array('https', $v['protocols']); + if(is_array($v)) { + return in_array('https', $v['protocols']); + } elseif (is_string($v)) { + return preg_match('/OpenSSL/i', $v); + } else { + return 0; + } } function get($url, $extra_headers = null) { - if ($this->isHTTPS($url) && !$this->supportsSSL()) { + if (!$this->canFetchURL($url)) { return null; } @@ -78,11 +90,21 @@ class Auth_Yadis_ParanoidHTTPFetcher extends Auth_Yadis_HTTPFetcher { $this->reset(); $c = curl_init(); + + if ($c === false) { + Auth_OpenID::log( + "curl_init returned false; could not " . + "initialize for URL '%s'", $url); + return null; + } + if (defined('CURLOPT_NOSIGNAL')) { curl_setopt($c, CURLOPT_NOSIGNAL, true); } if (!$this->allowedURL($url)) { + Auth_OpenID::log("Fetching URL not allowed: %s", + $url); return null; } @@ -95,6 +117,14 @@ class Auth_Yadis_ParanoidHTTPFetcher extends Auth_Yadis_HTTPFetcher { curl_setopt($c, CURLOPT_HTTPHEADER, $extra_headers); } + $cv = curl_version(); + if(is_array($cv)) { + $curl_user_agent = 'curl/'.$cv['version']; + } else { + $curl_user_agent = $cv; + } + curl_setopt($c, CURLOPT_USERAGENT, + Auth_OpenID_USER_AGENT.' '.$curl_user_agent); curl_setopt($c, CURLOPT_TIMEOUT, $off); curl_setopt($c, CURLOPT_URL, $url); @@ -105,6 +135,9 @@ class Auth_Yadis_ParanoidHTTPFetcher extends Auth_Yadis_HTTPFetcher { $headers = $this->headers; if (!$code) { + Auth_OpenID::log("Got no response code when fetching %s", $url); + Auth_OpenID::log("CURL error (%s): %s", + curl_errno($c), curl_error($c)); return null; } @@ -118,12 +151,16 @@ class Auth_Yadis_ParanoidHTTPFetcher extends Auth_Yadis_HTTPFetcher { $new_headers = array(); foreach ($headers as $header) { - if (preg_match("/:/", $header)) { - list($name, $value) = explode(": ", $header, 2); + if (strpos($header, ': ')) { + list($name, $value) = explode(': ', $header, 2); $new_headers[$name] = $value; } } + Auth_OpenID::log( + "Successfully fetched '%s': GET response code %s", + $url, $code); + return new Auth_Yadis_HTTPResponse($url, $code, $new_headers, $body); } @@ -136,19 +173,18 @@ class Auth_Yadis_ParanoidHTTPFetcher extends Auth_Yadis_HTTPFetcher { function post($url, $body, $extra_headers = null) { - $this->reset(); - - if ($this->isHTTPS($url) && !$this->supportsSSL()) { + if (!$this->canFetchURL($url)) { return null; } - if (!$this->allowedURL($url)) { - return null; - } + $this->reset(); $c = curl_init(); - curl_setopt($c, CURLOPT_NOSIGNAL, true); + if (defined('CURLOPT_NOSIGNAL')) { + curl_setopt($c, CURLOPT_NOSIGNAL, true); + } + curl_setopt($c, CURLOPT_POST, true); curl_setopt($c, CURLOPT_POSTFIELDS, $body); curl_setopt($c, CURLOPT_TIMEOUT, $this->timeout); @@ -161,6 +197,7 @@ class Auth_Yadis_ParanoidHTTPFetcher extends Auth_Yadis_HTTPFetcher { $code = curl_getinfo($c, CURLINFO_HTTP_CODE); if (!$code) { + Auth_OpenID::log("Got no response code when fetching %s", $url); return null; } @@ -168,20 +205,19 @@ class Auth_Yadis_ParanoidHTTPFetcher extends Auth_Yadis_HTTPFetcher { curl_close($c); - if ($extra_headers === null) { - $new_headers = null; - } else { - $new_headers = $extra_headers; - } + $new_headers = $extra_headers; foreach ($this->headers as $header) { - if (preg_match("/:/", $header)) { - list($name, $value) = explode(": ", $header, 2); + if (strpos($header, ': ')) { + list($name, $value) = explode(': ', $header, 2); $new_headers[$name] = $value; } } + Auth_OpenID::log("Successfully fetched '%s': POST response code %s", + $url, $code); + return new Auth_Yadis_HTTPResponse($url, $code, $new_headers, $body); } diff --git a/lib/Auth/Yadis/ParseHTML.php b/lib/Auth/Yadis/ParseHTML.php index 1922917ae681ad5f277a355c47ef858bdf1aefc5..297ccbd2c34d536a75f7bbc9fd6a1af09c1c624f 100644 --- a/lib/Auth/Yadis/ParseHTML.php +++ b/lib/Auth/Yadis/ParseHTML.php @@ -9,8 +9,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** @@ -41,7 +41,7 @@ class Auth_Yadis_ParseHTML { /** * @access private */ - var $_attr_find = '\b([-\w]+)=(".*?"|\'.*?\'|.+?)[\s>]'; + var $_attr_find = '\b([-\w]+)=(".*?"|\'.*?\'|.+?)[\/\s>]'; function Auth_Yadis_ParseHTML() { diff --git a/lib/Auth/Yadis/PlainHTTPFetcher.php b/lib/Auth/Yadis/PlainHTTPFetcher.php index 15dc3a7385c22bd65251ad0843eb2c161b2d5329..3e0ca2bb0c7302f5496244f4d9b36bd3fc1ce56a 100644 --- a/lib/Auth/Yadis/PlainHTTPFetcher.php +++ b/lib/Auth/Yadis/PlainHTTPFetcher.php @@ -10,8 +10,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** @@ -36,11 +36,7 @@ class Auth_Yadis_PlainHTTPFetcher extends Auth_Yadis_HTTPFetcher { function get($url, $extra_headers = null) { - if ($this->isHTTPS($url) && !$this->supportsSSL()) { - return null; - } - - if (!$this->allowedURL($url)) { + if (!$this->canFetchURL($url)) { return null; } @@ -67,13 +63,17 @@ class Auth_Yadis_PlainHTTPFetcher extends Auth_Yadis_HTTPFetcher { } } + if (!array_key_exists('path', $parts)) { + $parts['path'] = '/'; + } + $host = $parts['host']; if ($parts['scheme'] == 'https') { $host = 'ssl://' . $host; } - $user_agent = "PHP Yadis Library Fetcher"; + $user_agent = Auth_OpenID_USER_AGENT; $headers = array( "GET ".$parts['path']. @@ -105,8 +105,11 @@ class Auth_Yadis_PlainHTTPFetcher extends Auth_Yadis_HTTPFetcher { fputs($sock, implode("\r\n", $headers) . "\r\n\r\n"); $data = ""; - while (!feof($sock)) { + $kilobytes = 0; + while (!feof($sock) && + $kilobytes < Auth_OpenID_FETCHER_MAX_RESPONSE_KB ) { $data .= fgets($sock, 1024); + $kilobytes += 1; } fclose($sock); @@ -132,8 +135,12 @@ class Auth_Yadis_PlainHTTPFetcher extends Auth_Yadis_HTTPFetcher { foreach ($headers as $header) { if (preg_match("/:/", $header)) { - list($name, $value) = explode(": ", $header, 2); - $new_headers[$name] = $value; + $parts = explode(": ", $header, 2); + + if (count($parts) == 2) { + list($name, $value) = $parts; + $new_headers[$name] = $value; + } } } @@ -143,11 +150,7 @@ class Auth_Yadis_PlainHTTPFetcher extends Auth_Yadis_HTTPFetcher { function post($url, $body, $extra_headers = null) { - if ($this->isHTTPS($url) && !$this->supportsSSL()) { - return null; - } - - if (!$this->allowedURL($url)) { + if (!$this->canFetchURL($url)) { return null; } diff --git a/lib/Auth/Yadis/XML.php b/lib/Auth/Yadis/XML.php index 4854f12bbcd2e3ca5d4f382a16e9491da0ee611b..81b2ce2210a2332a7a82b202b7e3c244542ab3f9 100644 --- a/lib/Auth/Yadis/XML.php +++ b/lib/Auth/Yadis/XML.php @@ -91,7 +91,7 @@ class Auth_Yadis_XMLParser { * @return array $node_list An array of matching opaque node * objects to be used with other methods of this parser class. */ - function evalXPath($xpath, $node = null) + function &evalXPath($xpath, $node = null) { // Not implemented. } diff --git a/lib/Auth/Yadis/XRDS.php b/lib/Auth/Yadis/XRDS.php index 127bc961ce5e8a08cf6a4856e8db341cd6f3eec4..f14a7948e1a4379e61da6b1be9b3493182129d48 100644 --- a/lib/Auth/Yadis/XRDS.php +++ b/lib/Auth/Yadis/XRDS.php @@ -9,8 +9,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** @@ -204,6 +204,36 @@ class Auth_Yadis_Service { } } +/* + * Return the expiration date of this XRD element, or None if no + * expiration was specified. + * + * @param $default The value to use as the expiration if no expiration + * was specified in the XRD. + */ +function Auth_Yadis_getXRDExpiration($xrd_element, $default=null) +{ + $expires_element = $xrd_element->$parser->evalXPath('/xrd:Expires'); + if ($expires_element === null) { + return $default; + } else { + $expires_string = $expires_element->text; + + // Will raise ValueError if the string is not the expected + // format + $t = strptime($expires_string, "%Y-%m-%dT%H:%M:%SZ"); + + if ($t === false) { + return false; + } + + // [int $hour [, int $minute [, int $second [, + // int $month [, int $day [, int $year ]]]]]] + return mktime($t['tm_hour'], $t['tm_min'], $t['tm_sec'], + $t['tm_mon'], $t['tm_day'], $t['tm_year']); + } +} + /** * This class performs parsing of XRDS documents. * diff --git a/lib/Auth/Yadis/XRI.php b/lib/Auth/Yadis/XRI.php index d9d0726342697d668d17d9caac84ec46ea90e8c2..4e346231767b47e13f54642fd8a315b93a5681ee 100644 --- a/lib/Auth/Yadis/XRI.php +++ b/lib/Auth/Yadis/XRI.php @@ -5,8 +5,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ require_once 'Auth/Yadis/Misc.php'; @@ -15,7 +15,7 @@ require_once 'Auth/OpenID.php'; function Auth_Yadis_getDefaultProxy() { - return 'http://proxy.xri.net/'; + return 'http://xri.net/'; } function Auth_Yadis_getXRIAuthorities() @@ -43,7 +43,8 @@ function Auth_Yadis_getXrefRE() function Auth_Yadis_identifierScheme($identifier) { if (Auth_Yadis_startswith($identifier, 'xri://') || - (in_array($identifier[0], Auth_Yadis_getXRIAuthorities()))) { + ($identifier && + in_array($identifier[0], Auth_Yadis_getXRIAuthorities()))) { return "XRI"; } else { return "URI"; @@ -198,7 +199,7 @@ function Auth_Yadis_getCanonicalID($iname, $xrds) return false; } - $canonicalID = $canonicalID_nodes[count($canonicalID_nodes) - 1]; + $canonicalID = $canonicalID_nodes[0]; $canonicalID = Auth_Yadis_XRI($parser->content($canonicalID)); $childID = $canonicalID; @@ -207,13 +208,13 @@ function Auth_Yadis_getCanonicalID($iname, $xrds) $xrd = $xrd_list[$i]; $parent_sought = substr($childID, 0, strrpos($childID, '!')); - $parent_list = array(); - - foreach ($parser->evalXPath('xrd:CanonicalID', $xrd) as $c) { - $parent_list[] = Auth_Yadis_XRI($parser->content($c)); + $parentCID = $parser->evalXPath('xrd:CanonicalID', $xrd); + if (!$parentCID) { + return false; } + $parentCID = Auth_Yadis_XRI($parser->content($parentCID[0])); - if (!in_array($parent_sought, $parent_list)) { + if (strcasecmp($parent_sought, $parentCID)) { // raise XRDSFraud. return false; } @@ -230,4 +231,4 @@ function Auth_Yadis_getCanonicalID($iname, $xrds) return $canonicalID; } -?> \ No newline at end of file +?> diff --git a/lib/Auth/Yadis/XRIRes.php b/lib/Auth/Yadis/XRIRes.php index b90591fe2a88ec39cb539f327a01e51388106a21..4e8e8d0372deb1bd308a2db4ca7dc8101dcfb3dd 100644 --- a/lib/Auth/Yadis/XRIRes.php +++ b/lib/Auth/Yadis/XRIRes.php @@ -44,7 +44,7 @@ class Auth_Yadis_ProxyResolver { foreach ($service_types as $service_type) { $url = $this->queryURL($xri, $service_type); $response = $this->fetcher->get($url); - if ($response->status != 200) { + if ($response->status != 200 and $response->status != 206) { continue; } $xrds = Auth_Yadis_XRDS::parseXRDS($response->body); @@ -69,4 +69,4 @@ class Auth_Yadis_ProxyResolver { } } -?> \ No newline at end of file +?> diff --git a/lib/Auth/Yadis/Yadis.php b/lib/Auth/Yadis/Yadis.php index 514703cd0ed7a716ddaabe61e88b657f6c4f29fa..d89f77c6d7c5f4cbf647c45661f91f8325941b17 100644 --- a/lib/Auth/Yadis/Yadis.php +++ b/lib/Auth/Yadis/Yadis.php @@ -9,8 +9,8 @@ * * @package OpenID * @author JanRain, Inc. <openid@janrain.com> - * @copyright 2005 Janrain, Inc. - * @license http://www.gnu.org/copyleft/lesser.html LGPL + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** @@ -105,7 +105,7 @@ class Auth_Yadis_DiscoveryResult { function usedYadisLocation() { // Was the Yadis protocol's indirection used? - return $this->normalized_uri == $this->xrds_uri; + return $this->normalized_uri != $this->xrds_uri; } function isXRDS() @@ -116,6 +116,48 @@ class Auth_Yadis_DiscoveryResult { } } +/** + * + * Perform the Yadis protocol on the input URL and return an iterable + * of resulting endpoint objects. + * + * input_url: The URL on which to perform the Yadis protocol + * + * @return: The normalized identity URL and an iterable of endpoint + * objects generated by the filter function. + * + * xrds_parse_func: a callback which will take (uri, xrds_text) and + * return an array of service endpoint objects or null. Usually + * array('Auth_OpenID_ServiceEndpoint', 'fromXRDS'). + * + * discover_func: if not null, a callback which should take (uri) and + * return an Auth_Yadis_Yadis object or null. + */ +function Auth_Yadis_getServiceEndpoints($input_url, $xrds_parse_func, + $discover_func=null, $fetcher=null) +{ + if ($discover_func === null) { + $discover_function = array('Auth_Yadis_Yadis', 'discover'); + } + + $yadis_result = call_user_func_array($discover_func, + array($input_url, $fetcher)); + + if ($yadis_result === null) { + return array($input_url, array()); + } + + $endpoints = call_user_func_array($xrds_parse_func, + array($yadis_result->normalized_uri, + $yadis_result->response_text)); + + if ($endpoints === null) { + $endpoints = array(); + } + + return array($yadis_result->normalized_uri, $endpoints); +} + /** * This is the core of the PHP Yadis library. This is the only class * a user needs to use to perform Yadis discovery. This class @@ -281,15 +323,17 @@ class Auth_Yadis_Yadis { $result = new Auth_Yadis_DiscoveryResult($uri); $request_uri = $uri; - $headers = array("Accept: " . Auth_Yadis_CONTENT_TYPE); + $headers = array("Accept: " . Auth_Yadis_CONTENT_TYPE . + ', text/html; q=0.3, application/xhtml+xml; q=0.5'); - if (!$fetcher) { + if ($fetcher === null) { $fetcher = Auth_Yadis_Yadis::getHTTPFetcher($timeout); } $response = $fetcher->get($uri, $headers); - if (!$response || ($response->status != 200)) { + if (!$response || ($response->status != 200 and + $response->status != 206)) { $result->fail(); return $result; } @@ -318,7 +362,8 @@ class Auth_Yadis_Yadis { $response = $fetcher->get($yadis_location); - if ($response->status != 200) { + if ((!$response) || ($response->status != 200 and + $response->status != 206)) { $result->fail(); return $result; } @@ -334,4 +379,4 @@ class Auth_Yadis_Yadis { } } -?> \ No newline at end of file +?> diff --git a/modules/openid/www/consumer.php b/modules/openid/www/consumer.php index c697cdedc4f67bca9fd991ac46d198d9aa915a96..f0ca314f939ef1d745e424359a48c7f05f78cd70 100644 --- a/modules/openid/www/consumer.php +++ b/modules/openid/www/consumer.php @@ -146,9 +146,11 @@ function run_finish_auth() { $consumer = getConsumer(); + $return_to = SimpleSAML_Utilities::selfURL(); + // Complete the authentication process using the server's // response. - $response = $consumer->complete(); + $response = $consumer->complete($return_to); // Check the response status. if ($response->status == Auth_OpenID_CANCEL) {